@junghanacs/entwurf 0.12.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 +240 -0
- package/BASELINE.md +227 -0
- package/CHANGELOG.md +1210 -0
- package/CONTRIBUTING.md +63 -0
- package/DELIVERY.md +209 -0
- package/LICENSE +21 -0
- package/README.md +504 -0
- package/VERIFY.md +260 -0
- package/demo/README.md +188 -0
- package/demo/demo-baseline.sh +156 -0
- package/demo/demo.sh +183 -0
- package/docs/assets/entwurf-demo.gif +0 -0
- package/docs/assets/entwurf-doomemacs.gif +0 -0
- package/docs/assets/entwurf-entwurf.gif +0 -0
- package/docs/assets/entwurf-hero.jpg +0 -0
- package/docs/setup-clean-host.md +305 -0
- package/mcp/entwurf-bridge/src/index.ts +513 -0
- package/mcp/entwurf-bridge/start.sh +25 -0
- package/mcp/entwurf-bridge/test.sh +54 -0
- package/mcp/tsconfig.json +29 -0
- package/package.json +130 -0
- package/pi/entwurf-capabilities.json +9 -0
- package/pi/entwurf-targets.json +20 -0
- package/pi/meta-bridge/.claude-plugin/marketplace.json +12 -0
- package/pi/meta-bridge/entwurf-meta-receive/.claude-plugin/plugin.json +5 -0
- package/pi/meta-bridge/entwurf-meta-receive/hooks/hooks.json +50 -0
- package/pi/meta-bridge/entwurf-meta-receive/scripts/doorbell.sh +54 -0
- package/pi/settings.reference.json +43 -0
- package/pi/skill-plugin-example/.claude-plugin/plugin.json +4 -0
- package/pi/skill-plugin-example/skills/hello/SKILL.md +18 -0
- package/pi-extensions/acp-provider.ts +63 -0
- package/pi-extensions/entwurf-control.ts +1692 -0
- package/pi-extensions/lib/acp/acp-client.ts +90 -0
- package/pi-extensions/lib/acp/augment.ts +238 -0
- package/pi-extensions/lib/acp/backend-adapter.ts +319 -0
- package/pi-extensions/lib/acp/backend.ts +873 -0
- package/pi-extensions/lib/acp/config.ts +552 -0
- package/pi-extensions/lib/acp/context.ts +177 -0
- package/pi-extensions/lib/acp/engraving.ts +123 -0
- package/pi-extensions/lib/acp/event-mapper.ts +339 -0
- package/pi-extensions/lib/acp/models.ts +102 -0
- package/pi-extensions/lib/acp/overlay.ts +220 -0
- package/pi-extensions/lib/acp/prompts/engraving.md +1 -0
- package/pi-extensions/lib/acp/session-store.ts +418 -0
- package/pi-extensions/lib/acp/tool-surface.ts +184 -0
- package/pi-extensions/lib/entwurf-control-rpc.ts +208 -0
- package/pi-extensions/lib/entwurf-core.ts +2033 -0
- package/pi-extensions/lib/entwurf-deliverability.ts +123 -0
- package/pi-extensions/lib/entwurf-fact-provider.ts +158 -0
- package/pi-extensions/lib/entwurf-facts.ts +251 -0
- package/pi-extensions/lib/entwurf-mailbox-guard.ts +100 -0
- package/pi-extensions/lib/entwurf-peers-render.ts +154 -0
- package/pi-extensions/lib/entwurf-preflight.ts +247 -0
- package/pi-extensions/lib/entwurf-resume-args.ts +88 -0
- package/pi-extensions/lib/entwurf-self-address.ts +117 -0
- package/pi-extensions/lib/entwurf-v2-contract.ts +427 -0
- package/pi-extensions/lib/entwurf-v2-decider.ts +441 -0
- package/pi-extensions/lib/entwurf-v2-lock.ts +412 -0
- package/pi-extensions/lib/entwurf-v2-mailbox.ts +87 -0
- package/pi-extensions/lib/entwurf-v2-production.ts +326 -0
- package/pi-extensions/lib/entwurf-v2-release.ts +149 -0
- package/pi-extensions/lib/entwurf-v2-resume-marker.ts +34 -0
- package/pi-extensions/lib/entwurf-v2-runner.ts +196 -0
- package/pi-extensions/lib/entwurf-v2-send-fallback.ts +166 -0
- package/pi-extensions/lib/entwurf-v2-send.ts +271 -0
- package/pi-extensions/lib/entwurf-v2-spawn-production.ts +337 -0
- package/pi-extensions/lib/entwurf-v2-spawn.ts +323 -0
- package/pi-extensions/lib/entwurf-v2-surface.ts +205 -0
- package/pi-extensions/lib/meta-mailbox-body.ts +80 -0
- package/pi-extensions/lib/meta-session.ts +1951 -0
- package/pi-extensions/lib/project-trust-handler.ts +154 -0
- package/pi-extensions/lib/session-id.js +57 -0
- package/pi-extensions/lib/socket-discovery.ts +346 -0
- package/pi-extensions/lib/socket-probe.ts +92 -0
- package/pi-extensions/meta-bridge-hook.ts +250 -0
- package/pi-extensions/model-lock.ts +235 -0
- package/prompts/engraving.md +27 -0
- package/protocol.js +31 -0
- package/run.sh +2832 -0
- package/scripts/check-acp-backend-preflight.ts +130 -0
- package/scripts/check-acp-carrier-augment.ts +297 -0
- package/scripts/check-acp-config.ts +322 -0
- package/scripts/check-acp-event-mapper.ts +253 -0
- package/scripts/check-acp-overlay.ts +154 -0
- package/scripts/check-acp-prompt-builder.ts +191 -0
- package/scripts/check-acp-provider-surface.ts +159 -0
- package/scripts/check-acp-sdk-surface.ts +211 -0
- package/scripts/check-acp-session-reuse.ts +756 -0
- package/scripts/check-acp-session-store.ts +387 -0
- package/scripts/check-acp-tool-surface.ts +159 -0
- package/scripts/check-entwurf-bridge-boot.ts +199 -0
- package/scripts/check-entwurf-capabilities.ts +123 -0
- package/scripts/check-entwurf-control-rpc.ts +187 -0
- package/scripts/check-entwurf-deliverability.ts +158 -0
- package/scripts/check-entwurf-fact-provider.ts +316 -0
- package/scripts/check-entwurf-facts.ts +358 -0
- package/scripts/check-entwurf-mailbox-guard.ts +264 -0
- package/scripts/check-entwurf-peers-surface.ts +235 -0
- package/scripts/check-entwurf-resume-args.ts +149 -0
- package/scripts/check-entwurf-self-address.ts +209 -0
- package/scripts/check-entwurf-session-identity.ts +703 -0
- package/scripts/check-entwurf-v2-contract.ts +536 -0
- package/scripts/check-entwurf-v2-decider.ts +795 -0
- package/scripts/check-entwurf-v2-lock.ts +368 -0
- package/scripts/check-entwurf-v2-mailbox.ts +228 -0
- package/scripts/check-entwurf-v2-matrix.ts +437 -0
- package/scripts/check-entwurf-v2-production.ts +406 -0
- package/scripts/check-entwurf-v2-release.ts +245 -0
- package/scripts/check-entwurf-v2-runner.ts +452 -0
- package/scripts/check-entwurf-v2-send-fallback.ts +371 -0
- package/scripts/check-entwurf-v2-send.ts +454 -0
- package/scripts/check-entwurf-v2-spawn-production.ts +452 -0
- package/scripts/check-entwurf-v2-spawn.ts +398 -0
- package/scripts/check-entwurf-v2-surface.ts +352 -0
- package/scripts/check-keyset-overlap.py +120 -0
- package/scripts/check-mailbox-receipt-state.ts +170 -0
- package/scripts/check-meta-capability-source.ts +112 -0
- package/scripts/check-meta-dual-consumers.ts +154 -0
- package/scripts/check-meta-dual-read.ts +158 -0
- package/scripts/check-meta-listing.ts +138 -0
- package/scripts/check-meta-mailbox-state-write.ts +135 -0
- package/scripts/check-meta-migration.ts +212 -0
- package/scripts/check-meta-receiver-marker.ts +185 -0
- package/scripts/check-meta-record-v2.ts +191 -0
- package/scripts/check-meta-session.ts +673 -0
- package/scripts/check-model-lock.ts +408 -0
- package/scripts/check-package-source-routing.ts +253 -0
- package/scripts/check-pi-preflight.ts +304 -0
- package/scripts/check-project-trust-handler.ts +265 -0
- package/scripts/check-shell-quote.ts +121 -0
- package/scripts/check-socket-discovery.ts +428 -0
- package/scripts/check-socket-probe.ts +106 -0
- package/scripts/fixtures/probe-mcp-server.ts +33 -0
- package/scripts/gnew-rpc-drive.ts +211 -0
- package/scripts/lib/acp-child-cleanup.ts +116 -0
- package/scripts/meta-bridge-doctor.sh +315 -0
- package/scripts/meta-bridge-hook-log.sh +26 -0
- package/scripts/meta-bridge-install.sh +135 -0
- package/scripts/meta-bridge-prune.ts +199 -0
- package/scripts/meta-bridge-state.py +549 -0
- package/scripts/meta-bridge-statusline.sh +192 -0
- package/scripts/meta-bridge-store-doctor.ts +64 -0
- package/scripts/meta-bridge-uninstall.sh +39 -0
- package/scripts/new-session-id.ts +25 -0
- package/scripts/postinstall-chmod.cjs +58 -0
- package/scripts/raw-async-delivery/README.md +258 -0
- package/scripts/raw-async-delivery/cc-enqueue-addressed.sh +35 -0
- package/scripts/raw-async-delivery/cc-mailbox-rewake.sh +38 -0
- package/scripts/raw-async-delivery/cc-watch-filechanged.sh +19 -0
- package/scripts/raw-async-delivery/cc-watch-sessionstart.sh +15 -0
- package/scripts/raw-async-delivery/codex-local-appserver.sh +39 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/.claude-plugin/plugin.json +5 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/hooks/hooks.json +28 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/scripts/watch-filechanged.sh +28 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/scripts/watch-sessionstart.sh +27 -0
- package/scripts/raw-async-delivery/raw-agy-send.sh +29 -0
- package/scripts/raw-async-delivery/raw-claude-enqueue.sh +30 -0
- package/scripts/raw-async-delivery/raw-codex-ws-turn-start.py +164 -0
- package/scripts/raw-async-delivery/repro-addressed-routing.sh +96 -0
- package/scripts/raw-async-delivery/repro-plugin-idle-wake.sh +104 -0
- package/scripts/resolve-acp-bridge.ts +25 -0
- package/scripts/smoke-acp-bundled-mcp-live.ts +292 -0
- package/scripts/smoke-acp-carrier-augment-live.ts +192 -0
- package/scripts/smoke-acp-mcp-live.ts +129 -0
- package/scripts/smoke-acp-memory-containment-live.ts +389 -0
- package/scripts/smoke-acp-overlay-live.ts +314 -0
- package/scripts/smoke-acp-provider-live.ts +162 -0
- package/scripts/smoke-acp-raw-turn-live.ts +261 -0
- package/scripts/smoke-acp-session-reuse-live.ts +172 -0
- package/scripts/smoke-acp-skill-live.ts +144 -0
- package/scripts/smoke-acp-socket-citizen-live.ts +168 -0
- package/scripts/smoke-claude-native-resume-live.sh +198 -0
- package/scripts/smoke-entwurf-v2-matrix-live.ts +398 -0
- package/scripts/smoke-entwurf-v2-spawn-live.ts +175 -0
- package/scripts/smoke-entwurf-v2-spawn-resume-live.ts +428 -0
- package/scripts/smoke-meta-async-drift.sh +171 -0
- package/scripts/smoke-meta-honesty.sh +147 -0
- package/scripts/smoke-meta-install-state.sh +403 -0
- package/scripts/smoke-meta-keyset-guard.sh +111 -0
- package/scripts/smoke-meta-prune.sh +174 -0
- package/scripts/smoke-resident-garden-guard.sh +433 -0
- package/scripts/smoke-session-id-name.ts +187 -0
- package/scripts/tsconfig.json +34 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* resolve-acp-bridge — print the bridge extension path entwurf-core's
|
|
3
|
+
* getRegistryRouting() would inject for a provider=entwurf spawn under the
|
|
4
|
+
* CURRENT environment (PI_CODING_AGENT_DIR + settings.json package sources).
|
|
5
|
+
*
|
|
6
|
+
* Used by smoke-installed-entwurf-acp so the live smoke drives a real pi child
|
|
7
|
+
* with the resolver's OWN output: if package-source routing regresses (wrong
|
|
8
|
+
* path, or unresolved), this either prints the wrong -e or throws
|
|
9
|
+
* EntwurfRoutingError (non-zero exit), and the smoke fails instead of silently
|
|
10
|
+
* passing. Prints the `-e` value to stdout on success; throws on unresolved.
|
|
11
|
+
*
|
|
12
|
+
* argv[2]: "remote" selects the SSH remote path; anything else is local.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { ResolvedTarget } from "../pi-extensions/lib/entwurf-core.ts";
|
|
16
|
+
import { getRegistryRouting } from "../pi-extensions/lib/entwurf-core.ts";
|
|
17
|
+
|
|
18
|
+
const isRemote = process.argv[2] === "remote";
|
|
19
|
+
const target: ResolvedTarget = { provider: "entwurf", model: "claude-sonnet-4-6", explicitOnly: false };
|
|
20
|
+
|
|
21
|
+
// Throws EntwurfRoutingError (non-zero exit) when the bridge cannot be resolved —
|
|
22
|
+
// exactly the fail-fast the smoke wants to observe as a failure, not a pass.
|
|
23
|
+
const routing = getRegistryRouting(target, isRemote);
|
|
24
|
+
const i = routing.args.indexOf("-e");
|
|
25
|
+
process.stdout.write(i >= 0 ? routing.args[i + 1] : "");
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
// S2g LIVE 3 (axis 3) — the BUNDLED entwurf-bridge reaches the live ACP session
|
|
2
|
+
// via the 0.11.0 resident/RPC circuit. LIVE-gated, OUT of `pnpm check`.
|
|
3
|
+
//
|
|
4
|
+
// LIVE=1 ./run.sh smoke-acp-bundled-mcp-live
|
|
5
|
+
//
|
|
6
|
+
// WHY a separate axis. smoke-acp-mcp-live proves the generic passthrough with a
|
|
7
|
+
// TINY isolated probe MCP (so a failure isolates to "did mcpServers reach
|
|
8
|
+
// newSession" without identity/env coupling). This smoke proves the LAST mile that
|
|
9
|
+
// probe deliberately skips: the operator's REAL bundled `entwurf-bridge` — the one
|
|
10
|
+
// that needs PI_SESSION_ID/PI_AGENT_ID envelope injection (enrichMcpServersWithEnvelope)
|
|
11
|
+
// to answer entwurf_self — reaches the live ACP session and can be CALLED by the model.
|
|
12
|
+
// GLG verified this by hand (entwurf_self → sessionId/agentId/socketState alive); this
|
|
13
|
+
// codifies that hand-check into a repeatable gate.
|
|
14
|
+
//
|
|
15
|
+
// WHY resident/RPC, not `pi -p` one-shot. A `pi -p` one-shot + bundled tool-call
|
|
16
|
+
// hangs on closeSession/teardown — that hang is diagnostic backlog, NOT this smoke's
|
|
17
|
+
// subject, and NOT the 0.11.0 release circuit (whose bundled-MCP evidence was always
|
|
18
|
+
// resident/RPC). So this smoke RESTORES the 0.11.0 circuit: it drives the bundled
|
|
19
|
+
// bridge through a long-lived `pi --entwurf-control --mode rpc` resident, the same
|
|
20
|
+
// stdin-RPC / stdout-event-stream driver as scripts/gnew-rpc-drive.ts.
|
|
21
|
+
//
|
|
22
|
+
// METHOD (gnew-rpc-drive shape): launch a real resident on an ACP model, send one
|
|
23
|
+
// `{type:"prompt"}` over stdin asking the model to call mcp__entwurf-bridge__entwurf_self,
|
|
24
|
+
// and capture — DIRECTLY from the stdout RPC event stream — the identity envelope
|
|
25
|
+
// (the resident's own freshly-minted gid, agentId entwurf/<model>, socketState
|
|
26
|
+
// alive) plus `agent_end`. The gid is never told to the model: it lives only as the
|
|
27
|
+
// resident's PI_SESSION_ID, injected into the bridge env, so the model can surface it
|
|
28
|
+
// ONLY by actually calling the tool. JSONL is an L3 backstop, never the primary proof.
|
|
29
|
+
//
|
|
30
|
+
// The bundled bridge is supplied by the operator's REAL
|
|
31
|
+
// entwurfProvider.mcpServers.entwurf-bridge (global ~/.pi/agent/settings.json) —
|
|
32
|
+
// this is the operator circuit, not a scratch-isolated probe. If the operator has not
|
|
33
|
+
// wired it, the smoke fails loud (the circuit is not installed).
|
|
34
|
+
//
|
|
35
|
+
// LIVE-only — kept OUT of `pnpm check`; honest skip when LIVE!=1 (skip = CI safety,
|
|
36
|
+
// NOT an acceptance PASS). Model override: ENTWURF_ACP_PROVIDER_MODEL (default sonnet).
|
|
37
|
+
|
|
38
|
+
import { type ChildProcess, spawn } from "node:child_process";
|
|
39
|
+
import { existsSync } from "node:fs";
|
|
40
|
+
import * as os from "node:os";
|
|
41
|
+
import * as path from "node:path";
|
|
42
|
+
import * as readline from "node:readline";
|
|
43
|
+
import { fileURLToPath } from "node:url";
|
|
44
|
+
import { fetchControlSocketRuntimeInfo, formatRuntimeModel } from "../pi-extensions/lib/entwurf-control-rpc.ts";
|
|
45
|
+
import { generateSessionId } from "../pi-extensions/lib/entwurf-core.ts";
|
|
46
|
+
import { terminateChild } from "./lib/acp-child-cleanup.ts";
|
|
47
|
+
|
|
48
|
+
const ACP_PROVIDER = "entwurf";
|
|
49
|
+
const ACP_MODEL = process.env.ENTWURF_ACP_PROVIDER_MODEL?.trim() || "claude-sonnet-4-6";
|
|
50
|
+
|
|
51
|
+
const REAL_CONTROL_DIR = path.join(os.homedir(), ".pi", "entwurf-control");
|
|
52
|
+
const SOCKET_SUFFIX = ".sock";
|
|
53
|
+
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
54
|
+
// Load ONLY this checkout's extensions so the resident registers THIS acp-provider.ts.
|
|
55
|
+
const REPO_EXTENSION_ARGS = ["--no-extensions", "-e", REPO_ROOT] as const;
|
|
56
|
+
|
|
57
|
+
const BOOT_TIMEOUT_MS = 30_000;
|
|
58
|
+
const TURN_TIMEOUT_MS = Number(process.env.ENTWURF_ACP_PROVIDER_TIMEOUT_MS) || 240_000;
|
|
59
|
+
const POLL_MS = 100;
|
|
60
|
+
|
|
61
|
+
const STUB_PATTERN = /AcpBackendNotImplementedError|not implemented in S0/i;
|
|
62
|
+
|
|
63
|
+
let passed = 0;
|
|
64
|
+
function ok(label: string, cond: boolean): void {
|
|
65
|
+
if (!cond) throw new Error(`SMOKE FAIL: ${label}`);
|
|
66
|
+
console.log(` ok ${label}`);
|
|
67
|
+
passed++;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function sleep(ms: number): Promise<void> {
|
|
71
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function waitForSocket(sockPath: string, timeoutMs: number): Promise<boolean> {
|
|
75
|
+
const deadline = Date.now() + timeoutMs;
|
|
76
|
+
while (Date.now() < deadline) {
|
|
77
|
+
if (existsSync(sockPath)) return true;
|
|
78
|
+
await sleep(POLL_MS);
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function waitForGone(sockPath: string, timeoutMs: number): Promise<boolean> {
|
|
84
|
+
const deadline = Date.now() + timeoutMs;
|
|
85
|
+
while (Date.now() < deadline) {
|
|
86
|
+
if (!existsSync(sockPath)) return true;
|
|
87
|
+
await sleep(POLL_MS);
|
|
88
|
+
}
|
|
89
|
+
return !existsSync(sockPath);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
interface TurnCapture {
|
|
93
|
+
agentStartSeen: boolean;
|
|
94
|
+
agentEndSeen: boolean;
|
|
95
|
+
extensionErrors: Array<{ path: unknown; event: unknown; error: unknown }>;
|
|
96
|
+
promptAccepted: boolean;
|
|
97
|
+
// Every line the resident emitted AFTER the prompt was sent — the envelope (a tool
|
|
98
|
+
// result embedded as an escaped string) and the assistant's reply both land here.
|
|
99
|
+
stream: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Drive exactly one model turn over the resident's stdin RPC and capture the stdout
|
|
103
|
+
// event stream until `agent_end` (or a hard turn timeout). Mirrors gnew-rpc-drive.ts.
|
|
104
|
+
function driveSelfTurn(child: ChildProcess, prompt: string): Promise<TurnCapture> {
|
|
105
|
+
return new Promise((resolve) => {
|
|
106
|
+
const cap: TurnCapture = {
|
|
107
|
+
agentStartSeen: false,
|
|
108
|
+
agentEndSeen: false,
|
|
109
|
+
extensionErrors: [],
|
|
110
|
+
promptAccepted: false,
|
|
111
|
+
stream: "",
|
|
112
|
+
};
|
|
113
|
+
let settled = false;
|
|
114
|
+
const finish = (): void => {
|
|
115
|
+
if (settled) return;
|
|
116
|
+
settled = true;
|
|
117
|
+
clearTimeout(timer);
|
|
118
|
+
rl.close();
|
|
119
|
+
resolve(cap);
|
|
120
|
+
};
|
|
121
|
+
const timer = setTimeout(finish, TURN_TIMEOUT_MS);
|
|
122
|
+
// If the resident dies before agent_end, settle now instead of burning the full
|
|
123
|
+
// turn timeout — a dead child can never emit agent_end (the assertions then fail loud).
|
|
124
|
+
child.once("exit", finish);
|
|
125
|
+
|
|
126
|
+
const rl = readline.createInterface({ input: child.stdout! });
|
|
127
|
+
rl.on("line", (line: string) => {
|
|
128
|
+
const trimmed = line.trim();
|
|
129
|
+
if (!trimmed) return;
|
|
130
|
+
cap.stream += `${trimmed}\n`;
|
|
131
|
+
let evt: Record<string, unknown>;
|
|
132
|
+
try {
|
|
133
|
+
evt = JSON.parse(trimmed) as Record<string, unknown>;
|
|
134
|
+
} catch {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (evt.type === "agent_start") cap.agentStartSeen = true;
|
|
138
|
+
if (evt.type === "extension_error") {
|
|
139
|
+
cap.extensionErrors.push({ path: evt.extensionPath, event: evt.event, error: evt.error });
|
|
140
|
+
}
|
|
141
|
+
if (evt.type === "response" && evt.command === "prompt") cap.promptAccepted = evt.success === true;
|
|
142
|
+
if (evt.type === "agent_end") {
|
|
143
|
+
cap.agentEndSeen = true;
|
|
144
|
+
finish();
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
child.stdin?.write(`${JSON.stringify({ type: "prompt", message: prompt, id: "self" })}\n`);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function main(): Promise<void> {
|
|
153
|
+
if (process.env.LIVE !== "1") {
|
|
154
|
+
console.log(
|
|
155
|
+
"[smoke-acp-bundled-mcp-live] skipped — set LIVE=1 to run (spawns a real pi --entwurf-control resident + drives one model turn).",
|
|
156
|
+
);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const gid = generateSessionId();
|
|
161
|
+
const sockPath = path.join(REAL_CONTROL_DIR, `${gid}${SOCKET_SUFFIX}`);
|
|
162
|
+
const tmp = os.tmpdir();
|
|
163
|
+
|
|
164
|
+
// A fresh gid must not collide with a pre-existing socket (else teardown deletes
|
|
165
|
+
// someone else's live socket). Fail loud rather than risk it.
|
|
166
|
+
ok("fresh gid has no pre-existing control socket", !existsSync(sockPath));
|
|
167
|
+
|
|
168
|
+
console.error(`[smoke-acp-bundled-mcp-live] repo: ${REPO_ROOT}`);
|
|
169
|
+
console.error(
|
|
170
|
+
`[smoke-acp-bundled-mcp-live] gid: ${gid} (never told to the model — only the bridge env carries it)`,
|
|
171
|
+
);
|
|
172
|
+
console.error(`[smoke-acp-bundled-mcp-live] model: ${ACP_PROVIDER}/${ACP_MODEL}`);
|
|
173
|
+
|
|
174
|
+
let stderrTail = "";
|
|
175
|
+
let resident: ChildProcess | null = null;
|
|
176
|
+
let cap: TurnCapture | null = null;
|
|
177
|
+
try {
|
|
178
|
+
resident = spawn(
|
|
179
|
+
"pi",
|
|
180
|
+
[
|
|
181
|
+
...REPO_EXTENSION_ARGS,
|
|
182
|
+
"--session-id",
|
|
183
|
+
gid,
|
|
184
|
+
"--entwurf-control",
|
|
185
|
+
"--provider",
|
|
186
|
+
ACP_PROVIDER,
|
|
187
|
+
"--model",
|
|
188
|
+
ACP_MODEL,
|
|
189
|
+
"--mode",
|
|
190
|
+
"rpc",
|
|
191
|
+
],
|
|
192
|
+
{ cwd: tmp, stdio: ["pipe", "pipe", "pipe"], detached: false },
|
|
193
|
+
);
|
|
194
|
+
resident.stderr?.on("data", (b: Buffer) => {
|
|
195
|
+
stderrTail = (stderrTail + b.toString()).slice(-4000);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// The resident stands its control socket up (turn-free citizenship — S1).
|
|
199
|
+
const up = await waitForSocket(sockPath, BOOT_TIMEOUT_MS);
|
|
200
|
+
ok(`ACP-model resident stood up a control socket (${ACP_PROVIDER}/${ACP_MODEL})`, up);
|
|
201
|
+
|
|
202
|
+
// Citizen fact: get_info answers and reports the un-reverted ACP model (QM1).
|
|
203
|
+
const info = await fetchControlSocketRuntimeInfo(sockPath, { timeout: 3_000 });
|
|
204
|
+
ok(
|
|
205
|
+
`get_info reports the ACP model, not reverted (got ${formatRuntimeModel(info) ?? "none"})`,
|
|
206
|
+
info.modelProvider === ACP_PROVIDER && info.modelId === ACP_MODEL,
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
// Drive ONE model turn over the stdin RPC: call the BUNDLED bridge's entwurf_self.
|
|
210
|
+
const prompt =
|
|
211
|
+
"Call the mcp__entwurf-bridge__entwurf_self tool now. Then reply with exactly the " +
|
|
212
|
+
"sessionId, agentId, and socketState values it returned, one per line, and nothing else. " +
|
|
213
|
+
"Do not paraphrase or invent values — copy them verbatim from the tool result.";
|
|
214
|
+
// The prompt must NOT leak the gid — the gid in the envelope is the proof the bridge
|
|
215
|
+
// answered, so a gid in the prompt would make that proof circular (the model could echo
|
|
216
|
+
// the prompt). Assert it at the source, not just by convention.
|
|
217
|
+
ok("the prompt does not leak the resident gid (envelope-gid proof stays non-circular)", !prompt.includes(gid));
|
|
218
|
+
cap = await driveSelfTurn(resident, prompt);
|
|
219
|
+
|
|
220
|
+
// A real model turn ran AND completed over RPC — no hang (the whole point of the
|
|
221
|
+
// resident circuit vs. the deferred `pi -p` one-shot teardown hang).
|
|
222
|
+
ok("a real model turn ran over the stdin RPC (agent_start)", cap.agentStartSeen);
|
|
223
|
+
ok("the turn completed cleanly over RPC (agent_end — no hang)", cap.agentEndSeen);
|
|
224
|
+
ok("the prompt RPC command was accepted", cap.promptAccepted);
|
|
225
|
+
|
|
226
|
+
// No extension blew up, the S0 fail-loud stub never fired (the provider path is real).
|
|
227
|
+
ok("no extension_error during the turn", cap.extensionErrors.length === 0);
|
|
228
|
+
ok("S0 fail-loud backend stub did NOT fire", !STUB_PATTERN.test(`${cap.stream}\n${stderrTail}`));
|
|
229
|
+
|
|
230
|
+
// PRIMARY proof — narrow to the ENVELOPE lines, then assert the three identity
|
|
231
|
+
// values appear TOGETHER inside them. Asserting against the whole stream is weak:
|
|
232
|
+
// readline attaches before the prompt, so startup/buffered events could carry a
|
|
233
|
+
// stray match. The envelope is the entwurf_self tool result (carried in the
|
|
234
|
+
// [tool:done] notice and echoed in the assistant reply), so it is the lines bearing
|
|
235
|
+
// the envelope markers — restrict the match to those.
|
|
236
|
+
const envelopeText = cap.stream
|
|
237
|
+
.split("\n")
|
|
238
|
+
.filter((l) => /socketState|agentId|sessionId|entwurf_self/i.test(l))
|
|
239
|
+
.join("\n");
|
|
240
|
+
|
|
241
|
+
// The resident's OWN freshly-minted gid appears in the envelope. The gid is never
|
|
242
|
+
// told to the model (asserted above); it lives only as PI_SESSION_ID inside the
|
|
243
|
+
// bridge env. So its presence in the envelope proves entwurf_self was CALLED and its
|
|
244
|
+
// operator-injected identity reached the model — the bundled bridge's last mile.
|
|
245
|
+
ok(
|
|
246
|
+
"the entwurf_self envelope carried the resident's own gid (bundled bridge reached the session)",
|
|
247
|
+
envelopeText.includes(gid),
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
// socketState alive — entwurf_self computes this from the live --entwurf-control socket.
|
|
251
|
+
ok("the envelope reported socketState alive", /socketState[\\":\s]*"?alive/i.test(envelopeText));
|
|
252
|
+
|
|
253
|
+
// agentId is the ACP model identity, not reverted/blank.
|
|
254
|
+
ok(
|
|
255
|
+
`the envelope reported agentId ${ACP_PROVIDER}/${ACP_MODEL}`,
|
|
256
|
+
envelopeText.includes(`${ACP_PROVIDER}/${ACP_MODEL}`),
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
// SUPPLEMENTARY (not a hard gate) — the ACP event mapper emits a tool notice when
|
|
260
|
+
// showToolNotifications is on. S2g lets operator config turn notices OFF, in which
|
|
261
|
+
// case the tool can succeed with no notice — so the envelope above is the real proof
|
|
262
|
+
// and the notice is only corroborating. Log it; do not fail on its absence.
|
|
263
|
+
const sawToolCall = /\[tool:(start|running|done)\][^\n]*entwurf_self/i.test(cap.stream);
|
|
264
|
+
console.log(
|
|
265
|
+
` info tool-call notice ${sawToolCall ? "observed" : "not observed (notices may be off; envelope is the proof)"}`,
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
// Still alive after the turn (a live socket-citizen, not a one-shot that exits).
|
|
269
|
+
ok(
|
|
270
|
+
"resident still alive after the turn (live socket-citizen)",
|
|
271
|
+
resident.exitCode === null && resident.signalCode === null,
|
|
272
|
+
);
|
|
273
|
+
} catch (err) {
|
|
274
|
+
if (cap)
|
|
275
|
+
console.error(`[smoke-acp-bundled-mcp-live] stream tail:\n${cap.stream.split("\n").slice(-30).join("\n")}`);
|
|
276
|
+
if (stderrTail)
|
|
277
|
+
console.error(`[smoke-acp-bundled-mcp-live] stderr tail:\n${stderrTail.split("\n").slice(-20).join("\n")}`);
|
|
278
|
+
throw err;
|
|
279
|
+
} finally {
|
|
280
|
+
if (resident) await terminateChild(resident);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Hygiene: after teardown the control socket file is gone — no process/socket residue.
|
|
284
|
+
// (The session JSONL is the denote-id memory layer and is intentionally NOT scrubbed.)
|
|
285
|
+
ok("control socket file removed after teardown (no socket residue)", await waitForGone(sockPath, 5_000));
|
|
286
|
+
|
|
287
|
+
console.log(
|
|
288
|
+
`[smoke-acp-bundled-mcp-live] PASS — ${passed} checks (bundled entwurf-bridge reaches the live ACP session via the 0.11.0 resident/RPC circuit)`,
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
await main();
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// S2e-1 — carrier/augment live acceptance. LIVE-gated, OUT of `pnpm check`.
|
|
2
|
+
//
|
|
3
|
+
// LIVE=1 ./run.sh smoke-acp-carrier-augment-live
|
|
4
|
+
//
|
|
5
|
+
// S2d-1c shipped the billing carrier (engraving) + the rich first-user augment,
|
|
6
|
+
// but only the deterministic gate (check-acp-carrier-augment) proved their SHAPE.
|
|
7
|
+
// This is the S2e-1 live half: it drives the REAL pi provider path and proves the
|
|
8
|
+
// two 핀1-critical behaviors the gate cannot observe on a real model turn:
|
|
9
|
+
//
|
|
10
|
+
// 1. the augment reaches the model — a unique secret written ONLY into the
|
|
11
|
+
// scratch cwd's AGENTS.md (NEVER into the user prompt) comes back in the
|
|
12
|
+
// reply, so buildPiContextAugment's "## <cwd>/AGENTS.md" section actually rode
|
|
13
|
+
// the wire to the live model via streamShellAcp.
|
|
14
|
+
// 2. the default (EMPTY) carrier does not trip subscription billing — the turn
|
|
15
|
+
// exits 0 with no HTTP-400 / "extra usage" billing error. This is the 핀1
|
|
16
|
+
// live check: a carrier-absent run must bill like a normal subscription call.
|
|
17
|
+
//
|
|
18
|
+
// Read-tool caveat (deliberate — GPT c32a6c8 Q1): Claude ACP exposes Read, so a
|
|
19
|
+
// model COULD read AGENTS.md directly instead of answering from the augment. We do
|
|
20
|
+
// not forbid that at the wire — the deterministic gate already locks the augment
|
|
21
|
+
// SHAPE; this smoke asks the model to answer WITHOUT tools and treats the secret
|
|
22
|
+
// in the reply as evidence that the augment+provider path is live AND billing-
|
|
23
|
+
// clean. The honest claim is "the augment rode the live provider path and the
|
|
24
|
+
// empty-carrier turn billed fine", NOT "the model was physically unable to read
|
|
25
|
+
// the file". A wire-dump would over-build for this cut's purpose.
|
|
26
|
+
//
|
|
27
|
+
// Optional carrier-present path (SMOKE_ACP_CARRIER_PRESENT=1, non-blocking — GPT
|
|
28
|
+
// c32a6c8 Q2): a second turn with a TINY engraving via ENTWURF_ACP_ENGRAVING_PATH
|
|
29
|
+
// proves a small _meta.systemPrompt carrier also exits 0 (carrier present ≠ 400).
|
|
30
|
+
//
|
|
31
|
+
// NOT proved here: session reuse (smoke-acp-session-reuse-live) and RGG (S2e-2).
|
|
32
|
+
|
|
33
|
+
import { strict as assert } from "node:assert";
|
|
34
|
+
import { spawnSync } from "node:child_process";
|
|
35
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
36
|
+
import { tmpdir } from "node:os";
|
|
37
|
+
import { dirname, join, resolve } from "node:path";
|
|
38
|
+
import { fileURLToPath } from "node:url";
|
|
39
|
+
|
|
40
|
+
const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
41
|
+
const MODEL = process.env.ENTWURF_ACP_PROVIDER_MODEL?.trim() || "claude-sonnet-4-6";
|
|
42
|
+
const PROVIDER = "entwurf";
|
|
43
|
+
const TURN_TIMEOUT_MS = Number(process.env.ENTWURF_ACP_PROVIDER_TIMEOUT_MS) || 240_000;
|
|
44
|
+
|
|
45
|
+
function fail(msg: string): never {
|
|
46
|
+
console.error(`[smoke-acp-carrier-augment-live] FAIL: ${msg}`);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (process.env.LIVE !== "1") {
|
|
51
|
+
console.error("[smoke-acp-carrier-augment-live] skipped — set LIVE=1 to run the real pi provider turn.");
|
|
52
|
+
process.exit(0);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Billing-reclassification canaries (핀1). The empty carrier must NOT trip metered
|
|
56
|
+
// billing — if it ever did, the turn would surface one of these. We assert their
|
|
57
|
+
// ABSENCE in the output tail. Kept narrow so a normal reply cannot false-positive.
|
|
58
|
+
const BILLING_FAILURE_PATTERNS = [
|
|
59
|
+
/out of (extra )?usage/i,
|
|
60
|
+
/\bHTTP[\s/]*400\b/i,
|
|
61
|
+
/\b400 Bad Request\b/i,
|
|
62
|
+
/insufficient (credit|balance|quota)/i,
|
|
63
|
+
/metered.*(not|unavailable|denied)/i,
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
// The S0 stub canary — its error means the provider path never opened.
|
|
67
|
+
const STUB_PATTERN = /AcpBackendNotImplementedError|not implemented in S0/i;
|
|
68
|
+
|
|
69
|
+
/** Run one real `pi` provider turn in `cwd`; return {status, stdout, combinedTail}. */
|
|
70
|
+
function runTurn(
|
|
71
|
+
cwd: string,
|
|
72
|
+
prompt: string,
|
|
73
|
+
extraEnv: Record<string, string>,
|
|
74
|
+
): {
|
|
75
|
+
status: number | null;
|
|
76
|
+
stdout: string;
|
|
77
|
+
combined: string;
|
|
78
|
+
tail: string;
|
|
79
|
+
} {
|
|
80
|
+
const args = [
|
|
81
|
+
"--no-extensions",
|
|
82
|
+
"-e",
|
|
83
|
+
REPO_ROOT,
|
|
84
|
+
"--mode",
|
|
85
|
+
"text",
|
|
86
|
+
"-p",
|
|
87
|
+
"--approve",
|
|
88
|
+
"--provider",
|
|
89
|
+
PROVIDER,
|
|
90
|
+
"--model",
|
|
91
|
+
MODEL,
|
|
92
|
+
prompt,
|
|
93
|
+
];
|
|
94
|
+
const res = spawnSync("pi", args, {
|
|
95
|
+
cwd,
|
|
96
|
+
encoding: "utf8",
|
|
97
|
+
timeout: TURN_TIMEOUT_MS,
|
|
98
|
+
env: { ...process.env, ...extraEnv },
|
|
99
|
+
});
|
|
100
|
+
const stdout = res.stdout ?? "";
|
|
101
|
+
const stderr = res.stderr ?? "";
|
|
102
|
+
const combined = `${stdout}\n${stderr}`;
|
|
103
|
+
const tail = combined.split("\n").slice(-25).join("\n");
|
|
104
|
+
if (res.error) {
|
|
105
|
+
console.error(`[smoke-acp-carrier-augment-live] output tail:\n${tail}`);
|
|
106
|
+
fail(`pi spawn failed: ${res.error.message}`);
|
|
107
|
+
}
|
|
108
|
+
return { status: res.status, stdout, combined, tail };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function assertCleanTurn(label: string, turn: { status: number | null; combined: string; tail: string }): void {
|
|
112
|
+
assert.ok(!STUB_PATTERN.test(turn.combined), `${label}: S0 stub fired — provider path did not open`);
|
|
113
|
+
for (const pat of BILLING_FAILURE_PATTERNS) {
|
|
114
|
+
assert.ok(!pat.test(turn.combined), `${label}: billing-reclassification canary matched ${pat} (핀1 violation?)`);
|
|
115
|
+
}
|
|
116
|
+
if (turn.status !== 0) {
|
|
117
|
+
console.error(`[smoke-acp-carrier-augment-live] ${label} output tail:\n${turn.tail}`);
|
|
118
|
+
fail(`${label}: pi exited ${turn.status}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const scratch = mkdtempSync(join(tmpdir(), "entwurf-s2e1-"));
|
|
123
|
+
try {
|
|
124
|
+
// A nonce unique to this run; lives ONLY in the cwd AGENTS.md, never the prompt.
|
|
125
|
+
const nonce = `${process.pid.toString(36)}${Date.now().toString(36)}`;
|
|
126
|
+
const secret = `SAC_${nonce}`;
|
|
127
|
+
const agentsPath = join(scratch, "AGENTS.md");
|
|
128
|
+
writeFileSync(
|
|
129
|
+
agentsPath,
|
|
130
|
+
[
|
|
131
|
+
"# Scratch project — entwurf S2e-1 augment live check",
|
|
132
|
+
"",
|
|
133
|
+
`SECRET_PROJECT_CODE: ${secret}`,
|
|
134
|
+
"",
|
|
135
|
+
"When asked for the secret project code, reply with the value above.",
|
|
136
|
+
"",
|
|
137
|
+
].join("\n"),
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
// The prompt never names the secret; only the cwd AGENTS.md (carried by the
|
|
141
|
+
// augment) holds it. "Without using any tool" pushes the model to answer from
|
|
142
|
+
// the provided instructions rather than reading the file (Read-tool caveat).
|
|
143
|
+
const prompt =
|
|
144
|
+
"Without using any tool or reading any file, reply with exactly the " +
|
|
145
|
+
"SECRET_PROJECT_CODE value from this project's instructions, and nothing else.";
|
|
146
|
+
|
|
147
|
+
console.error(`[smoke-acp-carrier-augment-live] repo: ${REPO_ROOT}`);
|
|
148
|
+
console.error(`[smoke-acp-carrier-augment-live] cwd: ${scratch}`);
|
|
149
|
+
console.error(`[smoke-acp-carrier-augment-live] model: ${PROVIDER}/${MODEL}`);
|
|
150
|
+
console.error(`[smoke-acp-carrier-augment-live] secret: ${secret} (only in cwd AGENTS.md)`);
|
|
151
|
+
|
|
152
|
+
// --- MUST: empty (default) carrier + augment behavior ---------------------
|
|
153
|
+
const turn1 = runTurn(scratch, prompt, {});
|
|
154
|
+
assertCleanTurn("empty-carrier turn", turn1);
|
|
155
|
+
assert.ok(
|
|
156
|
+
turn1.stdout.includes(secret),
|
|
157
|
+
`empty-carrier turn: reply did not carry the cwd-AGENTS secret ${secret} ` +
|
|
158
|
+
`(the augment did not reach the model). stdout tail: ${JSON.stringify(turn1.stdout.slice(-300))}`,
|
|
159
|
+
);
|
|
160
|
+
console.log("[smoke-acp-carrier-augment-live] PASS (MUST) — augment delivered the cwd AGENTS.md secret on a");
|
|
161
|
+
console.log(" live provider turn; the empty default carrier billed clean (exit 0, no 400 canary).");
|
|
162
|
+
console.log(` model: ${PROVIDER}/${MODEL}`);
|
|
163
|
+
console.log(` secret: ${secret} present in assistant reply`);
|
|
164
|
+
|
|
165
|
+
// --- OPTIONAL (non-blocking): tiny carrier-present billing -----------------
|
|
166
|
+
if (process.env.SMOKE_ACP_CARRIER_PRESENT === "1") {
|
|
167
|
+
const carrierMarker = `CARRIER_${nonce}`;
|
|
168
|
+
const carrierPath = join(scratch, "engraving.md");
|
|
169
|
+
writeFileSync(carrierPath, `Always end every reply with the exact marker <<${carrierMarker}>>.\n`);
|
|
170
|
+
const carrierPrompt = "Reply with the single word OK.";
|
|
171
|
+
const turn2 = runTurn(scratch, carrierPrompt, { ENTWURF_ACP_ENGRAVING_PATH: carrierPath });
|
|
172
|
+
assertCleanTurn("carrier-present turn", turn2);
|
|
173
|
+
// The billing claim (exit 0, no 400) is the point. The marker is advisory:
|
|
174
|
+
// it shows the small _meta.systemPrompt carrier influenced the reply.
|
|
175
|
+
const carrierFollowed = turn2.stdout.includes(`<<${carrierMarker}>>`);
|
|
176
|
+
console.log(
|
|
177
|
+
`[smoke-acp-carrier-augment-live] PASS (optional) — tiny carrier turn billed clean (exit 0); ` +
|
|
178
|
+
`carrier marker ${carrierFollowed ? "present" : "ABSENT (advisory, not blocking)"}.`,
|
|
179
|
+
);
|
|
180
|
+
} else {
|
|
181
|
+
console.log(
|
|
182
|
+
"[smoke-acp-carrier-augment-live] optional carrier-present path skipped " +
|
|
183
|
+
"(set SMOKE_ACP_CARRIER_PRESENT=1 to also check a tiny carrier bills clean).",
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
} finally {
|
|
187
|
+
try {
|
|
188
|
+
rmSync(scratch, { recursive: true, force: true });
|
|
189
|
+
} catch {
|
|
190
|
+
// best-effort cleanup
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// S2g LIVE 1 — operator MCP passthrough acceptance. LIVE-gated, OUT of `pnpm check`.
|
|
2
|
+
//
|
|
3
|
+
// LIVE=1 ./run.sh smoke-acp-mcp-live
|
|
4
|
+
//
|
|
5
|
+
// THE baseline proof. GLG's baseline finding was: an ACP model booted with
|
|
6
|
+
// `entwurf/<model>` saw 4 tools and NO MCP servers — the operator's
|
|
7
|
+
// `entwurfProvider.mcpServers` never reached the session. S2g wired that
|
|
8
|
+
// passthrough; the deterministic gate (check-acp-config / check-acp-session-reuse
|
|
9
|
+
// Section G) proved the config reaches `newSession` on a FAKE seam. This live
|
|
10
|
+
// smoke proves the LAST mile a fake cannot: a real ACP child spawns the operator
|
|
11
|
+
// MCP server and the live model can actually CALL its tool.
|
|
12
|
+
//
|
|
13
|
+
// Method (GPT `…2f9325` LIVE 1 — a TINY isolated probe, not entwurf-bridge, so a
|
|
14
|
+
// failure isolates to "did mcpServers reach newSession" without identity/env
|
|
15
|
+
// coupling): a scratch `.pi/settings.json` registers ONE stdio MCP server
|
|
16
|
+
// (scripts/fixtures/probe-mcp-server.ts) that exposes a single tool `probe_nonce`
|
|
17
|
+
// returning a per-run secret carried in its env. The prompt asks the model to
|
|
18
|
+
// CALL the tool and echo the nonce. The nonce lives ONLY inside the MCP server's
|
|
19
|
+
// env — the model cannot produce it unless the tool was visible AND callable.
|
|
20
|
+
//
|
|
21
|
+
// Pre-S2g (hardcoded `mcpServers:[]`) this smoke FAILS: the tool never exists.
|
|
22
|
+
|
|
23
|
+
import { strict as assert } from "node:assert";
|
|
24
|
+
import { spawnSync } from "node:child_process";
|
|
25
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
26
|
+
import { tmpdir } from "node:os";
|
|
27
|
+
import { dirname, join, resolve } from "node:path";
|
|
28
|
+
import { fileURLToPath } from "node:url";
|
|
29
|
+
|
|
30
|
+
const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
31
|
+
const PROBE_SERVER = join(REPO_ROOT, "scripts", "fixtures", "probe-mcp-server.ts");
|
|
32
|
+
const MODEL = process.env.ENTWURF_ACP_PROVIDER_MODEL?.trim() || "claude-sonnet-4-6";
|
|
33
|
+
const PROVIDER = "entwurf";
|
|
34
|
+
const TURN_TIMEOUT_MS = Number(process.env.ENTWURF_ACP_PROVIDER_TIMEOUT_MS) || 240_000;
|
|
35
|
+
|
|
36
|
+
function fail(msg: string): never {
|
|
37
|
+
console.error(`[smoke-acp-mcp-live] FAIL: ${msg}`);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (process.env.LIVE !== "1") {
|
|
42
|
+
console.error("[smoke-acp-mcp-live] skipped — set LIVE=1 to run the real pi provider turn.");
|
|
43
|
+
process.exit(0);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const STUB_PATTERN = /AcpBackendNotImplementedError|not implemented in S0/i;
|
|
47
|
+
|
|
48
|
+
const scratch = mkdtempSync(join(tmpdir(), "entwurf-s2g-mcp-"));
|
|
49
|
+
try {
|
|
50
|
+
const nonce = `MCP_${process.pid.toString(36)}${Date.now().toString(36)}`;
|
|
51
|
+
// Register the probe MCP server in the scratch project settings — the exact
|
|
52
|
+
// `entwurfProvider.mcpServers` surface `./run.sh install` writes for real.
|
|
53
|
+
mkdirSync(join(scratch, ".pi"), { recursive: true });
|
|
54
|
+
writeFileSync(
|
|
55
|
+
join(scratch, ".pi", "settings.json"),
|
|
56
|
+
`${JSON.stringify(
|
|
57
|
+
{
|
|
58
|
+
entwurfProvider: {
|
|
59
|
+
mcpServers: {
|
|
60
|
+
probe: {
|
|
61
|
+
command: process.execPath,
|
|
62
|
+
args: ["--experimental-strip-types", PROBE_SERVER],
|
|
63
|
+
env: { PROBE_NONCE: nonce },
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
null,
|
|
69
|
+
2,
|
|
70
|
+
)}\n`,
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const prompt =
|
|
74
|
+
"Call the probe_nonce tool now, then reply with exactly the PROBE_NONCE value it returns " +
|
|
75
|
+
"(the part after the '=' sign), and nothing else.";
|
|
76
|
+
|
|
77
|
+
console.error(`[smoke-acp-mcp-live] repo: ${REPO_ROOT}`);
|
|
78
|
+
console.error(`[smoke-acp-mcp-live] cwd: ${scratch}`);
|
|
79
|
+
console.error(`[smoke-acp-mcp-live] model: ${PROVIDER}/${MODEL}`);
|
|
80
|
+
console.error(`[smoke-acp-mcp-live] nonce: ${nonce} (only inside the probe MCP server env)`);
|
|
81
|
+
|
|
82
|
+
const args = [
|
|
83
|
+
"--no-extensions",
|
|
84
|
+
"-e",
|
|
85
|
+
REPO_ROOT,
|
|
86
|
+
"--mode",
|
|
87
|
+
"text",
|
|
88
|
+
"-p",
|
|
89
|
+
"--approve",
|
|
90
|
+
"--provider",
|
|
91
|
+
PROVIDER,
|
|
92
|
+
"--model",
|
|
93
|
+
MODEL,
|
|
94
|
+
prompt,
|
|
95
|
+
];
|
|
96
|
+
const res = spawnSync("pi", args, { cwd: scratch, encoding: "utf8", timeout: TURN_TIMEOUT_MS, env: process.env });
|
|
97
|
+
const stdout = res.stdout ?? "";
|
|
98
|
+
const combined = `${stdout}\n${res.stderr ?? ""}`;
|
|
99
|
+
const tail = combined.split("\n").slice(-30).join("\n");
|
|
100
|
+
if (res.error) {
|
|
101
|
+
console.error(`[smoke-acp-mcp-live] output tail:\n${tail}`);
|
|
102
|
+
fail(`pi spawn failed: ${res.error.message}`);
|
|
103
|
+
}
|
|
104
|
+
assert.ok(!STUB_PATTERN.test(combined), "S0 stub fired — provider path did not open");
|
|
105
|
+
if (res.status !== 0) {
|
|
106
|
+
console.error(`[smoke-acp-mcp-live] output tail:\n${tail}`);
|
|
107
|
+
fail(`pi exited ${res.status}`);
|
|
108
|
+
}
|
|
109
|
+
// The tool-start notice is corroborating evidence the model invoked the MCP tool.
|
|
110
|
+
const sawToolCall = /\[tool:(start|running)\]\s+probe_nonce/i.test(combined);
|
|
111
|
+
assert.ok(
|
|
112
|
+
stdout.includes(nonce),
|
|
113
|
+
`reply did not carry the probe nonce ${nonce} — the operator MCP server was not visible/callable ` +
|
|
114
|
+
`(S2g passthrough failure). tool-call notice ${sawToolCall ? "WAS" : "was NOT"} seen. ` +
|
|
115
|
+
`stdout tail: ${JSON.stringify(stdout.slice(-300))}`,
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
console.log("[smoke-acp-mcp-live] PASS — the operator-declared MCP server reached the live ACP session: the model");
|
|
119
|
+
console.log(` called probe_nonce and returned ${nonce} (the nonce lives only inside the MCP server env).`);
|
|
120
|
+
console.log(
|
|
121
|
+
` model: ${PROVIDER}/${MODEL}; tool-call notice ${sawToolCall ? "observed" : "not observed (reply-only evidence)"}`,
|
|
122
|
+
);
|
|
123
|
+
} finally {
|
|
124
|
+
try {
|
|
125
|
+
rmSync(scratch, { recursive: true, force: true });
|
|
126
|
+
} catch {
|
|
127
|
+
// best-effort cleanup
|
|
128
|
+
}
|
|
129
|
+
}
|