@junghanacs/entwurf 0.41.1 → 0.41.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/AGENTS.md +1 -1
  2. package/BASELINE.md +7 -5
  3. package/CHANGELOG.md +185 -0
  4. package/DELIVERY.md +37 -20
  5. package/README.md +70 -65
  6. package/VERIFY.md +9 -9
  7. package/docs/external-mcp-host.md +1 -1
  8. package/docs/setup-clean-host.md +59 -33
  9. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +14 -11
  10. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-self-address.js +10 -0
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +23 -2
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +7 -1
  13. package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +3 -0
  14. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +21 -10
  15. package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +4 -2
  16. package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +5 -0
  17. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +6 -4
  18. package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/register.js +13 -2
  19. package/mcp/entwurf-bridge/dist/pi-extensions/lib/omp-fresh-preflight.js +2 -2
  20. package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-fresh-preflight.js +1 -1
  21. package/mcp/entwurf-bridge/dist/pi-extensions/lib/remote-pair-receive.js +25 -17
  22. package/mcp/entwurf-bridge/dist/pi-extensions/lib/remote-pair-send.js +94 -28
  23. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-omp.js +57 -7
  24. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +2 -2
  25. package/mcp/entwurf-bridge/dist/scripts/remote-pair-receive.js +12 -12
  26. package/mcp/entwurf-bridge/src/index.ts +14 -11
  27. package/native/win32-x64/entwurf_win.node +0 -0
  28. package/native/win32-x64/manifest.json +2 -2
  29. package/package.json +4 -4
  30. package/pi-extensions/lib/entwurf-self-address.ts +13 -0
  31. package/pi-extensions/lib/entwurf-v2-contract.ts +25 -2
  32. package/pi-extensions/lib/entwurf-v2-decider.ts +7 -0
  33. package/pi-extensions/lib/entwurf-v2-surface.ts +3 -0
  34. package/pi-extensions/lib/fresh-call-composition.ts +21 -10
  35. package/pi-extensions/lib/herdr-fresh-call.ts +4 -2
  36. package/pi-extensions/lib/meta-sender-identity.ts +4 -0
  37. package/pi-extensions/lib/mux-fresh-call.ts +6 -4
  38. package/pi-extensions/lib/native-push/register.ts +17 -2
  39. package/pi-extensions/lib/omp-fresh-preflight.ts +2 -2
  40. package/pi-extensions/lib/pi-durable-fresh-preflight.ts +1 -1
  41. package/pi-extensions/lib/remote-pair-receive.ts +27 -19
  42. package/pi-extensions/lib/remote-pair-send.ts +117 -30
  43. package/pi-extensions/meta-bridge-omp.ts +69 -7
  44. package/pi-extensions/meta-bridge-pi-durable.ts +2 -2
  45. package/run.sh +173 -22
  46. package/scripts/check-agy-sender-identity.ts +6 -4
  47. package/scripts/check-entwurf-self-address.ts +34 -3
  48. package/scripts/check-entwurf-v2-contract.ts +24 -2
  49. package/scripts/check-entwurf-v2-production.ts +45 -31
  50. package/scripts/check-gate-qualification.ts +10 -8
  51. package/scripts/check-native-push-adapter.ts +4 -0
  52. package/scripts/check-native-push-register.ts +32 -82
  53. package/scripts/check-remote-pair-installed.ts +2 -2
  54. package/scripts/dev-bin.sh +112 -1
  55. package/scripts/lib/chain-terminus-publication.ts +58 -0
  56. package/scripts/lib/codex-fresh-live-protocol.ts +30 -0
  57. package/scripts/lib/codex-fresh-source-receipts.ts +112 -3
  58. package/scripts/lib/pi-durable-fresh-live-receipts.ts +29 -1
  59. package/scripts/mutants/chain-terminus-publication.json +17 -0
  60. package/scripts/mutants/codex-native.json +98 -0
  61. package/scripts/mutants/control-send-receipt.json +2 -2
  62. package/scripts/mutants/mux-fresh-call.json +16 -1
  63. package/scripts/mutants/native-push-paused.json +17 -0
  64. package/scripts/mutants/omp-fresh.json +13 -2
  65. package/scripts/mutants/pack-install.json +46 -0
  66. package/scripts/mutants/remote-pair-display.json +3 -3
  67. package/scripts/mutants/remote-pair-receive.json +41 -11
  68. package/scripts/mutants/remote-pair-roundtrip.json +67 -7
  69. package/scripts/mutants/remote-pair-send.json +156 -17
  70. package/scripts/mutants/remote-pair-transport.json +4 -4
  71. package/scripts/mutants/source-install.json +66 -0
  72. package/scripts/remote-pair-receive.ts +12 -12
  73. package/scripts/smoke-agy-install-state.sh +121 -0
  74. package/scripts/smoke-agy-native-push-live.ts +13 -0
  75. package/scripts/smoke-codex-fresh-live.ts +50 -27
  76. package/scripts/smoke-entwurf-chain-live.ts +7 -9
  77. package/scripts/smoke-herdr-fresh-call-live.ts +4 -4
  78. package/scripts/smoke-herdr-plugin-build-live.ts +179 -50
  79. package/scripts/smoke-mux-lifecycle-live.ts +140 -8
  80. package/scripts/smoke-omp-fresh-live.ts +5 -4
  81. package/scripts/smoke-pi-durable-fresh-live.ts +29 -7
  82. package/scripts/smoke-setup-verdict.sh +27 -2
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * remote-pair-receive — `entwurf remote-pair-receive --pair <absolute file>` (#107 rail 1).
3
+ * remote-pair-receive — `entwurf remote-pair-receive --pair <absolute file>` (#107 rail 1, #149).
4
4
  *
5
- * The receiving host's entry for one declared remote pair. The sending host runs it over one
5
+ * The receiving host's entry for its declared remote pairs. The sending host runs it over one
6
6
  * SSH call and writes one JSON claim to stdin. This entry reads the operator-named pair
7
- * declaration once, applies the pair scope check, and hands a matching claim to the existing
8
- * v2 dispatch. The scope and trust model are in pi-extensions/lib/remote-pair-receive.ts.
7
+ * declaration once, selects the one route whose two ends are the claim's, and hands a matching
8
+ * claim to the existing v2 dispatch. The scope and trust model are in pi-extensions/lib/remote-pair-receive.ts.
9
9
  *
10
10
  * Scope: `--pair` must name THIS host's own opt-in file (`remotePairOptInPath()`), absolute and
11
- * normalized-equal; any other path — even a valid declaration — is exit 2. That one file is the pair
12
- * this host accepts and, because it names the sender's host as its route, the declared route back.
11
+ * normalized-equal; any other path — even a valid declaration — is exit 2. That one file holds the
12
+ * pairs this host accepts and, because each route names the sender's host, the declared route back.
13
13
  * The sender's declaration carries the path only so the argv shape is explicit; it selects nothing.
14
14
  *
15
15
  * EXIT CONTRACT (stdout carries a JSON receipt line only where named):
@@ -38,7 +38,7 @@ if (args.length !== 2 || args[0] !== "--pair") {
38
38
  process.exit(2);
39
39
  }
40
40
  const pairFile = args[1];
41
- let decl;
41
+ let decls;
42
42
  let claim;
43
43
  try {
44
44
  // The scope is THIS host's own opt-in, never a file the sender picks: `--pair` must name it
@@ -47,18 +47,18 @@ try {
47
47
  if (!path.isAbsolute(pairFile) || path.normalize(pairFile) !== own) {
48
48
  throw new Error(`--pair must be this host's opt-in file ${own} (got ${pairFile})`);
49
49
  }
50
- const route = readRemotePairOptIn(own);
51
- if (route === null)
50
+ const optIn = readRemotePairOptIn(own);
51
+ if (optIn === null)
52
52
  throw new Error(`this host has no remote pair opt-in (${own} is absent)`);
53
- decl = { remoteGardenId: route.remoteGardenId, ssh: route.ssh, localGardenId: route.localGardenId };
53
+ decls = optIn.routes.map((r) => ({ remoteGardenId: r.remoteGardenId, ssh: r.ssh, localGardenId: r.localGardenId }));
54
54
  claim = parseRemoteClaim(readFileSync(0, "utf8"));
55
55
  }
56
56
  catch (err) {
57
57
  emit({ kind: "invalid", detail: err instanceof Error ? err.message : String(err) }, 2);
58
58
  }
59
- const receipt = await receiveRemoteClaim(claim, decl, {
59
+ const receipt = await receiveRemoteClaim(claim, decls, {
60
60
  dispatch: (params, sender) => runAndRenderEntwurfV2FromSurface(params, { senderProvider: () => sender }),
61
- // The scope file IS this host's own opt-in, and it names the sender's host as its route: a
61
+ // The scope file IS this host's own opt-in, and the matched route names the sender's host: a
62
62
  // matching claim always has a declared route back. Declared, not probed.
63
63
  replyRoute: "declared",
64
64
  });
@@ -78,7 +78,7 @@ import { resolveMailboxReceiverFacts } from "../../../pi-extensions/lib/entwurf-
78
78
  import { listEntwurfFacts } from "../../../pi-extensions/lib/entwurf-fact-provider.ts";
79
79
  import { ENTWURF_PEERS_RENDER_LIMIT, renderEntwurfPeers } from "../../../pi-extensions/lib/entwurf-peers-render.ts";
80
80
  import { computeSelfAddressability, type MetaDeliveryDomain } from "../../../pi-extensions/lib/entwurf-self-address.ts";
81
- import { nativePushSupported } from "../../../pi-extensions/lib/entwurf-v2-contract.ts";
81
+ import { nativePushPaused, nativePushSupported } from "../../../pi-extensions/lib/entwurf-v2-contract.ts";
82
82
  import { resolveMailboxWakeModeCapability } from "../../../pi-extensions/lib/entwurf-v2-decider.ts";
83
83
  import { retireCitizen } from "../../../pi-extensions/lib/entwurf-v2-retire.ts";
84
84
  import { makeProductionRetireDeps } from "../../../pi-extensions/lib/entwurf-v2-retire-production.ts";
@@ -313,6 +313,7 @@ async function buildMetaSenderEnvelope(
313
313
  metaDeliveryDomain,
314
314
  recordBacked: true,
315
315
  probeAlive: await probeNativeSenderAlive(identity),
316
+ nativePushPaused: nativePushPaused(identity.backend),
316
317
  }
317
318
  : metaDeliveryDomain === "self-fetch"
318
319
  ? (() => {
@@ -488,7 +489,7 @@ server.tool(
488
489
  "mailbox only while deliverable, native-push direct injection only while its adapter probe is alive, " +
489
490
  "or a THIRD RESULT: rejection when neither holds. An inactive self-fetch citizen is mailbox-undeliverable; " +
490
491
  "native-push has NO mailbox: dead → native-push-target-dead; indeterminate → " +
491
- "native-push-probe-indeterminate. " +
492
+ "native-push-probe-indeterminate; agy (paused, #152) → native-push-paused, no probe. " +
492
493
  "DORMANT IS UNREACHABLE: a socket-domain citizen that is not running gets dormant-fire-forget-unsupported " +
493
494
  "— same receiver rule as the mailbox, no active drainer means no delivery. " +
494
495
  "The intent that used to answer there, owned-outcome, resumed it by launching a hidden background child " +
@@ -505,7 +506,8 @@ server.tool(
505
506
  "fire-and-forget = send/reply/hand-off to a LIVE socket target or to any citizen with no " +
506
507
  "socket liveness — the decider picks its rail, and a rail can also REJECT (self-fetch → " +
507
508
  "mailbox when deliverable, else mailbox-undeliverable; native-push → alive: direct injection, " +
508
- "dead: native-push-target-dead, indeterminate: native-push-probe-indeterminate); set " +
509
+ "dead: native-push-target-dead, indeterminate: native-push-probe-indeterminate; a paused " +
510
+ "backend — agy, #152 — rejects as native-push-paused before any probe); set " +
509
511
  "wants_reply for an answer. This is the ONLY intent: owned-outcome, which resumed a dormant " +
510
512
  "citizen by launching a hidden background child, was withdrawn under the visible-first rule. " +
511
513
  "It is not selectable, not deprecated-but-tolerated; a dormant citizen is currently " +
@@ -532,13 +534,13 @@ server.tool(
532
534
  // Resolved ONCE so the dispatch-moment timestamp is fixed and the control RPC sender
533
535
  // + the mailbox body sender share one envelope. No replyability gate (see above).
534
536
  const sender = await buildSendSenderEnvelope({ requestMeta: extra._meta });
535
- // #107: the operator's remote-pair opt-in is read per send. Absent = off and the local
536
- // surface below runs exactly as before; a declared target goes over that one route.
537
+ // #107/#149: the operator's remote-pair opt-in is read per send. Absent = off and the local
538
+ // surface below runs exactly as before; a declared target goes over its one exact route.
537
539
  const rendered = await runEntwurfV2WithRemoteRoute(
538
540
  { target, intent, message, mode, wants_reply },
539
541
  {
540
542
  senderProvider: () => sender,
541
- route: readRemotePairOptIn(remotePairOptInPath()),
543
+ optIn: readRemotePairOptIn(remotePairOptInPath()),
542
544
  // The decider's canonical socket dir AND this bridge's ENTWURF_DIR: a trace in either is local.
543
545
  localTrace: makeLocalTraceFact({
544
546
  sessionsDir: defaultMetaSessionsDir(),
@@ -651,7 +653,7 @@ server.tool(
651
653
  "or a REJECT when neither holds: mailbox-undeliverable for a self-fetch citizen whose inbox " +
652
654
  "nobody drains, and on the native-push probe dead → native-push-target-dead vs " +
653
655
  "indeterminate → native-push-probe-indeterminate, kept apart because an unestablished probe " +
654
- "is not a departed host. So " +
656
+ "is not a departed host; an agy target rejects as native-push-paused (#152) without being probed. So " +
655
657
  "`unsupported` does not promise reachability either: a record whose backend has no adapter on " +
656
658
  "this lane resolves to that reject. " +
657
659
  "Note: this is the *active* world and it is facts-only — it creates nothing. Pass an " +
@@ -735,8 +737,9 @@ server.tool(
735
737
  "must be LIVE: it is probed first, and a dead/indeterminate probe is refused (no garden id is " +
736
738
  "minted for a pointer that does not resolve to a real host). Re-registering the same " +
737
739
  "conversation attaches to the SAME garden id and refreshes the cwd. Only 'antigravity' is " +
738
- "registerable on this lane. No mailbox receiver marker is written — native-push has no " +
739
- "idle-wake watch; the returned garden id is the reply handle.",
740
+ "registerable on this lane, and antigravity native-push is PAUSED (#152): this verb refuses it by " +
741
+ "name before any probe and mints no garden id. No mailbox receiver marker is written — native-push " +
742
+ "has no idle-wake watch; the returned garden id is the reply handle.",
740
743
  {
741
744
  backend: z
742
745
  .enum(["antigravity"])
@@ -787,8 +790,8 @@ server.tool(
787
790
  "herdr workspace, pi and claude-code " +
788
791
  "only; everywhere else it opens in the operator's tmux with all six backends (pi, " +
789
792
  "claude-code, copilot, omp, codex, pi-durable). No fallback: an incomplete herdr context is refused by name, " +
790
- "never by a tmux window you cannot see from herdr. The sibling's FIRST action " +
791
- "is a callback to you carrying a nonce, whose sender envelope is its garden id — that is how you learn the " +
793
+ "never by a tmux window you cannot see from herdr. If proceeding, the sibling calls back before its task with a nonce; " +
794
+ "the sender envelope names its garden id — that is how you learn the " +
792
795
  "address of a thing that did not exist a moment ago. This returns a LAUNCH receipt (the owner's " +
793
796
  "coordinates plus that nonce): it does NOT mean the runtime started, the first turn ran, or " +
794
797
  "the task was delivered. Those coordinates are a VIEW, never an address — a herdr tab/pane id can change under " +
Binary file
@@ -3,7 +3,7 @@
3
3
  "platform": "win32",
4
4
  "arch": "x64",
5
5
  "file": "entwurf_win.node",
6
- "sha256": "9837893521ec1d033913ca6cd1b705cb6e942e8d5c288948a1ed0ca0e28655e5",
6
+ "sha256": "0864b2753a8c22831874dd00959febf597616315670130910aa45147aaa61d17",
7
7
  "napiVersion": 8,
8
8
  "builtWith": {
9
9
  "node": "24.18.1",
@@ -12,7 +12,7 @@
12
12
  "windowsTargetPlatformVersion": "10.0.26100.0",
13
13
  "visualStudio": {
14
14
  "displayName": "Visual Studio Enterprise 2026",
15
- "installationVersion": "18.10.12224.181"
15
+ "installationVersion": "18.11.12315.225"
16
16
  }
17
17
  },
18
18
  "source": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@junghanacs/entwurf",
3
- "version": "0.41.1",
3
+ "version": "0.41.2",
4
4
  "description": "Visible sibling sessions across independent agent harnesses: launch in Herdr or tmux, retire on request and reopen pi or Claude Code in tmux, and deliver messages by garden id; native bridges and Claude/Cortex ACP adapters.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -196,10 +196,10 @@
196
196
  "check:full": "bash scripts/check-elapsed.sh check:full check:core check:hermetic check:package",
197
197
  "check:core": "pnpm run check:toolchain && pnpm run check:vitest && pnpm run check:contracts",
198
198
  "check:toolchain": "pnpm lint && pnpm typecheck",
199
- "check:vitest": "./run.sh check-mux-fresh-call && ./run.sh check-acp-sdk-surface && ./run.sh check-pi-queue-oracle && ./run.sh check-tests-beside-behavior && ./run.sh check-remote-pair-installed-audit",
200
- "check:contracts": "./run.sh check-demo-reproducibility && ./run.sh check-shell-quote && ./run.sh check-entwurf-session-identity && ./run.sh check-meta-session && ./run.sh check-meta-v3-record && ./run.sh check-mailbox-receipt-state && ./run.sh check-entwurf-capabilities && ./run.sh check-harness-admission-parity && ./run.sh check-capability-bundle-reach && ./run.sh check-meta-mailbox-state-write && ./run.sh check-meta-receiver-marker && ./run.sh check-meta-capability-source && ./run.sh check-model-lock && ./run.sh check-dep-versions && ./run.sh check-node-floor-coherence && ./run.sh check-claude-floor-coherence && ./run.sh check-copilot-statusline && ./run.sh check-pi-import-surface && ./run.sh check-env-namespace && ./run.sh check-pi-runtime-version && ./run.sh check-pi-preflight && ./run.sh check-project-trust-handler && ./run.sh check-entwurf-v2-contract && ./run.sh check-entwurf-v2-send && ./run.sh check-entwurf-v2-runner && ./run.sh check-control-send-receipt && ./run.sh check-control-retire && ./run.sh check-pi-retire-request && ./run.sh check-cc-retire-pidfd && ./run.sh check-cc-retire-request && ./run.sh check-retirement-event && ./run.sh check-entwurf-v2-retire && ./run.sh check-compaction-send-guard && ./run.sh check-entwurf-resume-args && ./run.sh check-entwurf-v2-native-push && ./run.sh check-entwurf-v2-production && ./run.sh check-entwurf-v2-surface && ./run.sh check-entwurf-bridge-pi-free && ./run.sh check-resume-launch-identity && ./run.sh check-mux-placement && ./run.sh check-mux-launch && ./run.sh check-mux-resume-call && ./run.sh check-mux-parent-artifact && ./run.sh check-mux-launcher-fence && ./run.sh check-entwurf-v2-visible-resume && ./run.sh check-control-socket-path && ./run.sh check-win32-preview && ./run.sh check-pack-win32-carrier && ./run.sh check-meta-listing && ./run.sh check-entwurf-fact-provider && ./run.sh check-herdr-placement && ./run.sh check-herdr-fresh-call && ./run.sh check-herdr-supply && ./run.sh check-fresh-call-dispatch && ./run.sh check-fresh-call-provider && ./run.sh check-entwurf-peers-surface && ./run.sh check-entwurf-self-address && ./run.sh check-entwurf-deliverability && ./run.sh check-native-push-adapter && ./run.sh check-native-push-register && ./run.sh check-auth-boundary && ./run.sh check-acp-overlay && ./run.sh check-acp-tool-surface && ./run.sh check-acp-event-mapper && ./run.sh check-acp-prompt-builder && ./run.sh check-acp-config && ./run.sh check-acp-session-store && ./run.sh check-acp-carrier-augment && ./run.sh check-acp-foreground-only && ./run.sh check-acp-set-model-readback && ./run.sh check-acp-vendor-rejection && ./run.sh check-codex-sender-identity && ./run.sh check-codex-native-push && ./run.sh check-pi-durable-beside && ./run.sh check-pi-durable-carrier && ./run.sh check-pi-durable-native-module",
199
+ "check:vitest": "./run.sh check-mux-fresh-call && ./run.sh check-acp-sdk-surface && ./run.sh check-pi-queue-oracle && ./run.sh check-tests-beside-behavior && ./run.sh check-remote-pair-installed-audit && ./run.sh check-chain-terminus-publication",
200
+ "check:contracts": "./run.sh check-demo-reproducibility && ./run.sh check-shell-quote && ./run.sh check-entwurf-session-identity && ./run.sh check-meta-session && ./run.sh check-meta-v3-record && ./run.sh check-mailbox-receipt-state && ./run.sh check-entwurf-capabilities && ./run.sh check-harness-admission-parity && ./run.sh check-capability-bundle-reach && ./run.sh check-meta-mailbox-state-write && ./run.sh check-meta-receiver-marker && ./run.sh check-meta-capability-source && ./run.sh check-model-lock && ./run.sh check-dep-versions && ./run.sh check-node-floor-coherence && ./run.sh check-claude-floor-coherence && ./run.sh check-copilot-statusline && ./run.sh check-pi-import-surface && ./run.sh check-env-namespace && ./run.sh check-pi-runtime-version && ./run.sh check-pi-preflight && ./run.sh check-project-trust-handler && ./run.sh check-entwurf-v2-contract && ./run.sh check-entwurf-v2-send && ./run.sh check-entwurf-v2-runner && ./run.sh check-control-send-receipt && ./run.sh check-control-retire && ./run.sh check-pi-retire-request && ./run.sh check-cc-retire-pidfd && ./run.sh check-cc-retire-request && ./run.sh check-retirement-event && ./run.sh check-entwurf-v2-retire && ./run.sh check-compaction-send-guard && ./run.sh check-entwurf-resume-args && ./run.sh check-entwurf-v2-native-push && ./run.sh check-entwurf-v2-production && ./run.sh check-entwurf-v2-surface && ./run.sh check-entwurf-bridge-pi-free && ./run.sh check-resume-launch-identity && ./run.sh check-mux-placement && ./run.sh check-mux-launch && ./run.sh check-mux-resume-call && ./run.sh check-mux-parent-artifact && ./run.sh check-mux-launcher-fence && ./run.sh check-entwurf-v2-visible-resume && ./run.sh check-control-socket-path && ./run.sh check-win32-preview && ./run.sh check-pack-win32-carrier && ./run.sh check-meta-listing && ./run.sh check-entwurf-fact-provider && ./run.sh check-herdr-placement && ./run.sh check-herdr-fresh-call && ./run.sh check-herdr-supply && ./run.sh check-fresh-call-dispatch && ./run.sh check-fresh-call-provider && ./run.sh check-entwurf-peers-surface && ./run.sh check-entwurf-self-address && ./run.sh check-entwurf-deliverability && ./run.sh check-native-push-adapter && ./run.sh check-native-push-register && ./run.sh check-native-push-paused && ./run.sh check-auth-boundary && ./run.sh check-acp-overlay && ./run.sh check-acp-tool-surface && ./run.sh check-acp-event-mapper && ./run.sh check-acp-prompt-builder && ./run.sh check-acp-config && ./run.sh check-acp-session-store && ./run.sh check-acp-carrier-augment && ./run.sh check-acp-foreground-only && ./run.sh check-acp-set-model-readback && ./run.sh check-acp-vendor-rejection && ./run.sh check-codex-sender-identity && ./run.sh check-codex-native-push && ./run.sh check-pi-durable-beside && ./run.sh check-pi-durable-carrier && ./run.sh check-pi-durable-native-module && ./run.sh check-remote-pair-beside",
201
201
  "check:hermetic": "./run.sh check-bridge-delivery && ./run.sh smoke-pi-attach && ./run.sh check-fresh-cut-gate && ./run.sh check-hook-launch-topology && ./run.sh check-meta-hook-session-switch && ./run.sh check-copilot-birth-hook && ./run.sh check-copilot-receive-arm && ./run.sh check-copilot-launch && ./run.sh check-pi-launch && ./run.sh check-codex-app-server-launch && ./run.sh check-meta-identity-consumers && ./run.sh check-socket-probe && ./run.sh smoke-meta-honesty && ./run.sh check-meta-doctor-oracle && ./run.sh check-agy-permission-matrix && ./run.sh smoke-agy-statusline-state && ./run.sh smoke-copilot-statusline-state && ./run.sh smoke-copilot-mcp-state && ./run.sh smoke-omp-bridge-state && ./run.sh smoke-omp-mcp-state && ./run.sh smoke-omp-receive-state && ./run.sh check-omp-birth-hook && ./run.sh check-omp-receive-arm && ./run.sh check-omp-fresh-preflight && ./run.sh smoke-agy-hooks-state && ./run.sh smoke-pi-provider-state && ./run.sh smoke-user-scope-citizen && ./run.sh smoke-meta-prune && ./run.sh smoke-meta-keyset-guard && ./run.sh check-meta-manifest-schema && ./run.sh check-entwurf-control-rpc && ./run.sh check-entwurf-bridge-boot && ./run.sh check-probe-bridge-command && ./run.sh check-meta-facts && ./run.sh check-peer-facts && ./run.sh check-herdr-plugin && ./run.sh check-herdr-plugin-profile && ./run.sh check-herdr-plugin-build && ./run.sh check-herdr-runtime-bootstrap && ./run.sh check-herdr-activation && ./run.sh check-agy-sender-identity && ./run.sh check-acp-provider-surface && ./run.sh check-acp-stop-reason && ./run.sh check-acp-prompt-lifecycle && ./run.sh check-acp-usage-accounting && ./run.sh check-acp-launch-namespace && ./run.sh check-acp-stream-hooks && ./run.sh check-acp-backend-preflight && ./run.sh check-acp-session-reuse && ./run.sh check-typing-call-fence && ./run.sh check-gate-manifests && ./run.sh check-release-gate-outcomes && ./run.sh check-probe-ordering && ./run.sh check-probe-cli-shim && ./run.sh check-acp-cortex && ./run.sh check-codex-bridge-identity && ./run.sh check-pi-bridge-sender && ./run.sh check-pi-mcp-register && ./run.sh check-pi-mcp-bridge && ./run.sh check-codex-birth-hook && ./run.sh smoke-codex-config-state && ./run.sh smoke-codex-birth && ./run.sh check-herdr-sandbox",
202
- "check:package": "./run.sh check-install-surface && ./run.sh smoke-meta-install-state && ./run.sh smoke-agy-install-state && ./run.sh smoke-setup-verdict && ./run.sh check-pack-pin-matcher && ./run.sh check-pack-consumer-roots && ./run.sh check-package-source-routing && ./run.sh check-install-preflight && ./run.sh check-pack"
202
+ "check:package": "./run.sh check-install-surface && ./run.sh smoke-meta-install-state && ./run.sh smoke-agy-install-state && ./run.sh smoke-setup-verdict && ./run.sh check-pack-pin-matcher && ./run.sh check-pack-argv-matcher && ./run.sh check-pack-consumer-roots && ./run.sh check-package-source-routing && ./run.sh check-install-preflight && ./run.sh check-pack"
203
203
  },
204
204
  "pi": {
205
205
  "extensions": [
@@ -77,6 +77,8 @@ export interface SelfAddressabilityFacts {
77
77
  watchArmed?: boolean;
78
78
  /** meta-session (native-push): an adapter probe found this citizen's live native conversation. */
79
79
  probeAlive?: boolean;
80
+ /** meta-session (native-push): #152 — this backend's native-push is paused, so no probe ran. */
81
+ nativePushPaused?: boolean;
80
82
  }
81
83
 
82
84
  export interface SelfAddressabilityResult {
@@ -118,6 +120,17 @@ export function computeSelfAddressability(facts: SelfAddressabilityFacts): SelfA
118
120
  // default fail-closed row).
119
121
  switch (facts.metaDeliveryDomain) {
120
122
  case "native-push": {
123
+ // #152 policy, decided BEFORE the pure record+probe predicate and kept out of it:
124
+ // only a record-backed citizen can be "paused" (a record-less one keeps its own
125
+ // reason), and no probe was run for this answer, so none is reported.
126
+ if (facts.recordBacked === true && facts.nativePushPaused === true) {
127
+ return {
128
+ replyable: false,
129
+ socketState: "none",
130
+ reason:
131
+ "native-push unreachable — native-push for this backend is paused (#152); no probe ran for this answer",
132
+ };
133
+ }
121
134
  const push = nativePushDeliverable({ recordBacked: facts.recordBacked, probeAlive: facts.probeAlive });
122
135
  return {
123
136
  replyable: push.deliverable,
@@ -134,6 +134,26 @@ export function nativePushSupported(backend: string): backend is NativePushBacke
134
134
  return (NATIVE_PUSH_BACKENDS as readonly string[]).includes(backend);
135
135
  }
136
136
 
137
+ // ── Paused native-push backends (#152) ─────────────────────────────────────
138
+ // A native-push backend whose SEND is paused by product policy. On the external send path
139
+ // entwurf measured, Antigravity's language server refused the `agentapi` call with
140
+ // `Unauthenticated: missing CSRF token` (agy 1.2.16 in #128; agy 1.4.6 again for #152).
141
+ // Whether a first-party own-tool shell holds a token entwurf could legitimately present is
142
+ // UNMEASURED, not ruled out. So instead of probing a port that refused on that path and
143
+ // reporting it as an inconclusive liveness, the three public entries that would reach the agy LS —
144
+ // v2 dispatch, a sender's own `replyable`, and `entwurf_register_native` — refuse by name
145
+ // BEFORE any probe. This is a fixed constant, not a flag: no env, config or runtime toggle
146
+ // can lift it. The backend stays in NATIVE_PUSH_BACKENDS, so existing agy records, the V3
147
+ // reader and the peers listing are unchanged; only the native-push traffic stops.
148
+ // The one probe that is NOT behind this guard is the fresh-cut quiescence check
149
+ // (scripts/meta-bridge-fresh-cut.ts), which protects an operator-named archive and must keep
150
+ // failing closed rather than read the pause as a departed host.
151
+ export const NATIVE_PUSH_PAUSED_BACKENDS: readonly NativePushBackend[] = ["antigravity"];
152
+
153
+ export function nativePushPaused(backend: string): boolean {
154
+ return (NATIVE_PUSH_PAUSED_BACKENDS as readonly string[]).includes(backend);
155
+ }
156
+
137
157
  // NativePushLiveness = the 3-value liveness the native-push adapter probe yields.
138
158
  // The SAME three values as SocketLiveness, reused so there is ONE 3-value liveness
139
159
  // vocabulary — but these are NOT socket-bound (봉인 2: "주석만 socket-전용 오독 정정"):
@@ -173,6 +193,7 @@ export const ENTWURF_V2_REJECT_REASONS = [
173
193
  "mailbox-undeliverable", // F-mailbox: fire-and-forget to an unsupported citizen whose mailbox is not deliverable (fail-closed; future pi-backend non-drainable mailbox)
174
194
  "native-push-target-dead", // 봉인 1: fire-and-forget to a native-push (agy) target whose adapter probe found NO live conversation. Post-probe; observedLiveness = dead.
175
195
  "native-push-probe-indeterminate", // 봉인 1: fire-and-forget to a native-push target whose adapter probe was inconclusive (agy alive but no port served the conv, or a probe error). Post-probe; observedLiveness = indeterminate. Never coerced to dead.
196
+ "native-push-paused", // #152: fire-and-forget to a native-push backend whose send is paused by product policy (NATIVE_PUSH_PAUSED_BACKENDS — antigravity). Pre-probe, observedLiveness = null: nothing was probed, so no liveness is claimed — never coerced to dead or indeterminate.
176
197
  "bad-target", // R2: absent/typo garden-id (no existing citizen); fresh creation out of v2 scope
177
198
  "record-less-socket", // #50 C4: a gid-shaped non-symlink control socket exists but NO meta-record claims it. The record is the sole address authority (목표 ②), so a bare socket is not an addressable citizen — it is a diagnostic state, refused for EVERY intent before any lock/probe (pre-probe, observedLiveness=null). NOT `bad-target`: something real answers to this gid, and mislabeling it absent would hide the very state the reject exists to surface.
178
199
  "target-locked", // R5 pre-claim for bucket B F2 per-gid lockfile conflict
@@ -181,12 +202,13 @@ export const ENTWURF_V2_REJECT_REASONS = [
181
202
  export type EntwurfV2RejectReason = (typeof ENTWURF_V2_REJECT_REASONS)[number];
182
203
 
183
204
  // ── Pre-probe reject reasons (?6 — observedLiveness = null) ────────────────
184
- // These four rejects are decided BEFORE any liveness probe runs, so there is no
205
+ // These five rejects are decided BEFORE any liveness probe runs, so there is no
185
206
  // honest 4-value FactLiveness to stamp: `bad-target` (no citizen/backend),
186
207
  // `target-locked` (5a lock conflict, before lstat/connect), `target-address-conflict`
187
208
  // (address-subject conflict → probing is forbidden), `record-less-socket` (#50 C4:
188
209
  // no record ⇒ not a citizen ⇒ no in-domain probe ever runs — the presence hint is
189
- // a single lstat, not a liveness measurement). `indeterminate` means an
210
+ // a single lstat, not a liveness measurement), `native-push-paused` (#152: the backend's
211
+ // native-push send is paused by policy, so its adapter is never probed). `indeterminate` means an
190
212
  // in-domain probe was inconclusive (≠ "not looked yet"); `unsupported` means the
191
213
  // backend has no predicate (≠ "pre-probe"). So a pre-probe reject's
192
214
  // observedLiveness is `null`, NOT one of the four values. Every OTHER reject —
@@ -203,6 +225,7 @@ export const PRE_PROBE_REJECT_REASONS = [
203
225
  "target-locked",
204
226
  "target-address-conflict",
205
227
  "record-less-socket",
228
+ "native-push-paused",
206
229
  ] as const satisfies readonly EntwurfV2RejectReason[];
207
230
  export type PreProbeRejectReason = (typeof PRE_PROBE_REJECT_REASONS)[number];
208
231
 
@@ -52,6 +52,7 @@ import {
52
52
  isLivenessSupported,
53
53
  makeRejectReceipt,
54
54
  type NativePushBackend,
55
+ nativePushPaused,
55
56
  nativePushSupported,
56
57
  resolveDispatch,
57
58
  resolveNativePushDispatch,
@@ -306,6 +307,12 @@ export async function decideDispatch(input: DispatchInput, deps: DispatchDecider
306
307
  // branch is LOCK-FREE (봉인 4): the pi in-domain lock closes a socket TOCTOU, but a
307
308
  // volatile probe route has no lock meaning (a duplicate-send idempotency is a D8 future).
308
309
  if (nativePushSupported(identity.backend)) {
310
+ // #152: a paused native-push backend is refused by name BEFORE its adapter is probed —
311
+ // on the measured external path that language server refused the call (missing CSRF token),
312
+ // and a refusal must not be reported as a measured liveness. Pre-probe, observedLiveness null.
313
+ if (nativePushPaused(identity.backend)) {
314
+ return reject(makeRejectReceipt("native-push-paused", null));
315
+ }
309
316
  const probe = await deps.nativePushProbe(identity);
310
317
  const receipt = resolveNativePushDispatch(input.intent, probe.status);
311
318
  if (!receipt.ok) return reject(receipt);
@@ -107,6 +107,9 @@ export function actionableRejectHint(reason: string): string | undefined {
107
107
  // Native host up, but nothing served this conversation/thread — inconclusive, not a
108
108
  // hard dead. Backend-neutral for the same reason as the dead cell above.
109
109
  return "the native host (harness process or app-server) is up, but nothing served this conversation/thread, so the probe is INCONCLUSIVE — not a measured death. Retry once it is loaded, or verify the conversation/thread id.";
110
+ case "native-push-paused":
111
+ // #152: a policy refusal, not a liveness verdict — this dispatch probed and sent nothing.
112
+ return "native-push for this backend is paused (#152): its language server refused entwurf's measured external calls for a missing CSRF token, so this dispatch probed and sent nothing. The citizen and its record are kept; reach it from inside its own harness instead.";
110
113
  case "record-less-socket":
111
114
  // #50 C4: name the true cause AND the fix — a bare socket is a diagnostic
112
115
  // state, not an addressable citizen (the record is the address).
@@ -67,7 +67,7 @@ export const FRESH_CALL_CALLBACK_TOOL: Record<FreshCallBackend, string> = {
67
67
  copilot: "entwurf-bridge-entwurf_callback",
68
68
  omp: "mcp__entwurf_bridge_entwurf_callback",
69
69
  // Codex's tool process is the operator app-server, not the pane: the no-arg
70
- // verb refuses there by name. First action stays the delivery verb with args.
70
+ // verb refuses there by name. Its callback stays the delivery verb with args.
71
71
  codex: "mcp__entwurf_bridge__entwurf_v2",
72
72
  // The durable contact registers the bridge verbs as its OWN tools under their bare names, and the
73
73
  // app offers a tool to the model by its registered name (`durable/harness/registry.ts:99`
@@ -381,8 +381,8 @@ export function composeBackendArgs(
381
381
  * PLACEMENT SENTENCE. The first line names where the sibling was opened, and only the RAIL knows
382
382
  * that — "the operator's tmux session" is true of a `new-window` launch and false of a herdr
383
383
  * pane. It is therefore supplied by the caller rather than composed here. Everything BELOW that
384
- * line is rail-independent and stays the contract: the callback is the first action, the task
385
- * follows it, and the three prohibitions are measured rather than polite.
384
+ * line is rail-independent and stays the contract: the callback comes before the task, an optional
385
+ * read-only check of the caller may come before the callback, and the steering is stated as fact.
386
386
  */
387
387
  /**
388
388
  * The first-turn framing, WITHOUT the task.
@@ -394,8 +394,13 @@ export function composeBackendArgs(
394
394
  * the halves separate is what lets the framing stay plain prose on both rails while the operator's
395
395
  * task text keeps every byte it was written with.
396
396
  *
397
- * Order is the contract: the callback is the FIRST action and the task follows it, so a sibling
398
- * that gets stuck in the task has already told the caller who it is.
397
+ * Order is the contract: the callback comes BEFORE THE TASK, so a sibling that gets stuck in the
398
+ * task has already told the caller who it is. It is not the literal first tool call. `[GLG 직접,
399
+ * 2026-10-24]` checking the caller and then calling back is the right order, and the framing used
400
+ * to say both "FIRST ACTION … call back" and "you can corroborate the caller first" — two
401
+ * sentences that cannot both be obeyed. What may come before the callback is the one read-only
402
+ * check offered below (and, where a dialect needs it, loading the tools); reading files, running
403
+ * commands or starting the task may not.
399
404
  *
400
405
  * WHAT THIS SAYS AND WHY IT STOPPED SAYING THE REST (#116). Until 2026-09-17 this framing carried
401
406
  * three prohibitions, including "Do not inspect environment variables, do not call entwurf_self".
@@ -433,7 +438,7 @@ export function composeFreshCallFraming(params: {
433
438
  const firstAction =
434
439
  params.backend === "codex"
435
440
  ? [
436
- `FIRST ACTION, before reading files or anything else: call ${birthTool} with ` +
441
+ `BEFORE THE TASK, and before reading files or running commands: call ${birthTool} with ` +
437
442
  `target=${params.callerGardenId}, intent=fire-and-forget, wants_reply=false, and ` +
438
443
  `message set to exactly ${params.nonce} — that string alone, nothing added.`,
439
444
  `${params.nonce} is a correlation tag this caller minted for this one launch: it names no`,
@@ -443,7 +448,7 @@ export function composeFreshCallFraming(params: {
443
448
  "entwurf_callback is refused on this rail: the tool process is the operator app-server, not this pane.",
444
449
  ]
445
450
  : [
446
- `FIRST ACTION, before reading files or anything else: call ${birthTool} with no arguments.`,
451
+ `BEFORE THE TASK, and before reading files or running commands: call ${birthTool} with no arguments.`,
447
452
  "That call is how the caller learns the garden id you were born with. There are no",
448
453
  "parameters: a target or message you type would be a second address axis.",
449
454
  "Your own record does not carry the caller's address, so reporting your identity here does not reach it — that tool call does.",
@@ -453,9 +458,15 @@ export function composeFreshCallFraming(params: {
453
458
  "",
454
459
  ...firstAction,
455
460
  "",
456
- `You can corroborate the caller first if you want to: ${peersTool} is a read-only listing of`,
457
- `this garden's citizens and ${params.callerGardenId} is one of them. That listing is capped, so`,
458
- "a caller you do not happen to see in it is not a reason to skip the callback.",
461
+ // THE CHECK IS THE SIBLING'S, AND SO IS ITS VERDICT. `[GLG 직접, 2026-10-24]` a capped listing that
462
+ // does not show the caller proves nothing about the caller, but a sibling whose check does not hold
463
+ // up may say so and hold back. Entwurf decides nothing here — no trust store, no fallback, no
464
+ // watcher — and the cost of holding back is stated, not hidden: no callback, no address.
465
+ `You may corroborate the caller before that call: ${peersTool} is a read-only listing of`,
466
+ `this garden's citizens and ${params.callerGardenId} is one of them. That listing is capped, so not`,
467
+ "seeing the caller in it does not show the caller is absent. If what you can check is not enough,",
468
+ "or it conflicts with this message, you may say so here and hold the callback and the task; the",
469
+ "caller then has no address for you.",
459
470
  ...hintBlock,
460
471
  "",
461
472
  // WHERE THE RESULT GOES, said once, as topology. `[GLG 직접, 2026-09-18]` a Sonnet sibling
@@ -1232,10 +1232,12 @@ export function renderHerdrFreshCall(result: HerdrFreshCallResult): { text: stri
1232
1232
  `mean the sibling is running, that its first turn ran, or that the task was delivered. The tab and pane ` +
1233
1233
  `coordinates are a view and can change under the sibling — they are not an address and nothing may be ` +
1234
1234
  `dispatched to them.\n` +
1235
- `The sibling's garden id arrives separately — its first action is the zero-argument callback verb, and ` +
1235
+ `The sibling's garden id arrives separately — before its task it calls the zero-argument callback verb ` +
1236
+ `(a read-only look at the caller may come first), and ` +
1236
1237
  `the sender envelope of THAT message is the address. The nonce above is the correlation tag it carries; ` +
1237
1238
  `the sibling reads it from its own launch env, never from this receipt. Nothing is polling for it; if it ` +
1238
- `never comes, the pane is visible and can be read directly.`,
1239
+ `never comes — not started, stuck, or held back after its check; this receipt cannot tell which — the ` +
1240
+ `pane is visible and can be read directly.`,
1239
1241
  isError: false,
1240
1242
  };
1241
1243
  }
@@ -47,6 +47,7 @@
47
47
  * a backend listed there is one whose hook writes a pid marker, and codex birth writes none.
48
48
  */
49
49
 
50
+ import { nativePushPaused } from "./entwurf-v2-contract.ts";
50
51
  import {
51
52
  defaultMetaSessionsDir,
52
53
  describe,
@@ -229,6 +230,9 @@ export async function probeNativeSenderAlive(
229
230
  identity: Pick<MetaIdentity, "backend" | "nativeSessionId">,
230
231
  deps: { resolveAdapter?: (id: string) => Pick<NativePushAdapter, "probe"> } = {},
231
232
  ): Promise<boolean> {
233
+ // #152: a paused native-push backend cannot receive a reply, so its `replyable` is false by
234
+ // POLICY — no adapter is resolved or probed. This is not a liveness verdict about the citizen.
235
+ if (nativePushPaused(identity.backend)) return false;
232
236
  const resolveAdapter = deps.resolveAdapter ?? resolveNativePushAdapter;
233
237
  const probe = await resolveAdapter(identity.backend).probe(identity.nativeSessionId);
234
238
  return probe.status === "alive";
@@ -709,11 +709,13 @@ export function renderFreshCall(result: FreshCallResult): { text: string; isErro
709
709
  `\n` +
710
710
  `This is a LAUNCH receipt: tmux created that window and was asked to start the runtime with the model above. It does ` +
711
711
  `NOT mean the sibling is running, that its first turn ran, or that the task was delivered.\n` +
712
- `The sibling's garden id arrives separately — its first action is the zero-argument callback verb ` +
713
- `(codex is the one arm that still sends the nonce as arguments), and the sender envelope of THAT ` +
712
+ `The sibling's garden id arrives separately — before its task it calls the zero-argument callback verb ` +
713
+ `(codex is the one arm that still sends the nonce as arguments; a read-only look at the caller may come ` +
714
+ `first), and the sender envelope of THAT ` +
714
715
  `message is the address. The nonce above is the correlation tag it carries; the sibling reads it ` +
715
- `from its own launch env, never from this receipt. Nothing is polling for it; if it never comes, ` +
716
- `the window is visible and can be read directly.`,
716
+ `from its own launch env, never from this receipt. Nothing is polling for it; if it never comes — ` +
717
+ `not started, stuck, or held back after its check; this receipt cannot tell which — the window is visible ` +
718
+ `and can be read directly.`,
717
719
  isError: false,
718
720
  };
719
721
  }
@@ -19,10 +19,12 @@
19
19
  * deliverability semantics. `check-native-push-register` asserts this file references no
20
20
  * receiver-marker writer. (A dedicated register-provenance slot is a future concern.)
21
21
  *
22
- * LEAF-adjacent + pi-free: imports only the native-push adapter (pi-free) and the meta-session
23
- * upsert (pi-free), so the harness-neutral MCP bridge can reach it at boot.
22
+ * LEAF-adjacent + pi-free: imports only the native-push adapter (pi-free), the meta-session
23
+ * upsert (pi-free) and the pure v2 contract's paused-backend policy, so the harness-neutral MCP
24
+ * bridge can reach it at boot.
24
25
  */
25
26
 
27
+ import { nativePushPaused } from "../entwurf-v2-contract.ts";
26
28
  import { type MetaCitizenBackend, type UpsertAction, upsertMetaSession } from "../meta-session.ts";
27
29
  import { type NativePushAdapter, resolveNativePushAdapter } from "./adapter.ts";
28
30
 
@@ -62,6 +64,19 @@ export async function registerNativeConversation(
62
64
  input: RegisterNativeConversationInput,
63
65
  deps: RegisterNativeConversationDeps = {},
64
66
  ): Promise<RegisterNativeConversationResult> {
67
+ // #152: a paused native-push backend is refused by name BEFORE any adapter is resolved or
68
+ // probed — registration would otherwise reach a language server that refused the measured
69
+ // external calls (missing CSRF token).
70
+ // Nothing is probed and no record is written.
71
+ if (nativePushPaused(input.backend)) {
72
+ throw new Error(
73
+ `entwurf_register_native: refusing to register ${input.backend} conversation ${JSON.stringify(
74
+ input.nativeSessionId,
75
+ )} — native-push for ${input.backend} is paused (#152: its language server refused ` +
76
+ "entwurf's measured external calls for a missing CSRF token). Nothing was probed and no garden id was minted; " +
77
+ "existing records stay readable.",
78
+ );
79
+ }
65
80
  const resolveAdapter = deps.resolveAdapter ?? resolveNativePushAdapter;
66
81
  const adapter = resolveAdapter(input.backend);
67
82
  const probe = await adapter.probe(input.nativeSessionId);
@@ -20,8 +20,8 @@
20
20
  * callback callable — THE OMP-SPECIFIC ONE. `tools.xdev` defaults to TRUE, which mounts MCP
21
21
  * tools as `xd://` devices whose schemas never reach the prompt. The tool
22
22
  * would be configured, the bridge would be running, and the model still
23
- * could not call `mcp__entwurf_bridge_entwurf_v`. Step 9 clause 5 makes the
24
- * callback the FIRST action, so a fresh call onto a default-config host
23
+ * could not call `mcp__entwurf_bridge_entwurf_v`. Step 9 clause 5 puts the
24
+ * callback before the task, so a fresh call onto a default-config host
25
25
  * opens a window that can never name itself. `[측정]` #87 A-lane: the
26
26
  * vendor default wrapped the send tool and produced a false delivery report.
27
27
  *
@@ -15,7 +15,7 @@
15
15
  * provider spelled out (the bootstrap refuses a model resolved by inference), so
16
16
  * the model here is `<provider>/<model id>` split at the FIRST `/` — a catalog id
17
17
  * that itself contains `/` keeps it. A model with no provider part is refused.
18
- * first input the whole framing (callback first, then the task) rides the bootstrap's closed
18
+ * first input the whole framing (callback before the task) rides the bootstrap's closed
19
19
  * `{v:2,task}` payload, whose decoder caps `task` at 16000 characters. A framed
20
20
  * task over that cap would open a window whose bootstrap refuses its own argv.
21
21
  *
@@ -1,15 +1,16 @@
1
1
  /**
2
- * remote-pair-receive — the RECEIVING half of #107's one declared remote pair (rail 1).
2
+ * remote-pair-receive — the RECEIVING half of the declared remote pairs (#107 rail 1, #149 routes).
3
3
  *
4
4
  * The host that owns the target record runs this once per SSH call. It reads one JSON claim
5
- * from stdin, checks it against the operator-written pair declaration, and hands a matching
5
+ * from stdin, checks it against the operator-written pair declarations, and hands a matching
6
6
  * claim to the existing v2 dispatch. The record, owner and deliverability checks stay the
7
7
  * dispatch's own: nothing here writes a mailbox, resolves a rail, or retries.
8
8
  *
9
9
  * Trust model (GLG, #107 D3 (a)): the boundary is the same operator's SSH account. The pair
10
- * check is a SCOPE limit, not authentication: a claim whose `from`/`target` is not the declared
11
- * pair is refused before any mutation. It does not detect that same account sending the
12
- * declared gid falsely; nothing here claims to.
10
+ * check is a SCOPE limit, not authentication: a claim whose `from`/`target` is not one declared
11
+ * pair is refused before any mutation. Both ends are matched in the SAME route (#149), so a sender
12
+ * declared in one route cannot address the local citizen of another. It does not detect that same
13
+ * account sending a declared gid falsely; nothing here claims to.
13
14
  *
14
15
  * Sender: the stdin claim is the only sender input. Ambient carriers (`PI_SESSION_ID`,
15
16
  * `PI_AGENT_ID`, sender markers) in the SSH environment are never consulted, so this module
@@ -29,7 +30,7 @@ import type { EntwurfV2SurfaceRendered, SurfaceEntwurfV2Params } from "./entwurf
29
30
  import type { MailboxSenderEnvelope } from "./meta-mailbox-body.ts";
30
31
  import { requireGardenId } from "./meta-session.ts";
31
32
 
32
- /** One operator-declared pair, as written on the RECEIVING host. */
33
+ /** One operator-declared pair (one route), as written on the RECEIVING host. */
33
34
  export interface RemotePairDeclaration {
34
35
  /** The sender's garden id on the other host. */
35
36
  remoteGardenId: string;
@@ -131,24 +132,31 @@ export function parseRemoteClaim(text: string): RemoteClaim {
131
132
  };
132
133
  }
133
134
 
134
- /** The scope check. Null = inside the declared pair. */
135
- export function checkRemotePair(
135
+ /**
136
+ * The scope check: the one declared pair whose two ends are the claim's `from` and `target`, or a
137
+ * refusal. The two reasons are the ones every sender already reads (#107), so a sender on another
138
+ * release still classifies them: an undeclared `from` is a from-mismatch; a declared `from` with a
139
+ * target that is not ITS declared local (another route's local included) is a target-mismatch.
140
+ */
141
+ export function selectRemotePair(
136
142
  claim: RemoteClaim,
137
- decl: RemotePairDeclaration,
138
- ): { reason: RemotePairRejectReason; detail: string } | null {
139
- if (claim.from !== decl.remoteGardenId) {
143
+ decls: readonly RemotePairDeclaration[],
144
+ ): { decl: RemotePairDeclaration } | { reason: RemotePairRejectReason; detail: string } {
145
+ const fromRoutes = decls.filter((d) => d.remoteGardenId === claim.from);
146
+ if (fromRoutes.length === 0) {
140
147
  return {
141
148
  reason: "remote-pair-from-mismatch",
142
- detail: `from ${claim.from} is not the declared remote ${decl.remoteGardenId}`,
149
+ detail: `from ${claim.from} is not a declared remote (${decls.map((d) => d.remoteGardenId).join(", ")})`,
143
150
  };
144
151
  }
145
- if (claim.target !== decl.localGardenId) {
152
+ const decl = fromRoutes.find((d) => d.localGardenId === claim.target);
153
+ if (!decl) {
146
154
  return {
147
155
  reason: "remote-pair-target-mismatch",
148
- detail: `target ${claim.target} is not the declared local ${decl.localGardenId}`,
156
+ detail: `target ${claim.target} is not a declared local for remote ${claim.from} (${fromRoutes.map((d) => d.localGardenId).join(", ")})`,
149
157
  };
150
158
  }
151
- return null;
159
+ return { decl };
152
160
  }
153
161
 
154
162
  export function buildRemoteSenderEnvelope(
@@ -170,12 +178,12 @@ export function buildRemoteSenderEnvelope(
170
178
 
171
179
  export async function receiveRemoteClaim(
172
180
  claim: RemoteClaim,
173
- decl: RemotePairDeclaration,
181
+ decls: readonly RemotePairDeclaration[],
174
182
  deps: RemoteReceiveDeps,
175
183
  ): Promise<RemoteReceiveReceipt> {
176
- const mismatch = checkRemotePair(claim, decl);
177
- if (mismatch) return { kind: "rejected", ...mismatch };
178
- const sender = buildRemoteSenderEnvelope(claim, decl, (deps.now ?? (() => new Date()))(), deps.replyRoute);
184
+ const picked = selectRemotePair(claim, decls);
185
+ if (!("decl" in picked)) return { kind: "rejected", ...picked };
186
+ const sender = buildRemoteSenderEnvelope(claim, picked.decl, (deps.now ?? (() => new Date()))(), deps.replyRoute);
179
187
  const rendered = await deps.dispatch(
180
188
  { target: claim.target, intent: "fire-and-forget", message: claim.message, wants_reply: claim.wantsReply },
181
189
  sender,