Skip to content

feat(control-lane): forwarded events — the control instance hands a verified delivery to the instance that consumes it - #5797

Merged
rbuergi merged 2 commits into
mainfrom
feat/control-lane-forwarded-events
Sep 27, 2026
Merged

rbuergi merged 2 commits into
mainfrom
feat/control-lane-forwarded-events

Conversation

@rbuergi

@rbuergi rbuergi commented Sep 27, 2026

Copy link
Copy Markdown
Contributor

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 vocabulary ControlLaneEventSource) and name
    • target: a local inbox owner
    • payload: the verified body, verbatim
    • a validity window

    It 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 as Send.

  • The target uses the same endpoint and shares checks 1–2 (armed, signature). Then:

    • ControlLaneEvents.Admit checks the envelope, this deployment, a window ≤ 15 min, the payload size cap, and a target the instance declared under ControlLane:EventTargets. That list is deliberately separate from the public WebhookInbox:Targets, and empty means none.
    • Store creates {target}/_Inbox/{eventId} as a WebhookEvent. 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/ControlLane gets a new section, "Forwarded events".

Nothing is armed anywhere until a target mounts ControlLane:Key and declares an event target.

Verification

  • dotnet build -c Release -warnaserror: MeshWeaver.Graph and MeshWeaver.Graph.Test build clean. The endpoint (Memex.Portal.Shared) is unchanged.
  • ControlLaneTest: 16/16 over two meshes, 3 of them new:
    • an event is stored verbatim exactly once, and its replay is refused;
    • an undeclared inbox, another deployment, the fleet key or another deployment's key, and an expired window are each refused and store nothing;
    • an event cannot be read as a request or a report, and neither can be read as an event.
  • Negative control: removing the declared-target check reds 2 of the new cases.

Pairs-with: none — additive only; no public type or member is removed.
Implementers: none — no interface member is added (IControlLaneTransport/IControlLaneReportSink unchanged).
Mirror-sync: none — no i18n catalog key is added or re-worded.

🤖 Generated with Claude Code

…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>
Copilot AI lite review requested due to automatic review settings September 27, 2026 10:03

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 High severity · 1 Medium severity

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.

Comment on lines +193 to +197
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)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +534 to +535
var reading = await AsSystem(Mesh, () => MeshReading.Read(MeshQuery, $"path:{path} limit:1"));
return reading.Rows.FirstOrDefault(r => r.Path == path)?.ContentAs<WebhookEvent>(Mesh.JsonSerializerOptions);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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'.

@github-actions

github-actions Bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

410 tests   - 1 280   410 ✅  - 1 280   1m 1s ⏱️ - 3m 8s
  1 suites  -     1     0 💤 ±    0 
  1 files    -     1     0 ❌ ±    0 

Results for commit 6d5fadc. ± Comparison against base commit 2bb14d8.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 0)

  1 files  ±0    1 suites  ±0   3m 12s ⏱️ -2s
348 tests ±0  348 ✅ ±0  0 💤 ±0  0 ❌ ±0 
352 runs  ±0  352 ✅ ±0  0 💤 ±0  0 ❌ ±0 

Results for commit 6d5fadc. ± Comparison against base commit 2bb14d8.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 4)

    2 files   -   1      2 suites   - 1   3m 22s ⏱️ - 3m 0s
1 675 tests  - 501  1 675 ✅  - 501  0 💤 ±0  0 ❌ ±0 
1 675 runs   - 502  1 675 ✅  - 502  0 💤 ±0  0 ❌ ±0 

Results for commit 6d5fadc. ± Comparison against base commit 2bb14d8.

♻️ This comment has been updated with latest results.

…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>
@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 3)

997 tests  +537   997 ✅ +537   6m 8s ⏱️ + 5m 14s
  1 suites  -   2     0 💤 ±  0 
  1 files    -   2     0 ❌ ±  0 

Results for commit 6d5fadc. ± Comparison against base commit 2bb14d8.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 2)

    1 files   -     2      1 suites   - 2   5m 53s ⏱️ - 1m 31s
2 153 tests +1 391  2 153 ✅ +1 582  0 💤  - 191  0 ❌ ±0 
2 154 runs  +1 392  2 154 ✅ +1 583  0 💤  - 191  0 ❌ ±0 

Results for commit 6d5fadc. ± Comparison against base commit 2bb14d8.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 5)

    2 files   -     3      2 suites   - 3   8m 2s ⏱️ - 6m 1s
2 698 tests  - 1 560  2 698 ✅  - 1 558  0 💤  - 2  0 ❌ ±0 
2 702 runs   - 1 560  2 702 ✅  - 1 558  0 💤  - 2  0 ❌ ±0 

Results for commit 6d5fadc. ± Comparison against base commit 2bb14d8.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results

    8 files   -     9      8 suites   - 9   27m 41s ⏱️ - 8m 27s
8 281 tests  - 1 413  8 281 ✅  - 1 220  0 💤  - 193  0 ❌ ±0 
8 290 runs   - 1 413  8 290 ✅  - 1 220  0 💤  - 193  0 ❌ ±0 

Results for commit 6d5fadc. ± Comparison against base commit 2bb14d8.

@meshweaver-cloud
meshweaver-cloud Bot added this pull request to the merge queue Sep 27, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Sep 27, 2026
@meshweaver-cloud

Copy link
Copy Markdown
Contributor

⛔ Merge-queue steward: left dequeued — a job other than a test shard failed — a build or gate failure is never a flake.
Group build: https://github.com/Systemorph/MeshWeaver/actions/runs/36322819761

  • failed job: Dependent suites (MeshWeaver.Plugins)

Not a catalogued flake. Fix the failure, or — with run URLs, an issue and an assertion-message pattern — add it to .github/known-flakes.json (see Doc/Architecture/MergeQueue). Re-queue with gh pr merge <n> --auto once the head is green; label queue-rejected marks this PR as needing a person.

@meshweaver-cloud meshweaver-cloud Bot added the queue-rejected The merge queue rejected this PR on an uncatalogued failure; a person owns it now label Sep 27, 2026
@rbuergi
rbuergi added this pull request to the merge queue Sep 27, 2026
@rbuergi
rbuergi enabled auto-merge September 27, 2026 16:46
@rbuergi
rbuergi merged commit 758df96 into main Sep 27, 2026
52 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

queue-rejected The merge queue rejected this PR on an uncatalogued failure; a person owns it now

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants