mtech-ai-local-runner 0.1.8 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/README.md +56 -4
  2. package/dist/src/adapters/claude-code/adapter.js +162 -120
  3. package/dist/src/adapters/claude-code/completion-report.js +2 -64
  4. package/dist/src/adapters/claude-code/prompt-compiler.js +1 -60
  5. package/dist/src/adapters/claude-code/runtime-options.js +72 -0
  6. package/dist/src/adapters/codex/account-sources.js +113 -0
  7. package/dist/src/adapters/codex/adapter.js +217 -0
  8. package/dist/src/adapters/codex/app-server-events.js +84 -0
  9. package/dist/src/adapters/codex/compatibility.js +45 -0
  10. package/dist/src/adapters/codex/config-builder.js +43 -0
  11. package/dist/src/adapters/codex/exec-invocation.js +62 -0
  12. package/dist/src/adapters/codex/exec-launch-host.js +209 -0
  13. package/dist/src/adapters/codex/exec-observer.js +83 -0
  14. package/dist/src/adapters/codex/exec-preflight.js +41 -0
  15. package/dist/src/adapters/codex/exec-release-policy.js +44 -0
  16. package/dist/src/adapters/codex/exec-runtime-options.js +113 -0
  17. package/dist/src/adapters/codex/exec-tool-gate.js +81 -0
  18. package/dist/src/adapters/codex/global-instructions.js +32 -0
  19. package/dist/src/adapters/codex/host-admission.js +3 -0
  20. package/dist/src/adapters/codex/inspection-rpc.js +48 -0
  21. package/dist/src/adapters/codex/job-rpc.js +189 -0
  22. package/dist/src/adapters/codex/manual-policy-scope.js +67 -0
  23. package/dist/src/adapters/codex/native-policy.js +128 -0
  24. package/dist/src/adapters/codex/native-proof.js +14 -0
  25. package/dist/src/adapters/codex/native-session.js +356 -0
  26. package/dist/src/adapters/codex/plugin.js +26 -0
  27. package/dist/src/adapters/codex/pre-model-gate.js +112 -0
  28. package/dist/src/adapters/codex/prompt-compiler.js +13 -0
  29. package/dist/src/adapters/codex/runtime-options.js +163 -0
  30. package/dist/src/adapters/codex/scratch.js +28 -0
  31. package/dist/src/adapters/codex/stdio-rpc.js +113 -0
  32. package/dist/src/adapters/codex/stream-parser.js +85 -0
  33. package/dist/src/adapters/codex/supported-admission.js +97 -0
  34. package/dist/src/adapters/codex/tool-dispatcher.js +20 -0
  35. package/dist/src/adapters/codex/unmanaged-scope.js +85 -0
  36. package/dist/src/adapters/completion-report.js +64 -0
  37. package/dist/src/adapters/discovery.js +76 -0
  38. package/dist/src/adapters/prompt-compiler.js +60 -0
  39. package/dist/src/adapters/registry.js +10 -5
  40. package/dist/src/auth/token-provider.js +2 -0
  41. package/dist/src/cli.js +98 -58
  42. package/dist/src/commands/doctor.js +7 -1
  43. package/dist/src/commands/setup.js +3 -0
  44. package/dist/src/config.js +9 -2
  45. package/dist/src/daemon/execution-coordinator.js +19 -2
  46. package/dist/src/daemon/manual-codex.js +82 -0
  47. package/dist/src/daemon/runtime.js +43 -43
  48. package/dist/src/gateway-client.js +3 -2
  49. package/dist/src/process/node-process-group-host.js +74 -0
  50. package/dist/src/safety/cli-host-admission.js +74 -0
  51. package/dist/src/safety/cli-security-modes.js +4 -0
  52. package/dist/src/safety/codex-exec-production-host.js +249 -0
  53. package/dist/src/safety/codex-exec-releases.js +3 -0
  54. package/dist/src/safety/codex-manual-controller.js +72 -0
  55. package/dist/src/safety/codex-manual-profile-query.js +30 -0
  56. package/dist/src/safety/codex-manual-registered.js +30 -0
  57. package/dist/src/safety/codex-native-observation.js +68 -0
  58. package/dist/src/safety/codex-native-process.js +59 -0
  59. package/dist/src/safety/codex-production-host.js +267 -0
  60. package/dist/src/safety/codex-reviewed-manual-release.js +37 -0
  61. package/dist/src/safety/codex-runtime-host.js +36 -0
  62. package/dist/src/safety/codex-supported-releases.js +1 -0
  63. package/dist/src/safety/hook-cli.js +39 -9
  64. package/dist/src/safety/hook-command.js +156 -0
  65. package/dist/src/safety/hook-policy-builder.js +6 -11
  66. package/dist/src/safety/job-security-policy.js +62 -0
  67. package/dist/src/safety/job-security-receipt.js +20 -0
  68. package/dist/src/safety/job-security-session.js +133 -0
  69. package/dist/src/safety/job-tool-dispatcher.js +52 -0
  70. package/dist/src/safety/job-tool-policy.js +61 -0
  71. package/dist/src/safety/job-tool-session.js +82 -0
  72. package/dist/src/safety/local-tool-policy.js +101 -0
  73. package/dist/src/safety/local-tool-server.js +211 -0
  74. package/dist/src/safety/local-tool-worker.js +57 -0
  75. package/dist/src/safety/pre-tool-use-hook.js +3 -39
  76. package/dist/src/safety/registered-command-integrity.js +52 -0
  77. package/dist/src/safety/security-requirements.js +49 -0
  78. package/dist/src/safety/stable-file-read.js +33 -0
  79. package/dist/src/safety/subprocess-sandbox.js +62 -0
  80. package/native/bin/codex-observe-darwin-arm64 +0 -0
  81. package/package.json +5 -3
package/README.md CHANGED
@@ -10,12 +10,60 @@ The package stores safe runner configuration locally and keeps OAuth refresh cre
10
10
 
11
11
  Public npm installation requires no npm account or publishing OTP. Setup requires Gateway OAuth login, and local Claude Code must already be authenticated (`claude auth status`).
12
12
 
13
- ## 0.1.8 (release candidate)
13
+ ## 0.1.10: enforced PreToolUse hook
14
+
15
+ Earlier releases configured a hook command that the shell could not run, so Claude
16
+ Code ran tool calls without the hook policy. This release runs the hook through
17
+ Node. Unless the packaged hook denies a `git push origin HEAD` probe, `setup` and
18
+ the daemon stop and `doctor` reports `hook_not_enforcing`.
19
+
20
+ ## 0.1.9: gated Codex foundation and shared CLI security
21
+
22
+ This package includes the production Codex host/probe and common authenticated
23
+ four-tool policy, command/dependency pins, owned cancellation, retained-session
24
+ checks and security receipts. The source-reviewed Codex release registry is empty:
25
+ Codex cannot advertise or execute until an exact binary, model, policy and release
26
+ manifest have been genuinely reviewed. Do not enable a Gateway flag to bypass it.
27
+
28
+ **Upgrade limitation:** this package also closes the Claude adapter until its
29
+ reviewed native evidence is available. Do not replace a working 0.1.8 daemon as
30
+ part of the Codex-disabled Gateway delivery. Gateway retains the legacy Claude
31
+ prepare/launch and approval payloads unless an explicit Claude security manifest
32
+ is configured; Codex always requires the new security descriptor. Publication is
33
+ separate from an authorized daemon upgrade or actual account/model acceptance.
34
+
35
+ The Codex probe only reads the original supported file-backend login status and
36
+ never runs vendor login/status, refreshes tokens, writes credentials or switches
37
+ backends. Keychain-only/unknown backends remain unavailable rather than being
38
+ migrated. Native CLI startup can still perform vendor-managed refresh; a no-model
39
+ inspection is not a no-refresh guarantee. Keep the existing login and obtain the
40
+ separate account/activation approval before actual CLI startup.
41
+
42
+ The supported policy mode acknowledges vendor runtime/authentication/extension/
43
+ IPC/network trust; it does not claim total broker-only isolation. Synthetic test
44
+ receipts never authorize production. The repository report `docs/ai-ops-gateway/codex-production-host-validation.md`
45
+ records the tested scope and remaining rollout gates.
46
+
47
+ The intended employee request after rollout is: “Use my local Codex for Billing.
48
+ Inspect the payment entry points without editing files, and return the findings
49
+ and verification here.” Gateway OAuth signs the employee into Gateway; a separate
50
+ `codex login` using ChatGPT authorizes Codex. An OpenAI API key is not a fallback.
51
+ If both executors are usable, select one before approval; changing the executor
52
+ or server-resolved model requires a new launch approval.
53
+
54
+ Source operations use authenticated job-only tools. Registered test programs need
55
+ exact executable/argv and explicit dependency-file read grants; source writes,
56
+ network, ambient credentials and child process creation are denied to those test
57
+ programs. Broad npm/project test runners are not automatically supported. Missing
58
+ grants produce a visible blocked result. Actual employee-local task duration and
59
+ count remain unbounded; preparation retains the 0.1.8 limits.
60
+
61
+ ## 0.1.8
14
62
 
15
63
  - Report safe preparation stage/tool/error categories and settle known failures instead of silently reclaiming the same job forever.
16
64
  - Bound preparation to 15 minutes, retain terminal receipts across connection failures, and halt on unknown binding outcomes until reconciled.
17
65
  - Pair with Gateway cancellation propagation and three-delivery preparation bounds. Actual approved unbounded employee-local Claude execution remains unbounded.
18
- - Prepared locally; npm publication and daemon upgrade are separate actions. Keep a stopped runner stopped until its cancelled job is confirmed terminal on Gateway.
66
+ - npm publication and daemon upgrade are separate actions. Keep a stopped runner stopped until its cancelled job is confirmed terminal on Gateway.
19
67
 
20
68
  ## 0.1.7
21
69
 
@@ -46,8 +94,8 @@ The relay binds only to loopback. Local capabilities are stored in private mode-
46
94
  Existing employees can upgrade their installed daemon with:
47
95
 
48
96
  ```bash
49
- npx mtech-ai-local-runner@0.1.7 setup
50
- npx mtech-ai-local-runner@0.1.7 doctor
97
+ npx mtech-ai-local-runner setup
98
+ npx mtech-ai-local-runner doctor
51
99
  ```
52
100
 
53
101
  ### Cancellation and preparation failures
@@ -76,3 +124,7 @@ These preparation controls end at binding and do not impose time, turn, token
76
124
  or cost termination limits on an approved unbounded employee-local Claude run.
77
125
  Existing generation fencing, scope restrictions, cancellation and audit remain
78
126
  in force.
127
+
128
+ ### Manual Codex pilot (0.1.11)
129
+
130
+ The reviewed manual Codex path preserves the existing CLI and ChatGPT login. It is separate from unattended daemon readiness. After the exact CLI/native reader and Gateway release are installed, an employee-authorized foreground pilot uses `mtec-ai-local-runner manual-codex --job-id=<approved Job UUID>`. It refuses a running daemon, unresolved recovery state, unknown release/account or a different Job. It does not stop services or alter personal settings. A direct administrator policy inventory occurs just before the approved execution; historical inventory is not continuous MDM monitoring. The Gateway OAuth client refuses refresh in this manual command. Actual vendor account startup may still refresh shared credentials and requires disclosure in the exact launch approval.
@@ -1,17 +1,16 @@
1
1
  import path from "node:path";
2
- import { existsSync } from "node:fs";
2
+ import { requireSecurityRequirements } from '../../safety/security-requirements.js';
3
+ import { createJobSecurityReceipt } from '../../safety/job-security-receipt.js';
3
4
  import { isTerminalEventType } from "../../protocol.js";
4
5
  import { parseClaudeLine } from "./stream-parser.js";
5
6
  import { compilePrompt } from "./prompt-compiler.js";
6
7
  import { completionEvent } from "./completion-report.js";
7
- import { buildHookPolicy, encodeHookPolicy } from "../../safety/hook-policy-builder.js";
8
- import { HOOK_POLICY_ENV_VAR } from "../../safety/hook-cli.js";
9
- const ALLOWED_ENV_KEYS = ["PATH", "HOME", "USER", "LOGNAME", "LANG", "LC_ALL", "TMPDIR", "NODE_ENV"];
8
+ import { requireJobSecurity } from "../../safety/job-security-policy.js";
9
+ import { HOOK_POLICY_ENV_VAR, buildHookPolicy, encodeHookPolicy } from "../../safety/hook-policy-builder.js";
10
+ import { formatHookCommand } from "../../safety/hook-command.js";
11
+ const ALLOWED_ENV_KEYS = ["PATH", "HOME", "USER", "LOGNAME", "LANG", "LC_ALL", "TMPDIR", "NODE_ENV", "CLAUDE_CONFIG_DIR"];
10
12
  const ALLOWED_SUPERVISOR_COMMANDS = new Set(["continue", "answer_question", "request_revision", "request_checkpoint", "submit_for_review"]);
11
- const DEFAULT_BUILTIN_TOOL_SURFACE = ["Bash", "Edit", "Write", "MultiEdit", "NotebookEdit", "Read", "Glob", "Grep"];
12
- function defaultPathExists(p) {
13
- return existsSync(p);
14
- }
13
+ const LOCAL_NAMES = ["local_read", "local_search", "local_edit", "local_test"].map(name => `mcp__ai_local__${name}`);
15
14
  function allowlistedEnv(env) {
16
15
  const result = {};
17
16
  for (const key of ALLOWED_ENV_KEYS) {
@@ -38,18 +37,14 @@ function isUnboundedLocalExecution(taskPackage) {
38
37
  && execution?.runtime_adapter === "claude_code_cli"
39
38
  && execution?.limits_mode === "unbounded";
40
39
  }
41
- function contractReadTools(taskPackage, registeredReadTools) {
42
- const pkg = taskPackage;
43
- const contract = pkg.contract;
44
- const policy = contract?.policy;
45
- const allowed = new Set(Array.isArray(policy?.allowed_gateway_tools)
46
- ? policy.allowed_gateway_tools.filter((tool) => typeof tool === "string")
47
- : []);
48
- return [...new Set(registeredReadTools.filter((tool) => allowed.has(tool)))];
49
- }
50
40
  export class ClaudeCodeAdapter {
51
41
  options;
52
42
  handle = null;
43
+ tools;
44
+ cancelled = false;
45
+ started = false;
46
+ receipt;
47
+ securityReceipt() { return this.receipt ? structuredClone(this.receipt) : undefined; }
53
48
  constructor(options) {
54
49
  this.options = options;
55
50
  }
@@ -58,6 +53,31 @@ export class ClaudeCodeAdapter {
58
53
  if (!resolvedModelId) {
59
54
  throw new Error("refusing to start: no Gateway-resolved model ID was supplied");
60
55
  }
56
+ if (this.started)
57
+ throw new Error("adapter has already been started");
58
+ this.started = true;
59
+ const generation = ctx.generation;
60
+ if (!Number.isSafeInteger(generation) || generation === undefined || generation < 1)
61
+ throw new Error("JOB_SECURITY_REJECTED");
62
+ const assertAdmission = () => {
63
+ requireJobSecurity(this.options.securityPolicy, { adapterId: "claude_code_cli", adapterVersion: "1", jobId: this.options.jobId ?? ctx.bindingId, modelId: resolvedModelId, generation });
64
+ if (ctx.approvedTaskPackage?.resolved_provider !== "anthropic" || ctx.approvedTaskPackage?.resolved_model_id !== resolvedModelId
65
+ || ctx.approvedTaskPackage?.repository?.base_commit_sha !== this.options.securityPolicy.pin.sourceSha)
66
+ throw new Error("SECURITY_REQUIREMENTS_REJECTED");
67
+ requireSecurityRequirements(ctx.approvedTaskPackage?.security_requirements, { adapterId: "claude_code_cli", adapterVersion: "1", requirementsSha256: this.options.securityPolicy.pin.requirementsSha256 });
68
+ };
69
+ assertAdmission();
70
+ if (!ctx.securityBinding)
71
+ throw new Error('SECURITY_REQUIREMENTS_REJECTED');
72
+ this.receipt = createJobSecurityReceipt(this.options.securityPolicy, { jobId: this.options.jobId ?? ctx.bindingId, generation, modelId: resolvedModelId, sourceSha: this.options.securityPolicy.pin.sourceSha, ...ctx.securityBinding, requirements: ctx.approvedTaskPackage?.security_requirements });
73
+ const pkg = ctx.taskPackage;
74
+ const repos = pkg.repositories;
75
+ if (!Array.isArray(repos) || repos.length !== 1 || repos[0]?.base_commit_sha !== this.options.securityPolicy.pin.sourceSha)
76
+ throw new Error("JOB_SECURITY_REJECTED");
77
+ const cwd = this.options.cwd;
78
+ const rel = cwd ? path.relative(path.resolve(this.options.worktreeRoot), path.resolve(cwd)) : "";
79
+ if (!cwd || !path.isAbsolute(cwd) || rel === "" || (!rel.startsWith(".." + path.sep) && rel !== ".." && !path.isAbsolute(rel)))
80
+ throw new Error("trusted cwd must live outside the assigned worktree");
61
81
  const unbounded = isUnboundedLocalExecution(ctx.taskPackage);
62
82
  if (!unbounded && (!Number.isFinite(this.options.maxBudgetUsd) || this.options.maxBudgetUsd <= 0)) {
63
83
  throw new Error("refusing to start: no positive remaining contract budget was supplied");
@@ -68,66 +88,75 @@ export class ClaudeCodeAdapter {
68
88
  if (!ctx.sessionId) {
69
89
  throw new Error("refusing to start: no runner-generated session ID was supplied");
70
90
  }
71
- const hookPath = this.requireTrustedHookPath();
91
+ const hookCommand = this.requireTrustedHookCommand();
72
92
  const prompt = compilePrompt(ctx.taskPackage, { workItemLabel: this.options.workItemLabel });
73
- const allowedReadTools = contractReadTools(ctx.taskPackage, this.options.registeredReadTools);
74
- const mcpConfigPath = this.options.writeMcpConfig({ tools: { allow: allowedReadTools } });
75
- const maxBudgetUsd = unbounded
76
- ? null
77
- : Math.min(this.options.maxBudgetUsd, contractBudget(ctx.taskPackage));
78
- const hookPolicy = buildHookPolicy({
79
- taskPackage: ctx.taskPackage,
80
- worktreeRoot: this.options.worktreeRoot,
81
- registeredReadTools: this.options.registeredReadTools,
82
- generatedPaths: [mcpConfigPath],
83
- });
84
- const settings = {
85
- hooks: {
86
- PreToolUse: [{ matcher: "*", hooks: [{ type: "command", command: hookPath }] }],
87
- },
88
- };
89
- const builtinToolSurface = this.options.builtinToolSurface ?? DEFAULT_BUILTIN_TOOL_SURFACE;
90
- const mcpServerName = this.options.mcpServerName ?? "ai_gateway";
91
- const toolSurface = [
92
- ...builtinToolSurface,
93
- ...allowedReadTools.map((tool) => `mcp__${mcpServerName}__${tool}`),
94
- ];
95
- const args = [
96
- "--print",
97
- "--input-format", "stream-json",
98
- "--output-format", "stream-json",
99
- "--verbose",
100
- "--include-partial-messages",
101
- "--no-chrome",
102
- "--permission-mode", "bypassPermissions",
103
- "--dangerously-skip-permissions",
104
- "--model", resolvedModelId,
105
- ...(maxBudgetUsd === null ? [] : ["--max-budget-usd", String(maxBudgetUsd)]),
106
- "--name", this.options.workItemLabel.trim().slice(0, 80) || `work-item-${ctx.bindingId.slice(0, 8)}`,
107
- "--strict-mcp-config",
108
- "--mcp-config", mcpConfigPath,
109
- "--settings", JSON.stringify(settings),
110
- "--setting-sources", "user",
111
- "--disable-slash-commands",
112
- "--tools", builtinToolSurface.join(","),
113
- "--allowedTools", toolSurface.join(","),
114
- ];
115
- if (ctx.resumeSessionId) {
116
- args.push("--resume", ctx.resumeSessionId);
93
+ try {
94
+ if (!this.options.createTools || !this.options.writeLocalMcpConfig)
95
+ throw new Error("job-local tool setup is required");
96
+ this.tools = await this.options.createTools(ctx, pkg, ctx.approvedTaskPackage ?? {});
97
+ if (this.cancelled)
98
+ throw new Error("adapter startup cancelled");
99
+ const endpoint = await this.tools.start();
100
+ if (this.cancelled)
101
+ throw new Error("adapter startup cancelled");
102
+ const mcpConfigPath = this.options.writeLocalMcpConfig(endpoint);
103
+ const maxBudgetUsd = unbounded
104
+ ? null
105
+ : Math.min(this.options.maxBudgetUsd, contractBudget(ctx.taskPackage));
106
+ const hookPolicy = buildHookPolicy({
107
+ taskPackage: ctx.taskPackage,
108
+ worktreeRoot: this.options.worktreeRoot,
109
+ registeredReadTools: this.options.registeredReadTools,
110
+ generatedPaths: [mcpConfigPath],
111
+ });
112
+ const settings = {
113
+ hooks: {
114
+ PreToolUse: [{ matcher: "*", hooks: [{ type: "command", command: hookCommand }] }],
115
+ },
116
+ };
117
+ const args = [
118
+ "--print",
119
+ "--input-format", "stream-json",
120
+ "--output-format", "stream-json",
121
+ "--verbose",
122
+ "--include-partial-messages",
123
+ "--no-chrome",
124
+ "--permission-mode", "default",
125
+ "--model", resolvedModelId,
126
+ ...(maxBudgetUsd === null ? [] : ["--max-budget-usd", String(maxBudgetUsd)]),
127
+ "--name", this.options.workItemLabel.trim().slice(0, 80) || `work-item-${ctx.bindingId.slice(0, 8)}`,
128
+ "--strict-mcp-config",
129
+ "--mcp-config", mcpConfigPath,
130
+ "--settings", JSON.stringify(settings),
131
+ "--setting-sources", "",
132
+ "--disable-slash-commands",
133
+ "--tools", "",
134
+ "--allowedTools", LOCAL_NAMES.join(","),
135
+ ];
136
+ if (ctx.resumeSessionId) {
137
+ args.push("--resume", ctx.resumeSessionId);
138
+ }
139
+ else {
140
+ args.push("--session-id", ctx.sessionId);
141
+ }
142
+ const env = {
143
+ ...allowlistedEnv(this.options.env),
144
+ [HOOK_POLICY_ENV_VAR]: encodeHookPolicy(hookPolicy),
145
+ };
146
+ assertAdmission();
147
+ const handle = this.options.processHost.spawn(this.options.binaryPath ?? "claude", args, {
148
+ cwd,
149
+ env,
150
+ });
151
+ this.handle = handle;
152
+ if (this.cancelled)
153
+ throw new Error("adapter startup cancelled");
154
+ handle.stdin.write(`${JSON.stringify({ type: "user", message: { role: "user", content: [{ type: "text", text: prompt }] } })}\n`);
117
155
  }
118
- else {
119
- args.push("--session-id", ctx.sessionId);
156
+ catch (error) {
157
+ await this.cleanup();
158
+ throw error;
120
159
  }
121
- const env = {
122
- ...allowlistedEnv(this.options.env),
123
- [HOOK_POLICY_ENV_VAR]: encodeHookPolicy(hookPolicy),
124
- };
125
- const handle = this.options.processHost.spawn(this.options.binaryPath ?? "claude", args, {
126
- cwd: this.options.worktreeRoot,
127
- env,
128
- });
129
- this.handle = handle;
130
- handle.stdin.write(`${JSON.stringify({ type: "user", message: { role: "user", content: [{ type: "text", text: prompt }] } })}\n`);
131
160
  }
132
161
  async *events() {
133
162
  const handle = this.requireHandle();
@@ -137,36 +166,41 @@ export class ClaudeCodeAdapter {
137
166
  exitInfo.signal = signal;
138
167
  });
139
168
  let sawTerminal = false;
140
- for await (const line of handle.stdout) {
141
- let message;
142
- try {
143
- const value = JSON.parse(line);
144
- if (!value || typeof value !== "object" || Array.isArray(value))
169
+ try {
170
+ for await (const line of handle.stdout) {
171
+ let message;
172
+ try {
173
+ const value = JSON.parse(line);
174
+ if (!value || typeof value !== "object" || Array.isArray(value))
175
+ continue;
176
+ message = value;
177
+ }
178
+ catch {
145
179
  continue;
146
- message = value;
147
- }
148
- catch {
149
- continue;
150
- }
151
- // A nested agent message is not evidence that the executor has finished.
152
- if (message.parent_tool_use_id != null)
153
- continue;
154
- let parsed = parseClaudeLine(line, this.options.worktreeRoot);
155
- if (!parsed)
156
- continue;
157
- if (parsed.eventType === "completed") {
158
- parsed = completionEvent(message.result, parsed.payload?.cost_usd);
180
+ }
181
+ // A nested agent message is not evidence that the executor has finished.
182
+ if (message.parent_tool_use_id != null)
183
+ continue;
184
+ let parsed = parseClaudeLine(line, this.options.worktreeRoot);
185
+ if (!parsed)
186
+ continue;
187
+ if (parsed.eventType === "completed") {
188
+ parsed = completionEvent(message.result, parsed.payload?.cost_usd);
189
+ }
190
+ if (isTerminalEventType(parsed.eventType))
191
+ sawTerminal = true;
192
+ yield parsed;
193
+ if (sawTerminal) {
194
+ handle.stdin.end();
195
+ return;
196
+ }
159
197
  }
160
- if (isTerminalEventType(parsed.eventType))
161
- sawTerminal = true;
162
- yield parsed;
163
- if (sawTerminal) {
164
- handle.stdin.end();
165
- return;
198
+ if (!sawTerminal) {
199
+ yield { eventType: "failed", summary: `claude process exited unexpectedly (code ${exitInfo.code ?? "unknown"})` };
166
200
  }
167
201
  }
168
- if (!sawTerminal) {
169
- yield { eventType: "failed", summary: `claude process exited unexpectedly (code ${exitInfo.code ?? "unknown"})` };
202
+ finally {
203
+ await this.cleanup();
170
204
  }
171
205
  }
172
206
  async command(command) {
@@ -184,29 +218,37 @@ export class ClaudeCodeAdapter {
184
218
  handle.stdin.write(`${JSON.stringify({ type: "user", message: { role: "user", content: [{ type: "text", text: JSON.stringify(payload) }] } })}\n`);
185
219
  }
186
220
  async cancel() {
187
- const handle = this.requireHandle();
188
- handle.stdin.end();
189
- handle.kill("SIGTERM");
221
+ this.cancelled = true;
222
+ await this.cleanup();
190
223
  }
191
- requireTrustedHookPath() {
192
- const hookPath = this.options.hookExecutablePath;
193
- if (!hookPath || !path.isAbsolute(hookPath)) {
194
- throw new Error("refusing to start: PreToolUse hook executable path must be an absolute path");
195
- }
196
- if (!/^[A-Za-z0-9._/:\\-]+$/.test(hookPath)) {
197
- throw new Error("refusing to start: PreToolUse hook must use a safe executable path without shell metacharacters");
224
+ async cleanup() {
225
+ const tools = this.tools;
226
+ const handle = this.handle;
227
+ this.tools = undefined;
228
+ this.handle = null;
229
+ try {
230
+ tools?.revoke();
198
231
  }
199
- const worktreeRoot = path.resolve(this.options.worktreeRoot);
200
- const rel = path.relative(worktreeRoot, path.resolve(hookPath));
201
- const isInsideWorktree = rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
202
- if (isInsideWorktree) {
203
- throw new Error("refusing to start: PreToolUse hook executable must live outside the assigned worktree");
204
- }
205
- const exists = this.options.hookExecutableExists ?? defaultPathExists;
206
- if (!exists(hookPath)) {
207
- throw new Error("refusing to start: PreToolUse hook executable was not found at the trusted path");
232
+ finally {
233
+ try {
234
+ handle?.stdin.end();
235
+ }
236
+ finally {
237
+ try {
238
+ handle?.kill("SIGKILL");
239
+ }
240
+ finally {
241
+ await tools?.close();
242
+ }
243
+ }
208
244
  }
209
- return hookPath;
245
+ }
246
+ /** Claude Code treats a hook it cannot run as no objection, so the hook runs through Node. */
247
+ requireTrustedHookCommand() {
248
+ return formatHookCommand({
249
+ nodeExecutablePath: this.options.hookNodeExecutablePath ?? process.execPath,
250
+ hookScriptPath: this.options.hookExecutablePath,
251
+ }, { worktreeRoot: this.options.worktreeRoot, exists: this.options.hookExecutableExists });
210
252
  }
211
253
  requireHandle() {
212
254
  if (!this.handle) {
@@ -1,64 +1,2 @@
1
- import { MAX_EVENT_SUMMARY_CHARS } from "../../protocol.js";
2
- const MAX_EVIDENCE_ITEMS = 20;
3
- const MAX_EVIDENCE_CHARS = 500;
4
- const MAX_REPORT_ENVELOPE_CHARS = 16_000;
5
- export const COMPLETION_REPORT_NOTICE = "Do the assigned work yourself using the available tools; do not delegate or start background work. " +
6
- "Do not report completed while waiting for results, tests, or another agent. " +
7
- "Your final response must be one JSON object, without Markdown fences: " +
8
- '{"status":"completed","report":"actual findings and results","evidence":["file path and observed finding, or command and actual result"],"pending_work":[]}. ' +
9
- `The report must be nonempty and at most ${MAX_EVENT_SUMMARY_CHARS} characters. ` +
10
- `Include 1-${MAX_EVIDENCE_ITEMS} nonempty evidence strings, each at most ${MAX_EVIDENCE_CHARS} characters. ` +
11
- "Address the assigned objective and each applicable acceptance criterion with observed evidence, not a plan or promise. " +
12
- "Only attest to executor work you observed; Gateway records transport, completion, and review receipts separately. " +
13
- 'If the task is unfinished, use "status":"blocked" and list the remaining work in "pending_work". ' +
14
- "An incomplete or missing report is a failed execution, never a successful investigation artifact.";
15
- function boundedText(value, max) {
16
- return typeof value === "string" && value.trim().length > 0 && value.length <= max;
17
- }
18
- /** Validate the full final result before truncation; never publish malformed/interim output. */
19
- export function completionEvent(result, costUsd) {
20
- const usage = typeof costUsd === "number" && Number.isFinite(costUsd) && costUsd >= 0
21
- ? { cost_usd: costUsd }
22
- : {};
23
- const failed = {
24
- eventType: "failed",
25
- summary: "completion report is missing, incomplete or invalid",
26
- payload: usage,
27
- };
28
- if (typeof result !== "string" || result.length > MAX_REPORT_ENVELOPE_CHARS)
29
- return failed;
30
- let report;
31
- try {
32
- report = JSON.parse(result);
33
- }
34
- catch {
35
- // Claude may wrap the final report in one Markdown fence after a short
36
- // explanation. Accept framing only; retain every completion/evidence check.
37
- if ((result.match(/```/g) ?? []).length !== 2)
38
- return failed;
39
- const framed = result.trim().match(/^(?:[\s\S]*?\n)?```(?:json)?[ \t]*\r?\n([\s\S]*?)\r?\n```$/);
40
- if (!framed)
41
- return failed;
42
- try {
43
- report = JSON.parse(framed[1]);
44
- }
45
- catch {
46
- return failed;
47
- }
48
- }
49
- if (!report || typeof report !== "object" || Array.isArray(report))
50
- return failed;
51
- const value = report;
52
- if (Object.keys(value).some((key) => !["status", "report", "evidence", "pending_work"].includes(key))
53
- || value.status !== "completed"
54
- || !boundedText(value.report, MAX_EVENT_SUMMARY_CHARS)
55
- || !Array.isArray(value.pending_work) || value.pending_work.length !== 0
56
- || !Array.isArray(value.evidence) || value.evidence.length < 1 || value.evidence.length > MAX_EVIDENCE_ITEMS
57
- || !value.evidence.every((item) => boundedText(item, MAX_EVIDENCE_CHARS)))
58
- return failed;
59
- return {
60
- eventType: "completed",
61
- summary: value.report.trim(),
62
- payload: { ...usage, investigation_summary: value.report.trim(), completion_evidence: value.evidence },
63
- };
64
- }
1
+ // Compatibility export: keep the established Claude import path stable.
2
+ export { COMPLETION_REPORT_NOTICE, completionEvent } from "../completion-report.js";
@@ -1,60 +1 @@
1
- import { COMPLETION_REPORT_NOTICE } from "./completion-report.js";
2
- const STOP_BEFORE_PUBLICATION_RULE = "Stop before publication. Never run git commit, git push, create a pull request, merge, " +
3
- "deploy, access production, send an external message, or write data outside this worktree.";
4
- const EVENT_SCHEMA_NOTICE = "Report progress only through the normalized event lifecycle (started, progress, file_changed, question, " +
5
- "test_started, test_result, budget_warning, checkpoint_ready, blocked, completed, failed). " +
6
- "Checkpoint your work with checkpoint_ready before stopping whenever it is safe to do so.";
7
- function requiredArray(value, label) {
8
- if (!Array.isArray(value)) {
9
- throw new Error(`task package is missing ${label}`);
10
- }
11
- return value.map((item) => String(item));
12
- }
13
- function record(value) {
14
- return value !== null && typeof value === "object" && !Array.isArray(value)
15
- ? value
16
- : undefined;
17
- }
18
- export function compilePrompt(taskPackage, options) {
19
- const contract = record(taskPackage.contract);
20
- if (!contract) {
21
- throw new Error("task package is missing a contract");
22
- }
23
- const agentExecution = record(taskPackage.agent_execution);
24
- if (!agentExecution) {
25
- throw new Error("task package is missing the implementation brief (agent_execution section)");
26
- }
27
- const brief = record(agentExecution.implementation_brief);
28
- if (!brief) {
29
- throw new Error("task package is missing the implementation brief");
30
- }
31
- const unresolvedQuestions = requiredArray(brief.unresolved_questions ?? [], "unresolved_questions");
32
- if (unresolvedQuestions.length > 0) {
33
- throw new Error(`task package has unresolved questions that must be resolved before dispatch: ${unresolvedQuestions.join("; ")}`);
34
- }
35
- const contractObjective = record(contract.objective);
36
- const objective = String(brief.objective ?? contractObjective?.summary ?? "");
37
- if (!objective) {
38
- throw new Error("task package is missing an objective");
39
- }
40
- const acceptanceCriteria = requiredArray(brief.acceptance_criteria ?? contractObjective?.acceptance_criteria ?? [], "acceptance_criteria");
41
- const prohibitedActions = requiredArray(agentExecution.prohibited_actions ?? [], "prohibited_actions");
42
- const requiredChanges = requiredArray(brief.required_changes ?? [], "required_changes");
43
- const invariants = requiredArray(brief.invariants ?? [], "invariants");
44
- const nonGoals = requiredArray(brief.non_goals ?? [], "non_goals");
45
- const requiredTests = requiredArray(brief.required_tests ?? [], "required_tests");
46
- const sections = [
47
- `# Bounded implementation brief: ${options.workItemLabel}`,
48
- `## Objective\n${objective}`,
49
- `## Required changes\n${requiredChanges.map((item) => `- ${item}`).join("\n")}`,
50
- `## Invariants\n${invariants.map((item) => `- ${item}`).join("\n")}`,
51
- `## Non-goals\n${nonGoals.map((item) => `- ${item}`).join("\n")}`,
52
- `## Acceptance criteria\n${acceptanceCriteria.map((item) => `- ${item}`).join("\n")}`,
53
- `## Required tests\n${requiredTests.map((item) => `- ${item}`).join("\n")}`,
54
- `## Prohibited actions\n${prohibitedActions.map((item) => `- ${item}`).join("\n")}`,
55
- `## Stop-before-publication rule\n${STOP_BEFORE_PUBLICATION_RULE}`,
56
- `## Event and checkpoint obligations\n${EVENT_SCHEMA_NOTICE}`,
57
- `## Final completion report\n${COMPLETION_REPORT_NOTICE}`,
58
- ];
59
- return sections.join("\n\n");
60
- }
1
+ export { compilePrompt } from "../prompt-compiler.js";
@@ -0,0 +1,72 @@
1
+ import path from 'node:path';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { mkdir, realpath, rm } from 'node:fs/promises';
4
+ import { writeFileSync } from 'node:fs';
5
+ import { NodeProcessGroupHost } from '../../process/node-process-group-host.js';
6
+ import { JobToolSession } from '../../safety/job-tool-session.js';
7
+ import { jobToolPolicyOptions } from '../../safety/job-tool-policy.js';
8
+ import { requireJobSecurity } from '../../safety/job-security-policy.js';
9
+ const inside = (root, file) => file === root || file.startsWith(root + path.sep);
10
+ /** Wiring only. No release verifier or admission certificate exists here. */
11
+ export function createClaudeCodeAdapterOptions(input) {
12
+ const checkout = path.resolve(input.worktreePath), state = path.resolve(input.stateRoot);
13
+ if (inside(checkout, state))
14
+ throw new Error('Claude job controls must be outside the checkout');
15
+ const control = path.join(state, 'claude-jobs', randomUUID());
16
+ const home = path.join(control, 'home'), cwd = path.join(control, 'cwd'), config = path.join(control, 'mcp.json');
17
+ const env = {};
18
+ for (const key of ['PATH', 'LANG', 'LC_ALL', 'USER', 'LOGNAME'])
19
+ if (input.env[key] !== undefined)
20
+ env[key] = input.env[key];
21
+ env.HOME = home;
22
+ env.TMPDIR = path.join(control, 'tmp'); // Claude owns credential access. Preserve the account location by path only;
23
+ // never read/copy its files or send this environment to scoped workers.
24
+ // https://code.claude.com/docs/en/authentication#log-in-with-multiple-accounts
25
+ env.CLAUDE_CONFIG_DIR = input.env.CLAUDE_CONFIG_DIR ?? (input.env.HOME ? path.join(input.env.HOME, '.claude') : undefined);
26
+ if (env.CLAUDE_CONFIG_DIR && !path.isAbsolute(env.CLAUDE_CONFIG_DIR))
27
+ throw new Error('Claude account configuration path must be absolute');
28
+ return {
29
+ processHost: new NodeProcessGroupHost(), worktreeRoot: checkout, jobId: input.jobId, securityPolicy: input.securityPolicy,
30
+ cwd, env, binaryPath: input.binaryPath, hookExecutablePath: input.hookExecutablePath, hookNodeExecutablePath: input.hookNodeExecutablePath,
31
+ maxBudgetUsd: input.maxBudgetUsd ?? 1, workItemLabel: input.workItemLabel ?? input.jobId,
32
+ registeredReadTools: [], writeMcpConfig: () => { throw new Error('Gateway relay is not a model tool endpoint'); },
33
+ createTools: async (ctx, pkg, approved) => {
34
+ await mkdir(state, { recursive: true, mode: 0o700 });
35
+ const realState = await realpath(state), realCheckout = await realpath(checkout);
36
+ if (realState !== state || inside(realCheckout, realState))
37
+ throw new Error('Claude job controls must be outside the checkout');
38
+ await mkdir(control, { recursive: true, mode: 0o700 });
39
+ for (const directory of [home, cwd, env.TMPDIR])
40
+ await mkdir(directory, { mode: 0o700 });
41
+ let active = true;
42
+ const tools = new JobToolSession({
43
+ policyOptions: jobToolPolicyOptions(checkout, pkg, approved, input.commandRegistry ?? [], { runtimeAdapter: 'claude_code_cli', runtimeAdapterVersion: '1' }),
44
+ binding: () => {
45
+ const binding = input.nativeBinding?.();
46
+ return binding && binding.jobId === input.jobId && binding.generation === input.securityPolicy?.pin.generation ? binding : null;
47
+ },
48
+ active: () => {
49
+ if (!active)
50
+ return false;
51
+ try {
52
+ requireJobSecurity(input.securityPolicy, { adapterId: 'claude_code_cli', adapterVersion: '1', jobId: input.jobId, modelId: ctx.resolvedModelId });
53
+ return true;
54
+ }
55
+ catch {
56
+ return false;
57
+ }
58
+ },
59
+ });
60
+ return { start: () => tools.start(), revoke: () => { active = false; tools.revoke(); }, close: async () => { active = false; try {
61
+ await tools.close();
62
+ }
63
+ finally {
64
+ await rm(control, { recursive: true, force: true });
65
+ } } };
66
+ },
67
+ writeLocalMcpConfig: endpoint => {
68
+ writeFileSync(config, JSON.stringify({ mcpServers: { ai_local: { type: 'http', url: endpoint.url, headers: { Authorization: `Bearer ${endpoint.token}` } } } }), { mode: 0o600, flag: 'wx' });
69
+ return config;
70
+ },
71
+ };
72
+ }