@junghanacs/entwurf 0.41.1 → 0.45.0
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.
- package/AGENTS.md +2 -1
- package/BASELINE.md +7 -5
- package/CHANGELOG.md +344 -0
- package/DELIVERY.md +37 -20
- package/ENTWURF.json +236 -0
- package/README.md +75 -65
- package/VERIFY.md +9 -9
- package/docs/external-mcp-host.md +1 -1
- package/docs/setup-clean-host.md +59 -33
- package/mcp/entwurf-bridge/dist/ENTWURF.json +236 -0
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +32 -190
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/agent-guidance.js +299 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-self-address.js +10 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +23 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +7 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +23 -24
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-visible-resume.js +15 -11
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +21 -10
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +4 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +5 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +29 -24
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-resume-call.js +18 -16
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/register.js +13 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/omp-fresh-preflight.js +2 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-fresh-preflight.js +1 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/remote-pair-receive.js +25 -17
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/remote-pair-send.js +101 -32
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-omp.js +57 -7
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +23 -45
- package/mcp/entwurf-bridge/dist/scripts/remote-pair-receive.js +12 -12
- package/mcp/entwurf-bridge/src/index.ts +126 -321
- package/native/win32-x64/entwurf_win.node +0 -0
- package/native/win32-x64/manifest.json +2 -2
- package/package.json +5 -4
- package/pi-extensions/lib/agent-guidance.ts +341 -0
- package/pi-extensions/lib/entwurf-self-address.ts +13 -0
- package/pi-extensions/lib/entwurf-v2-contract.ts +25 -2
- package/pi-extensions/lib/entwurf-v2-decider.ts +7 -0
- package/pi-extensions/lib/entwurf-v2-surface.ts +27 -30
- package/pi-extensions/lib/entwurf-v2-visible-resume.ts +15 -16
- package/pi-extensions/lib/fresh-call-composition.ts +21 -10
- package/pi-extensions/lib/herdr-fresh-call.ts +4 -2
- package/pi-extensions/lib/meta-sender-identity.ts +4 -0
- package/pi-extensions/lib/mux-fresh-call.ts +29 -30
- package/pi-extensions/lib/mux-resume-call.ts +18 -19
- package/pi-extensions/lib/native-push/register.ts +17 -2
- package/pi-extensions/lib/omp-fresh-preflight.ts +2 -2
- package/pi-extensions/lib/pi-durable-fresh-preflight.ts +1 -1
- package/pi-extensions/lib/remote-pair-receive.ts +27 -19
- package/pi-extensions/lib/remote-pair-send.ts +124 -34
- package/pi-extensions/meta-bridge-omp.ts +69 -7
- package/pi-extensions/meta-bridge-pi-durable.ts +23 -64
- package/run.sh +182 -22
- package/scripts/build-bridge.sh +10 -0
- package/scripts/check-agy-sender-identity.ts +6 -4
- package/scripts/check-capability-bundle-reach.ts +86 -0
- package/scripts/check-entwurf-self-address.ts +37 -6
- package/scripts/check-entwurf-v2-contract.ts +24 -2
- package/scripts/check-entwurf-v2-production.ts +45 -31
- package/scripts/check-entwurf-v2-surface.ts +94 -98
- package/scripts/check-gate-qualification.ts +17 -13
- package/scripts/check-mux-resume-call.ts +20 -3
- package/scripts/check-native-push-adapter.ts +4 -0
- package/scripts/check-native-push-register.ts +32 -82
- package/scripts/check-remote-pair-installed.ts +2 -2
- package/scripts/dev-bin.sh +112 -1
- package/scripts/lib/chain-terminus-publication.ts +58 -0
- package/scripts/lib/codex-fresh-live-protocol.ts +30 -0
- package/scripts/lib/codex-fresh-source-receipts.ts +112 -3
- package/scripts/lib/pi-durable-fresh-live-receipts.ts +29 -1
- package/scripts/mutants/agent-guidance.json +160 -0
- package/scripts/mutants/bridge-boot-resume.json +6 -8
- package/scripts/mutants/capability-bundle-reach.json +124 -0
- package/scripts/mutants/chain-terminus-publication.json +17 -0
- package/scripts/mutants/codex-native.json +98 -0
- package/scripts/mutants/control-send-receipt.json +23 -6
- package/scripts/mutants/mux-fresh-call.json +30 -4
- package/scripts/mutants/mux-resume-call.json +28 -3
- package/scripts/mutants/native-push-paused.json +17 -0
- package/scripts/mutants/omp-fresh.json +13 -2
- package/scripts/mutants/pack-install.json +46 -0
- package/scripts/mutants/pi-durable.json +17 -5
- package/scripts/mutants/remote-pair-display.json +3 -3
- package/scripts/mutants/remote-pair-receive.json +41 -11
- package/scripts/mutants/remote-pair-roundtrip.json +67 -7
- package/scripts/mutants/remote-pair-send.json +177 -19
- package/scripts/mutants/remote-pair-transport.json +4 -4
- package/scripts/mutants/self-address.json +10 -10
- package/scripts/mutants/source-install.json +66 -0
- package/scripts/mutants/v2-surface.json +50 -64
- package/scripts/mutants/v2-visible-resume.json +11 -0
- package/scripts/remote-pair-receive.ts +12 -12
- package/scripts/smoke-agy-install-state.sh +121 -0
- package/scripts/smoke-agy-native-push-live.ts +13 -0
- package/scripts/smoke-codex-fresh-live.ts +50 -27
- package/scripts/smoke-entwurf-chain-live.ts +7 -9
- package/scripts/smoke-herdr-fresh-call-live.ts +4 -4
- package/scripts/smoke-herdr-plugin-build-live.ts +179 -50
- package/scripts/smoke-mux-lifecycle-live.ts +140 -8
- package/scripts/smoke-omp-fresh-live.ts +5 -4
- package/scripts/smoke-pi-durable-fresh-live.ts +29 -7
- package/scripts/smoke-setup-verdict.sh +27 -2
|
@@ -74,6 +74,7 @@
|
|
|
74
74
|
*/
|
|
75
75
|
|
|
76
76
|
import { ACP_MODEL_NOT_CURATED_HINT, AcpModelNotCuratedError } from "./acp/model-ids.js";
|
|
77
|
+
import { ENTWURF_RESUME_REJECT_HINTS } from "./agent-guidance.ts";
|
|
77
78
|
import { buildResumePiArgs } from "./entwurf-resume-args.ts";
|
|
78
79
|
import {
|
|
79
80
|
type AcquireLockResult,
|
|
@@ -633,24 +634,22 @@ export async function observeSameGidSocket(
|
|
|
633
634
|
}
|
|
634
635
|
}
|
|
635
636
|
|
|
637
|
+
// The static record/liveness hint text is authored in root ENTWURF.json (#154 C1-C); the reasons,
|
|
638
|
+
// this total table and the renderer stay here. acp-model-not-curated keeps the shared ACP constant,
|
|
639
|
+
// and the launch-stage mux hints folded into `detail` stay behind the mux import fence.
|
|
636
640
|
const REJECT_HINT: Record<VisibleResumeRejectReason, string> = {
|
|
637
|
-
"target-invalid": "
|
|
638
|
-
"target-backend-unsupported":
|
|
639
|
-
"that citizen's backend has no same-id resume (only pi and Claude Code do) — reach it with entwurf_v2, or open a fresh sibling",
|
|
641
|
+
"target-invalid": ENTWURF_RESUME_REJECT_HINTS["target-invalid"],
|
|
642
|
+
"target-backend-unsupported": ENTWURF_RESUME_REJECT_HINTS["target-backend-unsupported"],
|
|
640
643
|
"acp-model-not-curated": ACP_MODEL_NOT_CURATED_HINT,
|
|
641
|
-
"target-locked": "
|
|
642
|
-
"target-live": "
|
|
643
|
-
"target-indeterminate":
|
|
644
|
-
|
|
645
|
-
"target-receiver-
|
|
646
|
-
|
|
647
|
-
"target-receiver-
|
|
648
|
-
"target-
|
|
649
|
-
|
|
650
|
-
"target-receiver-unbound":
|
|
651
|
-
"the dead receiver owner is not bound to the record's last arm (none observed, a later attach, or the record moved), so a Claude Code process may still hold the session — if its window is closed, reopen it yourself",
|
|
652
|
-
"target-address-conflict": "that citizen's control-socket path is corrupt",
|
|
653
|
-
"launch-refused": "tmux was never asked to open a window",
|
|
644
|
+
"target-locked": ENTWURF_RESUME_REJECT_HINTS["target-locked"],
|
|
645
|
+
"target-live": ENTWURF_RESUME_REJECT_HINTS["target-live"],
|
|
646
|
+
"target-indeterminate": ENTWURF_RESUME_REJECT_HINTS["target-indeterminate"],
|
|
647
|
+
"target-receiver-absent": ENTWURF_RESUME_REJECT_HINTS["target-receiver-absent"],
|
|
648
|
+
"target-receiver-corrupt": ENTWURF_RESUME_REJECT_HINTS["target-receiver-corrupt"],
|
|
649
|
+
"target-receiver-mismatch": ENTWURF_RESUME_REJECT_HINTS["target-receiver-mismatch"],
|
|
650
|
+
"target-receiver-unbound": ENTWURF_RESUME_REJECT_HINTS["target-receiver-unbound"],
|
|
651
|
+
"target-address-conflict": ENTWURF_RESUME_REJECT_HINTS["target-address-conflict"],
|
|
652
|
+
"launch-refused": ENTWURF_RESUME_REJECT_HINTS["launch-refused"],
|
|
654
653
|
};
|
|
655
654
|
|
|
656
655
|
/**
|
|
@@ -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.
|
|
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
|
|
385
|
-
*
|
|
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
|
|
398
|
-
*
|
|
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
|
-
`
|
|
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
|
-
`
|
|
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
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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
|
|
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,
|
|
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";
|
|
@@ -87,6 +87,7 @@
|
|
|
87
87
|
*/
|
|
88
88
|
|
|
89
89
|
import { ACP_MODEL_NOT_CURATED_HINT } from "./acp/model-ids.js";
|
|
90
|
+
import { ENTWURF_FRESH_LAUNCH_REJECT_HINTS } from "./agent-guidance.ts";
|
|
90
91
|
import { callbackEnvAssignments } from "./callback-env.ts";
|
|
91
92
|
import { classifyTmuxCwd, type TmuxCwdRejectReason } from "./classify-tmux-cwd.ts";
|
|
92
93
|
import {
|
|
@@ -629,7 +630,9 @@ export function freshCall(
|
|
|
629
630
|
}
|
|
630
631
|
|
|
631
632
|
/** Why each refusal happened, in the caller's terms. A reason a caller cannot act on is a reason
|
|
632
|
-
* they will guess about.
|
|
633
|
+
* they will guess about. The static tmux launch-stage text is authored in root
|
|
634
|
+
* ENTWURF.json (#154 C1-D) and bound key by key, never spread, so a duplicate or missing reason
|
|
635
|
+
* still fails typecheck; slot-bearing, ACP and preflight-leaf hints stay code-owned. */
|
|
633
636
|
const REJECT_HINT: Record<FreshCallRejectReason, string> = {
|
|
634
637
|
// The Copilot capability reasons keep their repair text on the leaf that decides them, so
|
|
635
638
|
// the sentence an operator reads cannot drift away from the predicate that produced it.
|
|
@@ -641,34 +644,28 @@ const REJECT_HINT: Record<FreshCallRejectReason, string> = {
|
|
|
641
644
|
...CODEX_CALLER_PREFLIGHT_HINT,
|
|
642
645
|
...CODEX_CALLER_SEAT_HINT,
|
|
643
646
|
...FRESH_CALL_PROVIDER_HINT,
|
|
644
|
-
"no-tmux-context": "
|
|
645
|
-
"anchor-malformed": "
|
|
646
|
-
"anchor-unresolved": "
|
|
647
|
-
"anchor-mismatch": "
|
|
648
|
-
"caller-identity-unavailable":
|
|
649
|
-
|
|
650
|
-
"cwd-
|
|
651
|
-
|
|
652
|
-
"cwd-
|
|
653
|
-
|
|
654
|
-
"
|
|
655
|
-
|
|
656
|
-
"cwd-not-directory": "the requested cwd exists but is not a directory",
|
|
657
|
-
"tmux-session-name-invalid":
|
|
658
|
-
"the requested tmux session name is outside the shape this rail addresses (start with a letter or digit, then letters, digits, '_' or '-') — some other shapes tmux cannot resolve at all ('#' is expanded when the name is stored; '.' and ':' are its own pane/window separators inside a target; a name like '$0' loses to the session id '$0'), and the rest are declined to keep one narrow grammar, so rename the session or open one whose name fits",
|
|
659
|
-
"tmux-session-missing":
|
|
660
|
-
"no session with that exact name answers on this agent's tmux server (or that server stopped answering) — nothing was created, so open the session yourself and call again",
|
|
661
|
-
"model-empty": "model is empty after trimming; fresh calls require an explicit model",
|
|
647
|
+
"no-tmux-context": ENTWURF_FRESH_LAUNCH_REJECT_HINTS["no-tmux-context"],
|
|
648
|
+
"anchor-malformed": ENTWURF_FRESH_LAUNCH_REJECT_HINTS["anchor-malformed"],
|
|
649
|
+
"anchor-unresolved": ENTWURF_FRESH_LAUNCH_REJECT_HINTS["anchor-unresolved"],
|
|
650
|
+
"anchor-mismatch": ENTWURF_FRESH_LAUNCH_REJECT_HINTS["anchor-mismatch"],
|
|
651
|
+
"caller-identity-unavailable": ENTWURF_FRESH_LAUNCH_REJECT_HINTS["caller-identity-unavailable"],
|
|
652
|
+
"cwd-not-absolute": ENTWURF_FRESH_LAUNCH_REJECT_HINTS["cwd-not-absolute"],
|
|
653
|
+
"cwd-format-token": ENTWURF_FRESH_LAUNCH_REJECT_HINTS["cwd-format-token"],
|
|
654
|
+
"cwd-missing": ENTWURF_FRESH_LAUNCH_REJECT_HINTS["cwd-missing"],
|
|
655
|
+
"cwd-not-directory": ENTWURF_FRESH_LAUNCH_REJECT_HINTS["cwd-not-directory"],
|
|
656
|
+
"tmux-session-name-invalid": ENTWURF_FRESH_LAUNCH_REJECT_HINTS["tmux-session-name-invalid"],
|
|
657
|
+
"tmux-session-missing": ENTWURF_FRESH_LAUNCH_REJECT_HINTS["tmux-session-missing"],
|
|
658
|
+
"model-empty": ENTWURF_FRESH_LAUNCH_REJECT_HINTS["model-empty"],
|
|
662
659
|
"model-invalid": `model must be one ${MODEL_MAX_CHARS}-character argv-safe id/alias without whitespace or tmux syntax`,
|
|
663
660
|
"acp-model-not-curated": ACP_MODEL_NOT_CURATED_HINT,
|
|
664
|
-
"task-empty": "task
|
|
661
|
+
"task-empty": ENTWURF_FRESH_LAUNCH_REJECT_HINTS["task-empty"],
|
|
665
662
|
"task-too-long": `task exceeds ${TASK_MAX_CHARS} characters`,
|
|
666
|
-
"runtime-unresolved": "
|
|
667
|
-
"runtime-not-absolute": "
|
|
668
|
-
"runtime-path-whitespace": "
|
|
669
|
-
"runtime-missing": "
|
|
670
|
-
"runtime-not-regular-file": "
|
|
671
|
-
"runtime-not-executable": "
|
|
663
|
+
"runtime-unresolved": ENTWURF_FRESH_LAUNCH_REJECT_HINTS["runtime-unresolved"],
|
|
664
|
+
"runtime-not-absolute": ENTWURF_FRESH_LAUNCH_REJECT_HINTS["runtime-not-absolute"],
|
|
665
|
+
"runtime-path-whitespace": ENTWURF_FRESH_LAUNCH_REJECT_HINTS["runtime-path-whitespace"],
|
|
666
|
+
"runtime-missing": ENTWURF_FRESH_LAUNCH_REJECT_HINTS["runtime-missing"],
|
|
667
|
+
"runtime-not-regular-file": ENTWURF_FRESH_LAUNCH_REJECT_HINTS["runtime-not-regular-file"],
|
|
668
|
+
"runtime-not-executable": ENTWURF_FRESH_LAUNCH_REJECT_HINTS["runtime-not-executable"],
|
|
672
669
|
};
|
|
673
670
|
|
|
674
671
|
/**
|
|
@@ -709,11 +706,13 @@ export function renderFreshCall(result: FreshCallResult): { text: string; isErro
|
|
|
709
706
|
`\n` +
|
|
710
707
|
`This is a LAUNCH receipt: tmux created that window and was asked to start the runtime with the model above. It does ` +
|
|
711
708
|
`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
|
|
713
|
-
`(codex is the one arm that still sends the nonce as arguments
|
|
709
|
+
`The sibling's garden id arrives separately — before its task it calls the zero-argument callback verb ` +
|
|
710
|
+
`(codex is the one arm that still sends the nonce as arguments; a read-only look at the caller may come ` +
|
|
711
|
+
`first), and the sender envelope of THAT ` +
|
|
714
712
|
`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
|
|
713
|
+
`from its own launch env, never from this receipt. Nothing is polling for it; if it never comes — ` +
|
|
714
|
+
`not started, stuck, or held back after its check; this receipt cannot tell which — the window is visible ` +
|
|
715
|
+
`and can be read directly.`,
|
|
717
716
|
isError: false,
|
|
718
717
|
};
|
|
719
718
|
}
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
* to acceptance, not to the product's launch receipt.
|
|
50
50
|
*/
|
|
51
51
|
|
|
52
|
+
import { ENTWURF_RESUME_LAUNCH_REJECT_HINTS } from "./agent-guidance.ts";
|
|
52
53
|
import { classifyTmuxCwd, type TmuxCwdRejectReason } from "./classify-tmux-cwd.ts";
|
|
53
54
|
import {
|
|
54
55
|
assertLaunchTarget,
|
|
@@ -187,24 +188,22 @@ export function resumeCall(
|
|
|
187
188
|
}
|
|
188
189
|
|
|
189
190
|
/** Why each refusal happened, in the caller's terms. A reason a caller cannot act on is a reason
|
|
190
|
-
* they will guess about.
|
|
191
|
+
* they will guess about. The text is authored in root ENTWURF.json (#154 C1-D); each
|
|
192
|
+
* key is bound explicitly so the reason union stays exhaustive here. */
|
|
191
193
|
export const RESUME_CALL_REJECT_HINT: Record<ResumeCallRejectReason, string> = {
|
|
192
|
-
"no-tmux-context": "
|
|
193
|
-
"anchor-malformed": "
|
|
194
|
-
"anchor-unresolved": "
|
|
195
|
-
"anchor-mismatch": "
|
|
196
|
-
"cwd-not-absolute": "
|
|
197
|
-
"cwd-format-token":
|
|
198
|
-
|
|
199
|
-
"cwd-
|
|
200
|
-
|
|
201
|
-
"
|
|
202
|
-
"
|
|
203
|
-
|
|
204
|
-
"runtime-
|
|
205
|
-
"runtime-not-
|
|
206
|
-
"runtime-
|
|
207
|
-
"runtime-missing": "nothing exists at the resolved runtime path",
|
|
208
|
-
"runtime-not-regular-file": "the resolved runtime path is not a regular file",
|
|
209
|
-
"runtime-not-executable": "the resolved runtime path is not executable",
|
|
194
|
+
"no-tmux-context": ENTWURF_RESUME_LAUNCH_REJECT_HINTS["no-tmux-context"],
|
|
195
|
+
"anchor-malformed": ENTWURF_RESUME_LAUNCH_REJECT_HINTS["anchor-malformed"],
|
|
196
|
+
"anchor-unresolved": ENTWURF_RESUME_LAUNCH_REJECT_HINTS["anchor-unresolved"],
|
|
197
|
+
"anchor-mismatch": ENTWURF_RESUME_LAUNCH_REJECT_HINTS["anchor-mismatch"],
|
|
198
|
+
"cwd-not-absolute": ENTWURF_RESUME_LAUNCH_REJECT_HINTS["cwd-not-absolute"],
|
|
199
|
+
"cwd-format-token": ENTWURF_RESUME_LAUNCH_REJECT_HINTS["cwd-format-token"],
|
|
200
|
+
"cwd-missing": ENTWURF_RESUME_LAUNCH_REJECT_HINTS["cwd-missing"],
|
|
201
|
+
"cwd-not-directory": ENTWURF_RESUME_LAUNCH_REJECT_HINTS["cwd-not-directory"],
|
|
202
|
+
"herdr-resume-unsupported": ENTWURF_RESUME_LAUNCH_REJECT_HINTS["herdr-resume-unsupported"],
|
|
203
|
+
"runtime-unresolved": ENTWURF_RESUME_LAUNCH_REJECT_HINTS["runtime-unresolved"],
|
|
204
|
+
"runtime-not-absolute": ENTWURF_RESUME_LAUNCH_REJECT_HINTS["runtime-not-absolute"],
|
|
205
|
+
"runtime-path-whitespace": ENTWURF_RESUME_LAUNCH_REJECT_HINTS["runtime-path-whitespace"],
|
|
206
|
+
"runtime-missing": ENTWURF_RESUME_LAUNCH_REJECT_HINTS["runtime-missing"],
|
|
207
|
+
"runtime-not-regular-file": ENTWURF_RESUME_LAUNCH_REJECT_HINTS["runtime-not-regular-file"],
|
|
208
|
+
"runtime-not-executable": ENTWURF_RESUME_LAUNCH_REJECT_HINTS["runtime-not-executable"],
|
|
210
209
|
};
|
|
@@ -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)
|
|
23
|
-
* upsert (pi-free), so the harness-neutral MCP
|
|
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
|
|
24
|
-
* callback the
|
|
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
|
|
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
|
|
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
|
|
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
|
|
11
|
-
* pair is refused before any mutation.
|
|
12
|
-
* declared
|
|
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
|
-
/**
|
|
135
|
-
|
|
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
|
-
|
|
138
|
-
): { reason: RemotePairRejectReason; detail: string }
|
|
139
|
-
|
|
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
|
|
149
|
+
detail: `from ${claim.from} is not a declared remote (${decls.map((d) => d.remoteGardenId).join(", ")})`,
|
|
143
150
|
};
|
|
144
151
|
}
|
|
145
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
181
|
+
decls: readonly RemotePairDeclaration[],
|
|
174
182
|
deps: RemoteReceiveDeps,
|
|
175
183
|
): Promise<RemoteReceiveReceipt> {
|
|
176
|
-
const
|
|
177
|
-
if (
|
|
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,
|