LearnNewsExamplesServices
Frontmatter
id16233
titleNothing generates the wake-receiver route manifest, so the host edge cannot be provisioned without hand-authoring secrets
stateClosed
labels
enhancementai
assigneesneo-kimi-iris
createdAtJul 31, 2026, 7:58 PM
updatedAtAug 1, 2026, 1:47 PM
githubUrlhttps://github.com/neomjs/neo/issues/16233
authorneo-opus-ada
commentsCount6
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 1, 2026, 11:58 AM

Nothing generates the wake-receiver route manifest, so the host edge cannot be provisioned without hand-authoring secrets

Closed Backlog/active-chunk-11 enhancementai
neo-opus-ada
neo-opus-ada commented on Jul 31, 2026, 7:58 PM

Amended 2026-08-01 after the terminal Drop+Supersede review of PR #16234. The original body prescribed a producer contract that was wrong in four places; those are corrected below and a Contract Ledger added. PR #16234 is dropped — its salvage map is in the review.

Context

ai/daemons/wake/receiver.mjs refuses to start without a route manifest: mode 0600, and every route carrying a signingKey, an agentIdentity, harnessTargetMetadata, and a positive adapterConfig.attemptTimeoutMs.

Routes correspond to live WAKE_SUB records from manage_wake_subscription list.

Part of #16167 — gap 2 names "routes.json manifest = WAKE_SUB ids + host-held signing keys" as a deliverable.

Problem

Nothing produces that manifest, so provisioning the host edge means hand-authoring a secrets file and discovering the shape rules one throw at a time. ai/scripts/lifecycle/local-agent-os/README.md:63-100 still instructs operators to preserve keys and fill routes.json in an editor.

Architectural Reality

Four constraints the first implementation attempt got wrong. Each is load-bearing.

1. The server owns the signing key. WakeSubscriptionService.subscribe() mints the HMAC key once at subscribe-time and only for a2a-webhook targets, storing it in harnessTargetMetadata; WebhookDeliveryService signs with it. A generator that mints its own produces routes that boot cleanly and then reject every container wake with 401 — worse than failing, because the manifest reads healthy.

2. Only a2a-webhook is deliverable. CoalescingEngineService treats bridge-daemon, disabled and none as no-ops on the Shape-B path. A route built from one silently delivers nothing.

3. The shared manifest needs a strict mutex, and one already exists. ai/daemons/wake/outboxLock.mjs (withOutboxLock) is the proven contract in this same directory: it reclaims only when the holder's pid is dead via a liveness probe, and releases only while the descriptor still names its own pid. An age-based reclaim plus an unconditional path release admits overlapping holders — measured maxActive: 2 on the dropped implementation. Do not write a second mutex.

4. Unsubscribe deletes the row. WakeSubscriptionService removes the record, so the id disappears from the list entirely. Reconciling only ids present in the caller's input can therefore never withdraw the route of an unsubscribed seat — that requires a current-owner-set or tombstone model.

Contract Ledger

Surface Consumer Change Compatibility
buildWakeReceiverManifest({subscriptions, existingRoutes, adapterConfigById, attemptTimeoutMs}) CLI, specs New export New surface; no existing caller
withValidatedManifest publish path CLI New export Writes 0600; refuses to publish what the receiver rejects
Route lock at <manifest>.lock every peer's CLI invocation New shared file Must adopt withOutboxLock's pid-owned contract; a second incompatible lock on the same path is a correctness break
manifest.routes[*].harnessTargetMetadata receiver.mjs loader, receiver state records Sender-only signingKey/url stripped Receiver reads the key from route.signingKey; carried legacy routes must be re-sanitised, not republished verbatim
adapterConfig per route receiver.mjs:79-84 (codexBinary) Caller-supplied, keyed by subscription id Subscription records carry none; must be reachable through the CLI, not only programmatically
ai:wake-manifest CLI every family's seat New entrypoint Per-peer call; results compose
local-agent-os/README.md:63-100 operators Manual editing replaced by the CLI Docs and code must describe one path

Schema correction

The original body conflated two schemas. They are different and both are checked:

  • Manifest: schemaVersion is the number 1 (loadWakeReceiverManifest).
  • Signed wake envelope: schemaVersion is the string "1.0", and must equal the x-neo-wake-schema-version header; a numeric value fails 409 signed-route-mismatch.

Fix

A per-peer generator — not an operator batch job; three families are online and each provisions its own seat — that:

  1. maps only active a2a-webhook records, using the server-issued key, failing closed when a published key disagrees;
  2. skips undeliverable targets with a named reason rather than mapping or refusing the whole build;
  3. serialises the entire read/merge/publish under the existing withOutboxLock contract, so concurrent peers cannot unprovision each other and a slow holder is not reclaimed while alive;
  4. reconciles the caller's own routes against its current owner set, so an unsubscribed or retargeted seat's route is withdrawn even when its id no longer appears in the list;
  5. re-sanitises carried routes, so legacy sender secrets do not survive republication;
  6. exposes per-route adapter config through the CLI, so a Codex seat can supply codexBinary;
  7. validates through the receiver's own loader and publishes via an exclusive, symlink-safe staging file, creating the parent directory before taking the lock;
  8. replaces the manual runbook procedure.

Acceptance criteria

  1. A manifest built from live records starts the receiver with no hand editing, on a host where the target directory does not yet exist.
  2. Concurrent builds by different seats never lose a route, including when one holder outlives the staleness bound — mutual exclusion holds under stale-reclaim, not only under fast contention.
  3. A seat that unsubscribes or retargets has its published route withdrawn; peers' routes are untouched.
  4. A manifest the receiver would reject is never published, and no staging or lock residue survives any failure path.
  5. Rebuilding preserves server keys byte-for-byte; a disagreement fails closed rather than choosing a side.
  6. No signing key appears in any summary, log line, or receiver-visible metadata — including on routes carried from an older manifest.
  7. A Codex route with codexBinary is producible through the CLI.
  8. The runbook, this ticket, and the implementation describe the same path.

Out of scope

  • The container-Memory-Core → host-receiver leg. This produces the host half of the shared secret.
  • A managed ai:host-edge entrypoint with a requiredFor guard — #16229.

Successor — claimable by any seat

This ticket is the gate for the successor PR. It is unassigned deliberately: any family may claim it. Everything needed is below; you should not have to read the closed PR's review to start.

Salvage — reuse, do not rewrite

Branch ada/16167-wake-receiver-manifest-builder (PR #16234, closed) is retained. These surfaces passed review and carry green specs:

  • server-key authority — read from harnessTargetMetadata, never minted; fails closed on disagreement
  • deliverable-target filtering — a2a-webhook only, others skipped with a named reason
  • exclusive symlink-safe staging + validation through the receiver's own loader
  • carried-route sanitation — sender-only signingKey/url stripped from receiver-visible metadata, including on routes carried from an older manifest
  • ai:wake-manifest CLI skeleton and its per-peer framing
  • the specs covering all of the above

Replace

  • The mutex. The dropped implementation reclaimed by age and released by pathname, which admits overlapping holders — measured maxActive: 2 (A held past the staleness bound, B reclaimed, A's late release deleted B's lock, C entered while B was live). Adopt ai/daemons/wake/outboxLock.mjs (withOutboxLock): reclaim only on a dead pid via liveness probe, release only while the descriptor still names your own pid. Do not author a second lock.
  • Reconciliation. Withdrawing only ids present in the caller's input cannot handle unsubscribe, which deletes the row so the id never appears. Needs a current-owner-set or tombstone model.
  • Adapter config reachability. adapterConfigById existed programmatically but had no CLI flag, so a Codex route could not be produced through the real entrypoint.

Also outstanding

  • First boot into a non-existent directory fails: the lock is taken at <manifest>.lock before the writer creates the parent.
  • ai/scripts/lifecycle/local-agent-os/README.md:63-100 still instructs manual editing of the secret manifest.

Successor PR requirements

  1. Cite the terminal review on #16234 (4832662594) and map each reused vs replaced surface above.
  2. Falsifier-first, off the reviewer's exact probes — in particular mutual exclusion under stale reclaim, not fast contention, and the Codex path through the CLI, not a direct function call.
  3. Concept pass with @neo-gpt-emmy before code; that inversion is the working method for this lane.

Note on scheduling

This does not gate #16167 gap 2. That leg was decoupled: the wake receipt is provable with a hand-authored manifest, and the generator lands afterwards.

tobiu referenced in commit 0815553 - "feat(wake): generate the receiver route manifest with owner-safe publishing (#16233) (#16249) on Aug 1, 2026, 11:58 AM
tobiu closed this issue on Aug 1, 2026, 11:58 AM