feat(control-lane): forwarded events — the control instance hands a verified delivery to the instance that consumes it - #5797
Conversation
…erified delivery to the instance that consumes it
The lane's third kind beside a request and a report (Doc/Architecture/ControlLane,
"Forwarded events"). First use: the ONE GitHub organisation webhook posts pull-request and
check events to the control instance, which forwards them to the build instance's PR
steward heal/observe half (MeshWeaver.Plugins#2439, policy pr-babysitter-cadence) —
build gets no second hook.
- ControlLaneEvent ("kind": "control-lane-event"): eventId, deployment, source (open
vocabulary ControlLaneEventSource), name, target, payload verbatim, window. Signed with the
target deployment's OWN key, exactly like a request (ControlLaneClient.Forward/NewEvent).
- Target: same endpoint, same checks 1-2 (armed, signature); then ControlLaneEvents.Admit
(envelope, this deployment, window <= 15 min, a target DECLARED under
ControlLane:EventTargets — deliberately not the public WebhookInbox list — and the size cap)
and Store: {target}/_Inbox/{eventId} as a WebhookEvent; its creation is the single-use claim
(replay -> 409).
- It carries no operation, runs nothing, needs no approval and writes no report: its whole
effect is one node in an inbox the target opened to the lane, whose consumer re-reads live
state. The stored node carries no X-Hub-Signature-256, so an HMAC-verifying consumer drops a
misrouted one.
- ControlLaneTest: 3 new cases (stored verbatim once / replay; undeclared inbox, other
deployment, fleet or other key, expired all refused and store nothing; an event is neither a
request nor a report and vice versa). 16/16 locally; negative control (ignoring the declared
target list) reds 2.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Address the eventual-consistency delivery-loss risk and update the test helper to use the authoritative node stream.
Review effort: Lite
Findings: 1
Open (2)
What changed in this PR
Adds signed control-to-instance forwarded events for verified webhook delivery into declared target inboxes.
Changes:
- Adds event creation, signing, validation, forwarding, replay protection, and storage.
- Extends the control-lane receiver and client.
- Adds integration tests and protocol documentation.
| File | Summary |
|---|---|
test/MeshWeaver.Graph.Test/ControlLaneTest.cs |
Tests forwarding, replay protection, invalid targets, signatures, and expiry; includes a potentially flaky eventually consistent read. |
src/MeshWeaver.Graph/ControlLane/ControlLaneReceiver.cs |
Routes received forwarded events through admission and storage. |
src/MeshWeaver.Graph/ControlLane/ControlLaneEvents.cs |
Validates events and persists inbox entries; exact-path existence checks can reject valid deliveries due to eventual consistency. |
src/MeshWeaver.Graph/ControlLane/ControlLaneEnvelopes.cs |
Parses event envelope discriminators. |
src/MeshWeaver.Graph/ControlLane/ControlLaneClient.cs |
Creates, signs, and forwards events. |
src/MeshWeaver.Documentation/Data/Architecture/ControlLane.md |
Documents forwarded events and configuration. |
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| return SpaceDeletion.AsSystem(hub, () => mesh.Query<MeshNode>(MeshQueryRequest.FromQuery($"path:{target}"))) | ||
| .Where(change => change.ChangeType is QueryChangeType.Initial or QueryChangeType.Reset) | ||
| .Take(1) | ||
| .Timeout(MeshReading.DefaultBudget) | ||
| .SelectMany(change => change.Items.Any(n => n.Path == target) |
There was a problem hiding this comment.
Agreed — fixed in 6d5fadc. Existence is now a LISTING of the target's parent's children (ControlLaneEvents.ExistenceQuery → MeshReading.Read), never an exact-path query; a listing that is not an answer (fault, no complete frame, silent provider, no partition) refuses BY NAME (whether the target … exists could not be established (…)) instead of reading as absent, and the control side logs every refusal at Warning. On 'permanently losing a valid webhook': the forwarded event is only a trigger — the consumer (the build instance's PR babysitter) re-reads live GitHub state on its own 30-minute sweep, so a refused forward delays a pass, it loses no state.
| var reading = await AsSystem(Mesh, () => MeshReading.Read(MeshQuery, $"path:{path} limit:1")); | ||
| return reading.Rows.FirstOrDefault(r => r.Path == path)?.ContentAs<WebhookEvent>(Mesh.JsonSerializerOptions); |
There was a problem hiding this comment.
Agreed — fixed in 6d5fadc. Stored now reads an ACCEPTED event through GetMeshNodeStream (authoritative, never the index), and the refusal cases use a separate AnyStored that lists the inbox's children and asserts IsAnswer before reading 'nothing stored'.
…ing that must be an answer; tests read an accepted event through its own stream Review on #5797: the exact-path query could lag the owner's creation and read as absent. Existence is now a listing of the target's parent's children (MeshReading), and a listing that is not an answer refuses BY NAME instead of reading as 'absent'. The test reads an accepted event through GetMeshNodeStream (the index may lag the create) and asserts a refused one left nothing through a listing that must be an answer. 16/16. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
⛔ Merge-queue steward: left dequeued — a job other than a test shard failed — a build or gate failure is never a flake.
Not a catalogued flake. Fix the failure, or — with run URLs, an issue and an assertion-message pattern — add it to |


Extends the signed control→instance lane (#5785) with its third kind, a forwarded event, instead of a new channel.
First use: the maintainer decision on the PR steward. There is ONE GitHub org webhook (686037663, Systemorph/MeshWeaver.Plugins#2438), which posts pull-request and check events to the control instance. The control instance forwards them to the build instance, where the steward's heal/observe half runs (Systemorph/MeshWeaver.Plugins#2439, policy
pr-babysitter-cadence, core #5795). Build gets no second hook.What
ControlLaneEvent("kind": "control-lane-event") carries:eventId,deployment,source(open vocabularyControlLaneEventSource) andnametarget: a local inbox ownerpayload: the verified body, verbatimIt is signed with the target deployment's own key, like a request. The control half is
ControlLaneClient.NewEvent+Forward: same key rule, transport and signed-acceptance check asSend.The target uses the same endpoint and shares checks 1–2 (armed, signature). Then:
ControlLaneEvents.Admitchecks the envelope, this deployment, a window ≤ 15 min, the payload size cap, and a target the instance declared underControlLane:EventTargets. That list is deliberately separate from the publicWebhookInbox:Targets, and empty means none.Storecreates{target}/_Inbox/{eventId}as aWebhookEvent. The creation is the single-use claim, so a replay gets 409.No operation, run, approval or report. Its whole effect is one node in an inbox the target opened to the lane, and the consumer re-reads live state. The stored node carries no
X-Hub-Signature-256, so an HMAC-verifying consumer (the platform-build watcher) drops a misrouted one.Doc:
Doc/Architecture/ControlLanegets a new section, "Forwarded events".Nothing is armed anywhere until a target mounts
ControlLane:Keyand declares an event target.Verification
dotnet build -c Release -warnaserror:MeshWeaver.GraphandMeshWeaver.Graph.Testbuild clean. The endpoint (Memex.Portal.Shared) is unchanged.ControlLaneTest: 16/16 over two meshes, 3 of them new:Pairs-with: none — additive only; no public type or member is removed.
Implementers: none — no interface member is added (
IControlLaneTransport/IControlLaneReportSinkunchanged).Mirror-sync: none — no i18n catalog key is added or re-worded.
🤖 Generated with Claude Code