@junghanacs/entwurf 0.13.0 → 0.14.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 +52 -17
- package/BASELINE.md +60 -206
- package/CHANGELOG.md +64 -0
- package/CONTRIBUTING.md +14 -10
- package/DELIVERY.md +118 -261
- package/README.md +91 -431
- package/VERIFY.md +74 -95
- package/demo/README.md +1 -1
- package/demo/demo-baseline.sh +1 -3
- package/demo/demo.sh +2 -5
- package/docs/acp-backend-rail.md +236 -0
- package/docs/external-mcp-host.md +131 -0
- package/docs/fresh-cut-policy.md +99 -0
- package/docs/setup-clean-host.md +124 -328
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +160 -67
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/acp-client.js +12 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +11 -10
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/config.js +1 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/engraving.js +42 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +4 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/overlay.js +11 -9
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-control-rpc.js +7 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-core.js +13 -14
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-resume-args.js +45 -40
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +117 -95
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +23 -57
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-lock.js +16 -7
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +5 -53
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-release.js +21 -36
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +3 -15
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send-fallback.js +12 -11
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +2 -7
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +30 -67
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-visible-resume.js +256 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +91 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +258 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-launch.js +202 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-placement.js +289 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-resume-call.js +170 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/resume-launch-identity.js +136 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/session-id.js +8 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/socket-discovery.js +3 -3
- package/mcp/entwurf-bridge/dist/scripts/meta-facts.js +51 -0
- package/mcp/entwurf-bridge/dist/scripts/new-session-id.js +9 -4
- package/mcp/entwurf-bridge/src/index.ts +176 -67
- package/mcp/entwurf-bridge/start.sh +2 -2
- package/mcp/entwurf-bridge/test.sh +23 -9
- package/mcp/entwurf-bridge/tsconfig.build.json +11 -2
- package/package.json +25 -11
- package/pi-extensions/entwurf-control.ts +220 -71
- package/pi-extensions/lib/acp/acp-client.ts +22 -0
- package/pi-extensions/lib/acp/augment.ts +42 -3
- package/pi-extensions/lib/acp/backend-adapter.ts +12 -11
- package/pi-extensions/lib/acp/backend.ts +366 -38
- package/pi-extensions/lib/acp/config.ts +1 -1
- package/pi-extensions/lib/acp/engraving.ts +43 -1
- package/pi-extensions/lib/acp/event-mapper.ts +16 -13
- package/pi-extensions/lib/acp/models.ts +4 -4
- package/pi-extensions/lib/acp/overlay.ts +11 -9
- package/pi-extensions/lib/entwurf-control-rpc.ts +7 -5
- package/pi-extensions/lib/entwurf-core.ts +15 -15
- package/pi-extensions/lib/entwurf-resume-args.ts +41 -52
- package/pi-extensions/lib/entwurf-v2-contract-schema.ts +1 -1
- package/pi-extensions/lib/entwurf-v2-contract.ts +120 -99
- package/pi-extensions/lib/entwurf-v2-decider.ts +30 -91
- package/pi-extensions/lib/entwurf-v2-lock.ts +16 -7
- package/pi-extensions/lib/entwurf-v2-production.ts +4 -78
- package/pi-extensions/lib/entwurf-v2-release.ts +25 -49
- package/pi-extensions/lib/entwurf-v2-runner.ts +6 -21
- package/pi-extensions/lib/entwurf-v2-send-fallback.ts +12 -11
- package/pi-extensions/lib/entwurf-v2-send.ts +2 -7
- package/pi-extensions/lib/entwurf-v2-surface.ts +36 -76
- package/pi-extensions/lib/entwurf-v2-visible-resume.ts +370 -0
- package/pi-extensions/lib/meta-session.ts +93 -5
- package/pi-extensions/lib/mux-fresh-call.ts +328 -0
- package/pi-extensions/lib/mux-launch.ts +267 -0
- package/pi-extensions/lib/mux-placement.ts +387 -0
- package/pi-extensions/lib/mux-resume-call.ts +221 -0
- package/pi-extensions/lib/resume-launch-identity.ts +162 -0
- package/pi-extensions/lib/session-id.js +8 -5
- package/pi-extensions/lib/socket-discovery.ts +3 -3
- package/prompts/engraving.md +13 -5
- package/run.sh +688 -282
- package/scripts/agy-bridge-config.py +5 -1
- package/scripts/check-acp-backend-preflight.ts +1 -1
- package/scripts/check-acp-carrier-augment.ts +246 -10
- package/scripts/check-acp-cortex.ts +5 -5
- package/scripts/check-acp-overlay.ts +13 -3
- package/scripts/check-acp-prompt-lifecycle.ts +565 -0
- package/scripts/check-acp-stop-reason.ts +342 -0
- package/scripts/check-acp-stream-hooks.ts +504 -0
- package/scripts/check-elapsed.sh +25 -0
- package/scripts/check-entwurf-bridge-boot.ts +51 -4
- package/scripts/check-entwurf-bridge-pi-free.ts +6 -5
- package/scripts/check-entwurf-control-rpc.ts +4 -3
- package/scripts/check-entwurf-resume-args.ts +72 -70
- package/scripts/check-entwurf-session-identity.ts +14 -10
- package/scripts/check-entwurf-v2-contract.ts +34 -59
- package/scripts/check-entwurf-v2-decider.ts +17 -177
- package/scripts/check-entwurf-v2-lock.ts +5 -2
- package/scripts/check-entwurf-v2-matrix.ts +3 -53
- package/scripts/check-entwurf-v2-production.ts +2 -91
- package/scripts/check-entwurf-v2-release.ts +10 -105
- package/scripts/check-entwurf-v2-runner.ts +4 -85
- package/scripts/check-entwurf-v2-send-fallback.ts +5 -6
- package/scripts/check-entwurf-v2-send.ts +0 -28
- package/scripts/check-entwurf-v2-surface.ts +198 -128
- package/scripts/check-entwurf-v2-visible-resume.ts +445 -0
- package/scripts/check-fresh-cut-gate.sh +1 -1
- package/scripts/check-gate-qualification.ts +101 -7
- package/scripts/check-install-container.sh +10 -2
- package/scripts/check-install-surface.ts +1 -1
- package/scripts/check-keyset-overlap.py +1 -1
- package/scripts/check-meta-facts.ts +249 -0
- package/scripts/check-meta-identity-consumers.ts +1 -1
- package/scripts/check-meta-session.ts +169 -0
- package/scripts/check-mux-launch-tmux.ts +316 -0
- package/scripts/check-mux-launch.ts +288 -0
- package/scripts/check-mux-launcher-fence.ts +264 -0
- package/scripts/check-mux-parent-artifact.ts +195 -0
- package/scripts/check-mux-placement-tmux.ts +322 -0
- package/scripts/check-mux-placement.ts +323 -0
- package/scripts/check-mux-resume-call.ts +282 -0
- package/scripts/check-probe-cli-shim.ts +25 -22
- package/scripts/check-probe-ordering.ts +121 -81
- package/scripts/check-release-gate-outcomes.ts +452 -0
- package/scripts/check-resume-launch-identity.ts +244 -0
- package/scripts/check-socket-discovery.ts +1 -1
- package/scripts/fixtures/mux-parent-transcript.scrubbed.jsonl +3 -0
- package/scripts/inventory-verification-surface.ts +349 -0
- package/scripts/lib/claude-launcher-fence.ts +322 -0
- package/scripts/lib/live-skip.ts +33 -0
- package/scripts/lib/mutation-qualify.ts +109 -3
- package/scripts/lib/probe-acp-turn.ts +33 -12
- package/scripts/lib/step-outcome.sh +88 -0
- package/scripts/meta-bridge-doctor.sh +6 -8
- package/scripts/meta-facts.ts +60 -0
- package/scripts/mutants/acp-augment.json +106 -0
- package/scripts/mutants/acp-cortex.json +2 -2
- package/scripts/mutants/acp-overlay.json +17 -0
- package/scripts/mutants/acp-prompt-lifecycle.json +100 -0
- package/scripts/mutants/acp-stop-reason.json +80 -0
- package/scripts/mutants/acp-stream-hooks.json +158 -0
- package/scripts/mutants/bridge-boot-resume.json +45 -0
- package/scripts/mutants/meta-facts.json +50 -0
- package/scripts/mutants/meta-identity.json +36 -0
- package/scripts/mutants/mux-boundary.json +196 -0
- package/scripts/mutants/mux-fresh-call.json +185 -0
- package/scripts/mutants/mux-launcher-fence.json +123 -0
- package/scripts/mutants/mux-parent-artifact.json +39 -0
- package/scripts/mutants/mux-resume-call.json +148 -0
- package/scripts/mutants/probe-ordering.json +6 -1021
- package/scripts/mutants/release-gate.json +140 -0
- package/scripts/mutants/resume-args.json +76 -0
- package/scripts/mutants/resume-launch-identity.json +96 -0
- package/scripts/mutants/v2-surface.json +74 -23
- package/scripts/mutants/v2-visible-resume.json +215 -0
- package/scripts/new-session-id.ts +9 -4
- package/scripts/smoke-acp-bundled-mcp-live.ts +4 -3
- package/scripts/smoke-acp-carrier-augment-live.ts +2 -2
- package/scripts/smoke-acp-cortex-live.ts +8 -14
- package/scripts/smoke-acp-long-turn-live.ts +185 -0
- package/scripts/smoke-acp-mcp-live.ts +2 -2
- package/scripts/smoke-acp-memory-containment-live.ts +2 -2
- package/scripts/smoke-acp-ordering-probe-live.ts +2 -2
- package/scripts/smoke-acp-overlay-live.ts +2 -2
- package/scripts/smoke-acp-provider-live.ts +2 -2
- package/scripts/smoke-acp-raw-turn-live.ts +3 -3
- package/scripts/smoke-acp-session-reuse-live.ts +2 -2
- package/scripts/smoke-acp-skill-live.ts +2 -2
- package/scripts/smoke-acp-socket-citizen-live.ts +4 -3
- package/scripts/smoke-acp-v2-send-live.ts +4 -3
- package/scripts/smoke-agy-native-push-live.ts +10 -20
- package/scripts/smoke-claude-native-resume-live.sh +13 -3
- package/scripts/smoke-entwurf-chain-live.ts +352 -0
- package/scripts/smoke-entwurf-v2-matrix-live.ts +3 -3
- package/scripts/smoke-meta-honesty.sh +1 -1
- package/scripts/smoke-meta-install-state.sh +4 -0
- package/scripts/smoke-mux-fresh-call-live.ts +365 -0
- package/scripts/smoke-mux-lifecycle-live.ts +1136 -0
- package/scripts/smoke-pi-attach.ts +1 -1
- package/scripts/smoke-user-scope-citizen.sh +1 -1
- package/scripts/tsconfig.json +1 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-preflight.js +0 -160
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-spawn-production.js +0 -273
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-spawn.js +0 -216
- package/pi-extensions/lib/entwurf-v2-spawn-production.ts +0 -373
- package/pi-extensions/lib/entwurf-v2-spawn.ts +0 -323
- package/scripts/check-acp-sdk-surface.ts +0 -236
- package/scripts/check-entwurf-v2-spawn-production.ts +0 -551
- package/scripts/check-entwurf-v2-spawn.ts +0 -399
- package/scripts/smoke-entwurf-v2-spawn-live.ts +0 -188
- package/scripts/smoke-entwurf-v2-spawn-resume-live.ts +0 -469
|
@@ -1,323 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* entwurf-v2-spawn — the 5c-3a spawn-bg RESUME watcher hand (0.11 Stage 0 step 5c-3a).
|
|
3
|
-
* It WIRES the real spawn + socket-observe IO onto the pure release reducer (5c-1):
|
|
4
|
-
* launch the resume child, watch for the FIRST observable liveness transition, feed
|
|
5
|
-
* that observation to `reduceRelease`, and release the held lock EXACTLY ONCE — or, if
|
|
6
|
-
* NO observation can be obtained, RETAIN the lock and surface the evidence rather than
|
|
7
|
-
* release blind. Every IO seam is an injected dep (the gate fakes it with controlled
|
|
8
|
-
* promises), so the spawn→observe→release ORDERING is gate-provable without a real
|
|
9
|
-
* child, socket, or timer — the same "pure-before-IO, IO-via-dep" discipline 5b/5c-2
|
|
10
|
-
* used.
|
|
11
|
-
*
|
|
12
|
-
* The load-bearing contract (Fable 3, the whole reason 5c is sliced pure-before-IO):
|
|
13
|
-
* TIMEOUT IS NOT A RELEASE EVENT. A spawn-bg dispatch holds its per-gid lock until an
|
|
14
|
-
* OBSERVED transition — `socket-alive` (the resumed child stood its control socket up)
|
|
15
|
-
* or `child-exited` (any code, incl. null = killed by signal). A bare `observeTimeoutMs`
|
|
16
|
-
* expiry proves NOTHING (the child may stand its socket up a moment later), so releasing
|
|
17
|
-
* on it would reopen the exact double-spawn window 5a's lock exists to close. Instead the
|
|
18
|
-
* timeout RESOLVES BY OBSERVATION: kill the child, then wait a BOUNDED `killGraceMs` for
|
|
19
|
-
* the kill to produce a real `child-exited` (or a racing `socket-alive`). Only THAT
|
|
20
|
-
* observation releases. If even the grace elapses with no observation, the hand does NOT
|
|
21
|
-
* release — it returns a `lock-retained` diagnostic (released:false, with pid / socket /
|
|
22
|
-
* lockPath / timeouts) so an operator can SEE the long-held lock, exactly as F2-P2
|
|
23
|
-
* ("관측 가능해야 수용") demands. The function always returns BOUNDED — it never hangs.
|
|
24
|
-
*
|
|
25
|
-
* Post-spawn unexpected dep failure follows the SAME rule (GPT 5c-3a correction): once a
|
|
26
|
-
* child exists, an observation-less release is forbidden. A watch/timer/kill dep that
|
|
27
|
-
* throws is handled by best-effort kill → bounded attempt to OBSERVE the exit → release
|
|
28
|
-
* if observed, else `lock-retained` fail-closed. There is NO direct-release escape hatch:
|
|
29
|
-
* `deps.releaseLock` is reached ONLY through `reduceRelease` on a real observation event.
|
|
30
|
-
*
|
|
31
|
-
* Release authority is ALWAYS the LockClaim the decider handed over; the watcher reads
|
|
32
|
-
* ONLY `plan.expectedSocketPath` and never re-derives a socket path or a lock by gid.
|
|
33
|
-
*/
|
|
34
|
-
|
|
35
|
-
import type { ExecutionPlan } from "./entwurf-v2-decider.ts";
|
|
36
|
-
import type { LockClaim } from "./entwurf-v2-lock.ts";
|
|
37
|
-
import {
|
|
38
|
-
decideReleasePolicy,
|
|
39
|
-
initialReleaseState,
|
|
40
|
-
type ReleaseEvent,
|
|
41
|
-
type ReleasePolicy,
|
|
42
|
-
type ReleaseState,
|
|
43
|
-
reduceRelease,
|
|
44
|
-
} from "./entwurf-v2-release.ts";
|
|
45
|
-
|
|
46
|
-
/** The spawn-bg plan shape, narrowed from the decider's ExecutionPlan union. */
|
|
47
|
-
export type SpawnBgPlan = Extract<ExecutionPlan, { transport: "spawn-bg" }>;
|
|
48
|
-
|
|
49
|
-
/** A started resume child. `pid` is diagnostic-only (surfaced on a retained lock); the
|
|
50
|
-
* watcher tracks the child by this opaque handle, never by gid. */
|
|
51
|
-
export interface SpawnedChild {
|
|
52
|
-
pid?: number;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Every IO seam is a REQUIRED dep — the hand performs ZERO IO of its own so the gate can
|
|
57
|
-
* drive every observation order without a real spawn/socket/timer.
|
|
58
|
-
* - spawnChild — launch the resume child. Resolves a handle on a STARTED process;
|
|
59
|
-
* THROWS on a spawn-time failure (→ `spawn-start-failed`: no child to watch).
|
|
60
|
-
* - awaitSocketAlive — resolve when `expectedSocketPath` becomes observable (the child
|
|
61
|
-
* stood its control socket up). NEVER resolves if the socket never appears — the
|
|
62
|
-
* timeout path handles that. Honors the abort signal so a loser can be torn down.
|
|
63
|
-
* - awaitChildExit — resolve with the child's exit code (null = killed by signal).
|
|
64
|
-
* Honors the abort signal.
|
|
65
|
-
* - awaitTimeout — resolve after `ms` (the observe deadline, and again the kill
|
|
66
|
-
* grace). Honors the abort signal. NOTE: a timeout is NEVER fed to the reducer.
|
|
67
|
-
* - killChild — terminate the started child. Used ONLY to escalate a timeout (or
|
|
68
|
-
* a post-spawn dep failure) into a real `child-exited`; it is never a release itself.
|
|
69
|
-
* - releaseLock — release the held claim; reached ONLY via reduceRelease, at most
|
|
70
|
-
* once, only on a real observation event.
|
|
71
|
-
* - killGraceMs — bounded wait, after a kill, for the resulting observation. Keeps
|
|
72
|
-
* the watcher from hanging on a zombie: if it elapses, the lock is RETAINED (surfaced),
|
|
73
|
-
* not released. A watcher-policy constant (NOT a routing decision — so it lives on the
|
|
74
|
-
* dep, not the decider's plan).
|
|
75
|
-
*/
|
|
76
|
-
export interface SpawnBgResumeDeps {
|
|
77
|
-
spawnChild: (plan: SpawnBgPlan) => Promise<SpawnedChild>;
|
|
78
|
-
awaitSocketAlive: (socketPath: string, signal: AbortSignal) => Promise<void>;
|
|
79
|
-
awaitChildExit: (child: SpawnedChild, signal: AbortSignal) => Promise<number | null>;
|
|
80
|
-
awaitTimeout: (ms: number, signal: AbortSignal) => Promise<void>;
|
|
81
|
-
killChild: (child: SpawnedChild) => void;
|
|
82
|
-
releaseLock: (lock: LockClaim) => void;
|
|
83
|
-
killGraceMs: number;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/** Evidence carried on a `lock-retained` outcome so an operator can SEE (and clear) the
|
|
87
|
-
* lock that no observation released. */
|
|
88
|
-
export interface SpawnRetainedDiagnostic {
|
|
89
|
-
targetGardenId: string;
|
|
90
|
-
pid?: number;
|
|
91
|
-
expectedSocketPath: string;
|
|
92
|
-
lockPath: string;
|
|
93
|
-
observeTimeoutMs: number;
|
|
94
|
-
killGraceMs: number;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* The terminal outcome of a spawn-bg resume watch. The three RELEASED outcomes each fed
|
|
99
|
-
* exactly one observation event to the reducer; the `lock-retained` outcome fed NONE (it
|
|
100
|
-
* is the observation-less fail-closed — `released:false`, lock left for the operator).
|
|
101
|
-
* - socket-alive — the resumed child stood its socket up.
|
|
102
|
-
* - child-exited — the child exited (any code; null = signal/kill).
|
|
103
|
-
* - spawn-start-failed — the child never started (spawnChild threw); nothing to watch.
|
|
104
|
-
* - lock-retained — no observation was obtainable (kill grace elapsed, or a
|
|
105
|
-
* post-spawn dep failed and the exit could not be observed). Lock NOT released.
|
|
106
|
-
*/
|
|
107
|
-
export type SpawnBgResumeResult =
|
|
108
|
-
| { kind: "socket-alive"; released: true; pid?: number }
|
|
109
|
-
| { kind: "child-exited"; released: true; exitCode: number | null; pid?: number }
|
|
110
|
-
| { kind: "spawn-start-failed"; released: true; error: string }
|
|
111
|
-
| {
|
|
112
|
-
kind: "lock-retained";
|
|
113
|
-
released: false;
|
|
114
|
-
reason: "kill-unconfirmed" | "observe-failed";
|
|
115
|
-
error?: string;
|
|
116
|
-
diagnostic: SpawnRetainedDiagnostic;
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
// An internal tag for the observation race: which source settled first.
|
|
120
|
-
type Observation = { tag: "socket" } | { tag: "exit"; code: number | null } | { tag: "timeout" };
|
|
121
|
-
|
|
122
|
-
function tagSocket(p: Promise<void>): Promise<Observation> {
|
|
123
|
-
return p.then(() => ({ tag: "socket" }) as const);
|
|
124
|
-
}
|
|
125
|
-
function tagExit(p: Promise<number | null>): Promise<Observation> {
|
|
126
|
-
return p.then((code) => ({ tag: "exit", code }) as const);
|
|
127
|
-
}
|
|
128
|
-
function tagTimeout(p: Promise<void>): Promise<Observation> {
|
|
129
|
-
return p.then(() => ({ tag: "timeout" }) as const);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function errMsg(err: unknown): string {
|
|
133
|
-
return err instanceof Error ? err.message : String(err);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/** Swallow a watcher promise's eventual rejection when it is abandoned without a race
|
|
137
|
-
* handler (the S1 sync-throw path tears down before the race attaches one), so an aborted
|
|
138
|
-
* loser cannot surface as an unhandledRejection. A no-op when the promise never started. */
|
|
139
|
-
function defuse(p: Promise<unknown> | undefined): void {
|
|
140
|
-
if (p) void p.catch(() => {});
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Launch a spawn-bg resume child and watch for the first observable liveness transition,
|
|
145
|
-
* releasing the held lock EXACTLY ONCE on that observation — or RETAINING the lock (with
|
|
146
|
-
* surfaced evidence) if no observation can be obtained. `lock` MUST be the in-domain claim
|
|
147
|
-
* the decider handed over — `decideReleasePolicy` throws if it is null or paired with the
|
|
148
|
-
* wrong gid (a mis-wire is fail-loud, not a runtime branch). Always returns bounded.
|
|
149
|
-
*
|
|
150
|
-
* A `releaseLock` throw on a RELEASED path propagates honestly (the observation already
|
|
151
|
-
* happened, so the caller must NOT re-spawn; the lock is dirty and needs manual cleanup —
|
|
152
|
-
* 5b masking direction). Such a throw is the ONE non-result exit; every other terminal
|
|
153
|
-
* state is a returned `SpawnBgResumeResult`.
|
|
154
|
-
*/
|
|
155
|
-
export async function executeSpawnBgResume(
|
|
156
|
-
plan: SpawnBgPlan,
|
|
157
|
-
lock: LockClaim | null,
|
|
158
|
-
deps: SpawnBgResumeDeps,
|
|
159
|
-
): Promise<SpawnBgResumeResult> {
|
|
160
|
-
// Throws on a null / mis-paired lock (?7 + gid invariants). After this the spawn-bg
|
|
161
|
-
// policy is release-after-spawn-observation and the lock is non-null.
|
|
162
|
-
const policy: ReleasePolicy = decideReleasePolicy(plan, lock);
|
|
163
|
-
const held = lock as LockClaim;
|
|
164
|
-
|
|
165
|
-
let state: ReleaseState = initialReleaseState();
|
|
166
|
-
// The ONLY path to deps.releaseLock: fold a real observation event through the reducer
|
|
167
|
-
// and release iff (and exactly once) the reducer says so. No direct-release hatch.
|
|
168
|
-
const fire = (event: ReleaseEvent): void => {
|
|
169
|
-
const r = reduceRelease(policy, state, event);
|
|
170
|
-
state = r.state;
|
|
171
|
-
if (r.shouldRelease) deps.releaseLock(held);
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
// Spawn. A throw here means NO child exists → spawn-start-failed releases (nothing to
|
|
175
|
-
// watch; keeping the lock would pin the gid forever). No child means no lock-leak risk.
|
|
176
|
-
let child: SpawnedChild;
|
|
177
|
-
try {
|
|
178
|
-
child = await deps.spawnChild(plan);
|
|
179
|
-
} catch (err) {
|
|
180
|
-
fire({ kind: "spawn-start-failed", error: errMsg(err) });
|
|
181
|
-
return { kind: "spawn-start-failed", released: true, error: errMsg(err) };
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const controller = new AbortController();
|
|
185
|
-
const { signal } = controller;
|
|
186
|
-
// Created ONCE and reused across the primary race and the kill-grace race; in the
|
|
187
|
-
// timeout branch both are still pending (timeout won), so re-racing them is safe.
|
|
188
|
-
//
|
|
189
|
-
// S1 (Fable 2차): the creation is in its OWN try because a watch dep can throw
|
|
190
|
-
// SYNCHRONOUSLY (a buggy dep that throws where it must RETURN a Promise — the same grade
|
|
191
|
-
// the send hand's case-13 backstop covers). The child already exists, so this is a
|
|
192
|
-
// post-spawn failure and the SAME rule applies: an observation-less release is forbidden.
|
|
193
|
-
// Best-effort kill, defuse any watcher that DID start (so its later rejection is not
|
|
194
|
-
// unhandled), tear down, and fail-closed to a retained diagnostic. We do NOT try to
|
|
195
|
-
// observe the exit — awaitChildExit may itself be the thrower; retained is the honest
|
|
196
|
-
// floor. (The async-rejection path is handled later by the race catch → backstop.)
|
|
197
|
-
let socketP!: Promise<Observation>;
|
|
198
|
-
let exitP!: Promise<Observation>;
|
|
199
|
-
try {
|
|
200
|
-
socketP = tagSocket(deps.awaitSocketAlive(plan.expectedSocketPath, signal));
|
|
201
|
-
exitP = tagExit(deps.awaitChildExit(child, signal));
|
|
202
|
-
} catch (err) {
|
|
203
|
-
try {
|
|
204
|
-
deps.killChild(child);
|
|
205
|
-
} catch {
|
|
206
|
-
// best-effort
|
|
207
|
-
}
|
|
208
|
-
controller.abort();
|
|
209
|
-
defuse(socketP);
|
|
210
|
-
defuse(exitP);
|
|
211
|
-
return retained(plan, child, "observe-failed", deps.killGraceMs, held.lockPath, errMsg(err));
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
try {
|
|
215
|
-
// Primary observation race. `timeout` participates but is NEVER a release event.
|
|
216
|
-
let first: Observation;
|
|
217
|
-
try {
|
|
218
|
-
const timeoutP = tagTimeout(deps.awaitTimeout(plan.observeTimeoutMs, signal));
|
|
219
|
-
first = await Promise.race([socketP, exitP, timeoutP]);
|
|
220
|
-
} catch (err) {
|
|
221
|
-
// A watch/timer dep rejected BEFORE any observation → post-spawn backstop.
|
|
222
|
-
return await backstop(plan, deps, child, held.lockPath, exitP, signal, fire, errMsg(err));
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
if (first.tag === "socket") {
|
|
226
|
-
fire({ kind: "socket-alive" });
|
|
227
|
-
return { kind: "socket-alive", released: true, pid: child.pid };
|
|
228
|
-
}
|
|
229
|
-
if (first.tag === "exit") {
|
|
230
|
-
fire({ kind: "child-exited", code: first.code });
|
|
231
|
-
return { kind: "child-exited", released: true, exitCode: first.code, pid: child.pid };
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// first.tag === "timeout": NOT a release. Escalate to a kill, then wait a BOUNDED
|
|
235
|
-
// grace for the kill to produce a real child-exited (or a racing socket-alive).
|
|
236
|
-
try {
|
|
237
|
-
deps.killChild(child);
|
|
238
|
-
} catch {
|
|
239
|
-
// kill itself threw — still try to observe within the grace; if nothing, retain.
|
|
240
|
-
}
|
|
241
|
-
let second: Observation;
|
|
242
|
-
try {
|
|
243
|
-
const graceP = tagTimeout(deps.awaitTimeout(deps.killGraceMs, signal));
|
|
244
|
-
second = await Promise.race([socketP, exitP, graceP]);
|
|
245
|
-
} catch (err) {
|
|
246
|
-
// A dep rejected during the grace → cannot confirm; retain the lock, surface it.
|
|
247
|
-
return retained(plan, child, "kill-unconfirmed", deps.killGraceMs, held.lockPath, errMsg(err));
|
|
248
|
-
}
|
|
249
|
-
if (second.tag === "socket") {
|
|
250
|
-
fire({ kind: "socket-alive" });
|
|
251
|
-
return { kind: "socket-alive", released: true, pid: child.pid };
|
|
252
|
-
}
|
|
253
|
-
if (second.tag === "exit") {
|
|
254
|
-
fire({ kind: "child-exited", code: second.code });
|
|
255
|
-
return { kind: "child-exited", released: true, exitCode: second.code, pid: child.pid };
|
|
256
|
-
}
|
|
257
|
-
// Grace elapsed with NO observation → the kill is unconfirmed. Do NOT release blind;
|
|
258
|
-
// retain the lock and surface the evidence (bounded return, never a hang).
|
|
259
|
-
return retained(plan, child, "kill-unconfirmed", deps.killGraceMs, held.lockPath, undefined);
|
|
260
|
-
} finally {
|
|
261
|
-
// Tear down the losing watchers (their timers / FS watches) on every exit.
|
|
262
|
-
controller.abort();
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Post-spawn backstop (GPT 5c-3a correction): a child exists but a watch/timer dep threw
|
|
268
|
-
* before any observation. An observation-less release is forbidden (it reopens the
|
|
269
|
-
* double-spawn window), so: best-effort kill → BOUNDED attempt to OBSERVE the exit →
|
|
270
|
-
* release iff observed, else `lock-retained` fail-closed. We race ONLY the exit (not the
|
|
271
|
-
* already-suspect socket watcher) plus a fresh grace timer.
|
|
272
|
-
*/
|
|
273
|
-
async function backstop(
|
|
274
|
-
plan: SpawnBgPlan,
|
|
275
|
-
deps: SpawnBgResumeDeps,
|
|
276
|
-
child: SpawnedChild,
|
|
277
|
-
lockPath: string,
|
|
278
|
-
exitP: Promise<Observation>,
|
|
279
|
-
signal: AbortSignal,
|
|
280
|
-
fire: (event: ReleaseEvent) => void,
|
|
281
|
-
originalError: string,
|
|
282
|
-
): Promise<SpawnBgResumeResult> {
|
|
283
|
-
try {
|
|
284
|
-
deps.killChild(child);
|
|
285
|
-
} catch {
|
|
286
|
-
// best-effort
|
|
287
|
-
}
|
|
288
|
-
try {
|
|
289
|
-
const graceP = tagTimeout(deps.awaitTimeout(deps.killGraceMs, signal));
|
|
290
|
-
const obs = await Promise.race([exitP, graceP]);
|
|
291
|
-
if (obs.tag === "exit") {
|
|
292
|
-
fire({ kind: "child-exited", code: obs.code });
|
|
293
|
-
return { kind: "child-exited", released: true, exitCode: obs.code, pid: child.pid };
|
|
294
|
-
}
|
|
295
|
-
} catch {
|
|
296
|
-
// exit watcher itself was the failing dep (or threw again) → fall through to retain.
|
|
297
|
-
}
|
|
298
|
-
return retained(plan, child, "observe-failed", deps.killGraceMs, lockPath, originalError);
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
function retained(
|
|
302
|
-
plan: SpawnBgPlan,
|
|
303
|
-
child: SpawnedChild,
|
|
304
|
-
reason: "kill-unconfirmed" | "observe-failed",
|
|
305
|
-
killGraceMs: number,
|
|
306
|
-
lockPath: string,
|
|
307
|
-
error: string | undefined,
|
|
308
|
-
): SpawnBgResumeResult {
|
|
309
|
-
return {
|
|
310
|
-
kind: "lock-retained",
|
|
311
|
-
released: false,
|
|
312
|
-
reason,
|
|
313
|
-
error,
|
|
314
|
-
diagnostic: {
|
|
315
|
-
targetGardenId: plan.targetGardenId,
|
|
316
|
-
pid: child.pid,
|
|
317
|
-
expectedSocketPath: plan.expectedSocketPath,
|
|
318
|
-
lockPath,
|
|
319
|
-
observeTimeoutMs: plan.observeTimeoutMs,
|
|
320
|
-
killGraceMs,
|
|
321
|
-
},
|
|
322
|
-
};
|
|
323
|
-
}
|
|
@@ -1,236 +0,0 @@
|
|
|
1
|
-
// Deterministic gate for the S2a ACP SDK dependency surface.
|
|
2
|
-
//
|
|
3
|
-
// Pins the ACP runtime deps to the current behavior-oracle versions and locks
|
|
4
|
-
// the peer-resolution that makes the Claude ACP adapter satisfiable:
|
|
5
|
-
//
|
|
6
|
-
// @agentclientprotocol/sdk 1.3.0 wire SDK (acp-bridge import source)
|
|
7
|
-
// @agentclientprotocol/claude-agent-acp 0.62.0 Claude adapter (spawn binary)
|
|
8
|
-
// @anthropic-ai/sdk 0.100.1 peer-resolution pin ONLY (see below)
|
|
9
|
-
//
|
|
10
|
-
// 2026-07-27 bump 0.61.0 → 0.62.0: the adapter's own `dist/` is byte-identical
|
|
11
|
-
// across the two releases; 0.62.0 moves only its declared deps
|
|
12
|
-
// (claude-agent-sdk 0.3.217 → 0.3.219; its devDep anthropic sdk, which does not
|
|
13
|
-
// reach us). So this bump is a dependency refresh that carries NO adapter-code
|
|
14
|
-
// change — and specifically NOT a fix for the bundled-MCP readiness race
|
|
15
|
-
// (ROADMAP 🔴): 0.62.0 adds no explicit readiness fence. Effective runtime
|
|
16
|
-
// behavior is NOT claimed identical: the transitive SDK moved, and MCP startup
|
|
17
|
-
// lives inside it, so timing may differ — what is established is only that no
|
|
18
|
-
// new fence was added.
|
|
19
|
-
//
|
|
20
|
-
// The anthropic SDK is NOT an API client / auth surface here. It is a direct
|
|
21
|
-
// dep solely to satisfy @anthropic-ai/claude-agent-sdk@0.3.219's peer floor
|
|
22
|
-
// (>=0.93.0 — re-measured at 0.3.219, unchanged from 0.3.217, so the 0.100.1
|
|
23
|
-
// pin stands rather than rising mechanically); drop it and the tree resolves a stale 0.91.1 so the peer goes
|
|
24
|
-
// unmet — a failure that would only surface at the first raw turn. The
|
|
25
|
-
// lockfile proves the same shape. Source-level import / API-client
|
|
26
|
-
// instantiation / credential use stays forbidden — asserted in layer (4).
|
|
27
|
-
// (GPT hard constraint 2, revised 2026-06-18: direct dep allowed ONLY as an
|
|
28
|
-
// exact peer-resolution pin; source-level use remains forbidden.)
|
|
29
|
-
//
|
|
30
|
-
// Layers:
|
|
31
|
-
// (1) package.json exact pins for all three deps;
|
|
32
|
-
// (2) pnpm-lock peer-resolution lock (adapter + claude-agent-sdk → 0.100.1);
|
|
33
|
-
// (2b) runtime peer-resolution probe (claude-agent-sdk context → 0.100.1);
|
|
34
|
-
// (3) @agentclientprotocol/sdk value-export surface (silent-rename gate);
|
|
35
|
-
// (4) no tracked source imports the anthropic SDK or builds an API client.
|
|
36
|
-
|
|
37
|
-
import { strict as assert } from "node:assert";
|
|
38
|
-
import { execFileSync } from "node:child_process";
|
|
39
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
40
|
-
import { createRequire } from "node:module";
|
|
41
|
-
import { dirname, resolve } from "node:path";
|
|
42
|
-
|
|
43
|
-
// Split literals so this gate's own regexes never self-match in layer (4).
|
|
44
|
-
const ANTHROPIC_SDK = `@anthropic-ai/${"sdk"}`;
|
|
45
|
-
const API_CLIENT_CLASS = `${"Anthropic"}`;
|
|
46
|
-
|
|
47
|
-
const repoRoot = resolve(import.meta.dirname, "..");
|
|
48
|
-
const read = (p: string): string => readFileSync(resolve(repoRoot, p), "utf8");
|
|
49
|
-
|
|
50
|
-
// ---------------------------------------------------------------------------
|
|
51
|
-
// (1) package.json — exact pins (no caret/range; S2a freezes the oracle set)
|
|
52
|
-
// ---------------------------------------------------------------------------
|
|
53
|
-
const pkg = JSON.parse(read("package.json")) as { dependencies?: Record<string, string> };
|
|
54
|
-
const deps = pkg.dependencies ?? {};
|
|
55
|
-
const PINS: Record<string, string> = {
|
|
56
|
-
"@agentclientprotocol/sdk": "1.3.0",
|
|
57
|
-
"@agentclientprotocol/claude-agent-acp": "0.62.0",
|
|
58
|
-
[ANTHROPIC_SDK]: "0.100.1",
|
|
59
|
-
};
|
|
60
|
-
for (const [name, ver] of Object.entries(PINS)) {
|
|
61
|
-
assert.equal(
|
|
62
|
-
deps[name],
|
|
63
|
-
ver,
|
|
64
|
-
`package.json dependencies["${name}"] must be exact "${ver}" (got "${deps[name]}") — S2a pins the current oracle versions`,
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// ---------------------------------------------------------------------------
|
|
69
|
-
// (2) pnpm-lock — peer-resolution lock
|
|
70
|
-
// The adapter and claude-agent-sdk MUST peer-resolve the anthropic SDK to
|
|
71
|
-
// 0.100.1; a 0.91.1 resolution means the >=0.93.0 peer is unmet (the bug
|
|
72
|
-
// this whole pin exists to prevent).
|
|
73
|
-
// ---------------------------------------------------------------------------
|
|
74
|
-
const lock = read("pnpm-lock.yaml");
|
|
75
|
-
assert.match(
|
|
76
|
-
lock,
|
|
77
|
-
/@agentclientprotocol\/claude-agent-acp@0\.62\.0\(@anthropic-ai\/sdk@0\.100\.1/,
|
|
78
|
-
"pnpm-lock: claude-agent-acp@0.62.0 must peer-resolve @anthropic-ai/sdk@0.100.1 (peer-pin), not the stale 0.91.1",
|
|
79
|
-
);
|
|
80
|
-
assert.match(
|
|
81
|
-
lock,
|
|
82
|
-
/@anthropic-ai\/claude-agent-sdk@0\.3\.219\(@anthropic-ai\/sdk@0\.100\.1/,
|
|
83
|
-
"pnpm-lock: claude-agent-sdk@0.3.219 must peer-resolve @anthropic-ai/sdk@0.100.1 — else its >=0.93.0 peer floor is unmet",
|
|
84
|
-
);
|
|
85
|
-
|
|
86
|
-
// ---------------------------------------------------------------------------
|
|
87
|
-
// (2b) runtime peer-resolution probe — the actual Node resolver, not lock text.
|
|
88
|
-
// Layer (2) freezes the publish/install floor; this probes that the
|
|
89
|
-
// claude-agent-sdk → @anthropic-ai/sdk peer edge really resolves to
|
|
90
|
-
// 0.100.1 in a live module graph. Two different failures, both needed.
|
|
91
|
-
// Note: a top-level/adapter-context resolve of the anthropic SDK may see
|
|
92
|
-
// 0.91.1 (pi's own transitive) — that is normal. The edge that must be
|
|
93
|
-
// 0.100.1 is the one *inside* claude-agent-sdk's context.
|
|
94
|
-
// ---------------------------------------------------------------------------
|
|
95
|
-
const pkgInfoFromEntry = (entryPath: string): { name: string; version: string; dir: string } => {
|
|
96
|
-
// A `<pkg>/package.json` subpath resolve can fail under "exports"; walk up
|
|
97
|
-
// from the resolved entry to the nearest package.json instead.
|
|
98
|
-
let dir = dirname(entryPath);
|
|
99
|
-
for (;;) {
|
|
100
|
-
try {
|
|
101
|
-
const pj = JSON.parse(readFileSync(resolve(dir, "package.json"), "utf8")) as {
|
|
102
|
-
name?: string;
|
|
103
|
-
version?: string;
|
|
104
|
-
};
|
|
105
|
-
if (pj.name && pj.version) return { name: pj.name, version: pj.version, dir };
|
|
106
|
-
} catch {
|
|
107
|
-
// no package.json here (or unreadable) — keep walking up.
|
|
108
|
-
}
|
|
109
|
-
const parent = dirname(dir);
|
|
110
|
-
if (parent === dir) throw new Error(`no package.json found walking up from ${entryPath}`);
|
|
111
|
-
dir = parent;
|
|
112
|
-
}
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
const rootRequire = createRequire(resolve(repoRoot, "package.json"));
|
|
116
|
-
const adapterPkgJson = rootRequire.resolve("@agentclientprotocol/claude-agent-acp/package.json");
|
|
117
|
-
const adapterRequire = createRequire(adapterPkgJson);
|
|
118
|
-
const casEntry = adapterRequire.resolve("@anthropic-ai/claude-agent-sdk");
|
|
119
|
-
const casInfo = pkgInfoFromEntry(casEntry);
|
|
120
|
-
assert.equal(
|
|
121
|
-
casInfo.version,
|
|
122
|
-
"0.3.219",
|
|
123
|
-
`@anthropic-ai/claude-agent-sdk must runtime-resolve to 0.3.219 from the adapter context (got ${casInfo.version})`,
|
|
124
|
-
);
|
|
125
|
-
const casRequire = createRequire(resolve(casInfo.dir, "package.json"));
|
|
126
|
-
const sdkEntry = casRequire.resolve(ANTHROPIC_SDK);
|
|
127
|
-
const sdkInfo = pkgInfoFromEntry(sdkEntry);
|
|
128
|
-
assert.equal(
|
|
129
|
-
sdkInfo.version,
|
|
130
|
-
"0.100.1",
|
|
131
|
-
`${ANTHROPIC_SDK} must peer-resolve to 0.100.1 from the claude-agent-sdk context (got ${sdkInfo.version}) — a 0.91.1 here means the >=0.93.0 peer is unmet and the raw turn would break`,
|
|
132
|
-
);
|
|
133
|
-
|
|
134
|
-
// ---------------------------------------------------------------------------
|
|
135
|
-
// (2c) adapter-context wire-SDK + MCP-SDK runtime resolve — the ACP dep bump's
|
|
136
|
-
// real fault surface. Lock text (layer 2) freezes the publish floor; these probe
|
|
137
|
-
// the LIVE module graph the adapter actually traverses: the adapter must SEE
|
|
138
|
-
// the same wire SDK the backend imports at root (1.3.0), and claude-agent-sdk
|
|
139
|
-
// must SEE its declared MCP peer (1.29.x). Cheap edges, both newly relevant
|
|
140
|
-
// after the 0.54→0.61 / 1.1→1.3 bump.
|
|
141
|
-
// ---------------------------------------------------------------------------
|
|
142
|
-
const wireEntry = adapterRequire.resolve("@agentclientprotocol/sdk");
|
|
143
|
-
const wireInfo = pkgInfoFromEntry(wireEntry);
|
|
144
|
-
assert.equal(
|
|
145
|
-
wireInfo.version,
|
|
146
|
-
"1.3.0",
|
|
147
|
-
`@agentclientprotocol/sdk must runtime-resolve to 1.3.0 from the adapter context (got ${wireInfo.version}) — the adapter and the backend must share one wire SDK`,
|
|
148
|
-
);
|
|
149
|
-
// @modelcontextprotocol/sdk gates its bare specifier behind "exports", so a
|
|
150
|
-
// require.resolve of the package ROOT throws (no resolvable entry) — resolve a
|
|
151
|
-
// real SUBPATH instead and walk up to its package.json.
|
|
152
|
-
//
|
|
153
|
-
// This used to read the repo's own hoisted copy and justify it with "pnpm
|
|
154
|
-
// hoists ONE mcp instance". That is true of today's lockfile, which is exactly
|
|
155
|
-
// why it asserted nothing: the read never traversed the edge it claimed to
|
|
156
|
-
// verify, so a nested @modelcontextprotocol/sdk under claude-agent-sdk would
|
|
157
|
-
// leave this gate GREEN while the adapter loaded a peer no gate had seen. A
|
|
158
|
-
// probe whose subject is "whatever the root happens to hoist" is a coincidence,
|
|
159
|
-
// not a check. Resolve FROM the claude-agent-sdk context so the assertion binds
|
|
160
|
-
// the real adapter → claude-agent-sdk → MCP edge, the same way the anthropic
|
|
161
|
-
// SDK peer above is bound.
|
|
162
|
-
const mcpEntry = casRequire.resolve("@modelcontextprotocol/sdk/server/index.js");
|
|
163
|
-
const mcpInfo = pkgInfoFromEntry(mcpEntry);
|
|
164
|
-
assert.ok(
|
|
165
|
-
mcpInfo.version.startsWith("1.29."),
|
|
166
|
-
`@modelcontextprotocol/sdk must runtime-resolve to 1.29.x from the claude-agent-sdk context (got ${mcpInfo.version}) — claude-agent-sdk 0.3.219 declares a ^1.29.0 peer`,
|
|
167
|
-
);
|
|
168
|
-
|
|
169
|
-
// ---------------------------------------------------------------------------
|
|
170
|
-
// (3) @agentclientprotocol/sdk value-export surface (silent-rename gate)
|
|
171
|
-
// The ACP bridge imports these from the wire SDK; a silent upstream
|
|
172
|
-
// rename would not fail typecheck (type-only erasure) but would break the
|
|
173
|
-
// raw turn. Assert the *value* exports exist at runtime.
|
|
174
|
-
// ---------------------------------------------------------------------------
|
|
175
|
-
// These are the value imports the real ACP code uses: the connectAcpClient
|
|
176
|
-
// adapter (acp-client.ts) drives `client` + the `AGENT_METHODS`/`CLIENT_METHODS`
|
|
177
|
-
// method tables; the backend + raw-turn smoke value-import `ndJsonStream` (the
|
|
178
|
-
// stdio transport) and `PROTOCOL_VERSION`. The deprecated `ClientSideConnection`
|
|
179
|
-
// is no longer used, so it is no longer gated. (Per GPT: gate only the value
|
|
180
|
-
// imports the real code uses — type-only imports are erased.)
|
|
181
|
-
const acpSdk = (await import("@agentclientprotocol/sdk")) as Record<string, unknown>;
|
|
182
|
-
for (const sym of ["client", "ndJsonStream", "PROTOCOL_VERSION", "AGENT_METHODS", "CLIENT_METHODS"]) {
|
|
183
|
-
assert.ok(
|
|
184
|
-
sym in acpSdk,
|
|
185
|
-
`@agentclientprotocol/sdk lost value export "${sym}" — silent upstream rename; the raw ACP turn would break`,
|
|
186
|
-
);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// ---------------------------------------------------------------------------
|
|
190
|
-
// (4) no source-level anthropic SDK import / API client
|
|
191
|
-
// peer-pin only. The credential boundary (AGENTS §Operating boundaries)
|
|
192
|
-
// forbids the bridge from importing the SDK or instantiating an API client.
|
|
193
|
-
// Line-based: only real `import ... from` / `require(...)` statements count,
|
|
194
|
-
// so this gate's own split-literal regexes above are never offenders.
|
|
195
|
-
// ---------------------------------------------------------------------------
|
|
196
|
-
const tracked = execFileSync("git", ["ls-files", "*.ts", "*.js", "*.mjs", "*.cjs"], {
|
|
197
|
-
cwd: repoRoot,
|
|
198
|
-
encoding: "utf8",
|
|
199
|
-
})
|
|
200
|
-
.split("\n")
|
|
201
|
-
.filter(Boolean)
|
|
202
|
-
// `git ls-files` still names an UNSTAGED deletion (same contract as
|
|
203
|
-
// check-install-surface / the run.sh floor sweeps): a release-surface
|
|
204
|
-
// migration deletes tracked files before the commit workflow stages them;
|
|
205
|
-
// absence cannot import the SDK and must not crash this read-only sweep.
|
|
206
|
-
.filter((f) => existsSync(resolve(repoRoot, f)));
|
|
207
|
-
|
|
208
|
-
// Specifier-shaped: any module binding to the anthropic SDK, in any of the
|
|
209
|
-
// forms a source file could reach it — static `from`, `export ... from`,
|
|
210
|
-
// side-effect `import "X"`, dynamic `import("X")`, and `require("X")`.
|
|
211
|
-
const spec = `["']${ANTHROPIC_SDK.replace("/", "\\/")}["']`;
|
|
212
|
-
const specifierPatterns: ReadonlyArray<{ re: RegExp; kind: string }> = [
|
|
213
|
-
{ re: new RegExp(String.raw`\bfrom\s+${spec}`), kind: `import/export from ${ANTHROPIC_SDK}` },
|
|
214
|
-
{ re: new RegExp(String.raw`^\s*import\s+${spec}`), kind: `side-effect import ${ANTHROPIC_SDK}` },
|
|
215
|
-
{ re: new RegExp(String.raw`\bimport\(\s*${spec}\s*\)`), kind: `dynamic import(${ANTHROPIC_SDK})` },
|
|
216
|
-
{ re: new RegExp(String.raw`\brequire\(\s*${spec}\s*\)`), kind: `require(${ANTHROPIC_SDK})` },
|
|
217
|
-
];
|
|
218
|
-
const clientRe = new RegExp(String.raw`\bnew\s+${API_CLIENT_CLASS}\s*\(`);
|
|
219
|
-
|
|
220
|
-
const offenders: string[] = [];
|
|
221
|
-
for (const f of tracked) {
|
|
222
|
-
const src = read(f);
|
|
223
|
-
for (const line of src.split("\n")) {
|
|
224
|
-
for (const { re, kind } of specifierPatterns) {
|
|
225
|
-
if (re.test(line)) offenders.push(`${f}: ${kind}`);
|
|
226
|
-
}
|
|
227
|
-
if (clientRe.test(line)) offenders.push(`${f}: new ${API_CLIENT_CLASS}() API client`);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
assert.equal(
|
|
231
|
-
offenders.length,
|
|
232
|
-
0,
|
|
233
|
-
`${ANTHROPIC_SDK} is a peer-resolution pin ONLY — no source import / API client allowed:\n ${offenders.join("\n ")}`,
|
|
234
|
-
);
|
|
235
|
-
|
|
236
|
-
console.log("[check-acp-sdk-surface] ACP dep pins + peer-resolution lock + wire-SDK export surface + no-client-use ok");
|