mtech-ai-local-runner 0.1.7 → 0.1.9

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 (56) hide show
  1. package/README.md +77 -2
  2. package/dist/src/adapters/claude-code/adapter.js +157 -98
  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/adapter.js +170 -0
  7. package/dist/src/adapters/codex/app-server-events.js +84 -0
  8. package/dist/src/adapters/codex/compatibility.js +45 -0
  9. package/dist/src/adapters/codex/config-builder.js +40 -0
  10. package/dist/src/adapters/codex/host-admission.js +3 -0
  11. package/dist/src/adapters/codex/inspection-rpc.js +48 -0
  12. package/dist/src/adapters/codex/job-rpc.js +189 -0
  13. package/dist/src/adapters/codex/native-policy.js +112 -0
  14. package/dist/src/adapters/codex/native-proof.js +14 -0
  15. package/dist/src/adapters/codex/native-session.js +375 -0
  16. package/dist/src/adapters/codex/plugin.js +26 -0
  17. package/dist/src/adapters/codex/pre-model-gate.js +112 -0
  18. package/dist/src/adapters/codex/prompt-compiler.js +13 -0
  19. package/dist/src/adapters/codex/runtime-options.js +163 -0
  20. package/dist/src/adapters/codex/scratch.js +28 -0
  21. package/dist/src/adapters/codex/stdio-rpc.js +113 -0
  22. package/dist/src/adapters/codex/stream-parser.js +85 -0
  23. package/dist/src/adapters/codex/supported-admission.js +97 -0
  24. package/dist/src/adapters/codex/tool-dispatcher.js +20 -0
  25. package/dist/src/adapters/completion-report.js +64 -0
  26. package/dist/src/adapters/discovery.js +76 -0
  27. package/dist/src/adapters/prompt-compiler.js +60 -0
  28. package/dist/src/adapters/registry.js +10 -5
  29. package/dist/src/cli.js +50 -58
  30. package/dist/src/commands/doctor.js +7 -1
  31. package/dist/src/commands/setup.js +3 -0
  32. package/dist/src/daemon/execution-coordinator.js +91 -19
  33. package/dist/src/daemon/job-poller.js +10 -1
  34. package/dist/src/daemon/runtime.js +55 -41
  35. package/dist/src/gateway-client.js +19 -3
  36. package/dist/src/process/node-process-group-host.js +74 -0
  37. package/dist/src/safety/cli-host-admission.js +74 -0
  38. package/dist/src/safety/cli-security-modes.js +4 -0
  39. package/dist/src/safety/codex-production-host.js +267 -0
  40. package/dist/src/safety/codex-supported-releases.js +1 -0
  41. package/dist/src/safety/hook-cli.js +21 -7
  42. package/dist/src/safety/hook-policy-builder.js +4 -11
  43. package/dist/src/safety/job-security-policy.js +62 -0
  44. package/dist/src/safety/job-security-receipt.js +20 -0
  45. package/dist/src/safety/job-security-session.js +133 -0
  46. package/dist/src/safety/job-tool-dispatcher.js +52 -0
  47. package/dist/src/safety/job-tool-policy.js +61 -0
  48. package/dist/src/safety/job-tool-session.js +82 -0
  49. package/dist/src/safety/local-tool-policy.js +101 -0
  50. package/dist/src/safety/local-tool-server.js +174 -0
  51. package/dist/src/safety/local-tool-worker.js +57 -0
  52. package/dist/src/safety/pre-tool-use-hook.js +3 -39
  53. package/dist/src/safety/registered-command-integrity.js +52 -0
  54. package/dist/src/safety/security-requirements.js +38 -0
  55. package/dist/src/safety/subprocess-sandbox.js +62 -0
  56. package/package.json +1 -1
package/README.md CHANGED
@@ -10,6 +10,54 @@ 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.9: gated Codex foundation and shared CLI security
14
+
15
+ This package includes the production Codex host/probe and common authenticated
16
+ four-tool policy, command/dependency pins, owned cancellation, retained-session
17
+ checks and security receipts. The source-reviewed Codex release registry is empty:
18
+ Codex cannot advertise or execute until an exact binary, model, policy and release
19
+ manifest have been genuinely reviewed. Do not enable a Gateway flag to bypass it.
20
+
21
+ **Upgrade limitation:** this package also closes the Claude adapter until its
22
+ reviewed native evidence is available. Do not replace a working 0.1.8 daemon as
23
+ part of the Codex-disabled Gateway delivery. Gateway retains the legacy Claude
24
+ prepare/launch and approval payloads unless an explicit Claude security manifest
25
+ is configured; Codex always requires the new security descriptor. Publication is
26
+ separate from an authorized daemon upgrade or actual account/model acceptance.
27
+
28
+ The Codex probe only reads the original supported file-backend login status and
29
+ never runs vendor login/status, refreshes tokens, writes credentials or switches
30
+ backends. Keychain-only/unknown backends remain unavailable rather than being
31
+ migrated. Native CLI startup can still perform vendor-managed refresh; a no-model
32
+ inspection is not a no-refresh guarantee. Keep the existing login and obtain the
33
+ separate account/activation approval before actual CLI startup.
34
+
35
+ The supported policy mode acknowledges vendor runtime/authentication/extension/
36
+ IPC/network trust; it does not claim total broker-only isolation. Synthetic test
37
+ receipts never authorize production. The repository report `docs/ai-ops-gateway/codex-production-host-validation.md`
38
+ records the tested scope and remaining rollout gates.
39
+
40
+ The intended employee request after rollout is: “Use my local Codex for Billing.
41
+ Inspect the payment entry points without editing files, and return the findings
42
+ and verification here.” Gateway OAuth signs the employee into Gateway; a separate
43
+ `codex login` using ChatGPT authorizes Codex. An OpenAI API key is not a fallback.
44
+ If both executors are usable, select one before approval; changing the executor
45
+ or server-resolved model requires a new launch approval.
46
+
47
+ Source operations use authenticated job-only tools. Registered test programs need
48
+ exact executable/argv and explicit dependency-file read grants; source writes,
49
+ network, ambient credentials and child process creation are denied to those test
50
+ programs. Broad npm/project test runners are not automatically supported. Missing
51
+ grants produce a visible blocked result. Actual employee-local task duration and
52
+ count remain unbounded; preparation retains the 0.1.8 limits.
53
+
54
+ ## 0.1.8
55
+
56
+ - Report safe preparation stage/tool/error categories and settle known failures instead of silently reclaiming the same job forever.
57
+ - Bound preparation to 15 minutes, retain terminal receipts across connection failures, and halt on unknown binding outcomes until reconciled.
58
+ - Pair with Gateway cancellation propagation and three-delivery preparation bounds. Actual approved unbounded employee-local Claude execution remains unbounded.
59
+ - npm publication and daemon upgrade are separate actions. Keep a stopped runner stopped until its cancelled job is confirmed terminal on Gateway.
60
+
13
61
  ## 0.1.7
14
62
 
15
63
  - Accept one fenced final JSON report after an explanatory preamble while preserving status, evidence, length and pending-work validation. Reject ambiguous multiple code blocks.
@@ -39,6 +87,33 @@ The relay binds only to loopback. Local capabilities are stored in private mode-
39
87
  Existing employees can upgrade their installed daemon with:
40
88
 
41
89
  ```bash
42
- npx mtech-ai-local-runner@0.1.7 setup
43
- npx mtech-ai-local-runner@0.1.7 doctor
90
+ npx mtech-ai-local-runner setup
91
+ npx mtech-ai-local-runner doctor
44
92
  ```
93
+
94
+ ### Cancellation and preparation failures
95
+
96
+ Cancelling a Work Loop also cancels its non-terminal local jobs. Repeating the
97
+ cancellation repairs jobs left by an earlier Gateway version; terminal results
98
+ are preserved. Claim/reclaim and lease-gated launch checks reject cancelled
99
+ Loops. An active adapter stops when the next lease heartbeat fails; cancellation
100
+ is not an immediate process-kill acknowledgement.
101
+
102
+ Before binding, the Gateway permits at most three queue deliveries and a
103
+ 15-minute preparation window from the first claim. The daemon also bounds its
104
+ workspace/probe/shadow/binding preparation to 15 minutes. A known preparation
105
+ failure is reported as `blocked` immediately, with a safe stage, tool name when
106
+ available, and error category. Tokens, grants, arguments and raw error responses
107
+ are excluded from these diagnostics. Unacknowledged terminal receipts remain in
108
+ private state for reconciliation before polling continues.
109
+
110
+ A binding request whose outcome is unknown is retained as `binding_uncertain`;
111
+ the daemon stops polling and requires authoritative reconciliation of the
112
+ recorded job/generation and deterministic binding reference before restart.
113
+ It neither repeats the binding request nor assumes the server did not create it.
114
+ Do not delete that state to force another execution.
115
+
116
+ These preparation controls end at binding and do not impose time, turn, token
117
+ or cost termination limits on an approved unbounded employee-local Claude run.
118
+ Existing generation fencing, scope restrictions, cancellation and audit remain
119
+ in force.
@@ -1,14 +1,17 @@
1
1
  import path from "node:path";
2
+ import { requireSecurityRequirements } from '../../safety/security-requirements.js';
3
+ import { createJobSecurityReceipt } from '../../safety/job-security-receipt.js';
2
4
  import { existsSync } from "node:fs";
3
5
  import { isTerminalEventType } from "../../protocol.js";
4
6
  import { parseClaudeLine } from "./stream-parser.js";
5
7
  import { compilePrompt } from "./prompt-compiler.js";
6
8
  import { completionEvent } from "./completion-report.js";
9
+ import { requireJobSecurity } from "../../safety/job-security-policy.js";
7
10
  import { buildHookPolicy, encodeHookPolicy } from "../../safety/hook-policy-builder.js";
8
11
  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"];
12
+ const ALLOWED_ENV_KEYS = ["PATH", "HOME", "USER", "LOGNAME", "LANG", "LC_ALL", "TMPDIR", "NODE_ENV", "CLAUDE_CONFIG_DIR"];
10
13
  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"];
14
+ const LOCAL_NAMES = ["local_read", "local_search", "local_edit", "local_test"].map(name => `mcp__ai_local__${name}`);
12
15
  function defaultPathExists(p) {
13
16
  return existsSync(p);
14
17
  }
@@ -38,18 +41,14 @@ function isUnboundedLocalExecution(taskPackage) {
38
41
  && execution?.runtime_adapter === "claude_code_cli"
39
42
  && execution?.limits_mode === "unbounded";
40
43
  }
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
44
  export class ClaudeCodeAdapter {
51
45
  options;
52
46
  handle = null;
47
+ tools;
48
+ cancelled = false;
49
+ started = false;
50
+ receipt;
51
+ securityReceipt() { return this.receipt ? structuredClone(this.receipt) : undefined; }
53
52
  constructor(options) {
54
53
  this.options = options;
55
54
  }
@@ -58,6 +57,31 @@ export class ClaudeCodeAdapter {
58
57
  if (!resolvedModelId) {
59
58
  throw new Error("refusing to start: no Gateway-resolved model ID was supplied");
60
59
  }
60
+ if (this.started)
61
+ throw new Error("adapter has already been started");
62
+ this.started = true;
63
+ const generation = ctx.generation;
64
+ if (!Number.isSafeInteger(generation) || generation === undefined || generation < 1)
65
+ throw new Error("JOB_SECURITY_REJECTED");
66
+ const assertAdmission = () => {
67
+ requireJobSecurity(this.options.securityPolicy, { adapterId: "claude_code_cli", adapterVersion: "1", jobId: this.options.jobId ?? ctx.bindingId, modelId: resolvedModelId, generation });
68
+ if (ctx.approvedTaskPackage?.resolved_provider !== "anthropic" || ctx.approvedTaskPackage?.resolved_model_id !== resolvedModelId
69
+ || ctx.approvedTaskPackage?.repository?.base_commit_sha !== this.options.securityPolicy.pin.sourceSha)
70
+ throw new Error("SECURITY_REQUIREMENTS_REJECTED");
71
+ requireSecurityRequirements(ctx.approvedTaskPackage?.security_requirements, { adapterId: "claude_code_cli", adapterVersion: "1", requirementsSha256: this.options.securityPolicy.pin.requirementsSha256 });
72
+ };
73
+ assertAdmission();
74
+ if (!ctx.securityBinding)
75
+ throw new Error('SECURITY_REQUIREMENTS_REJECTED');
76
+ 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 });
77
+ const pkg = ctx.taskPackage;
78
+ const repos = pkg.repositories;
79
+ if (!Array.isArray(repos) || repos.length !== 1 || repos[0]?.base_commit_sha !== this.options.securityPolicy.pin.sourceSha)
80
+ throw new Error("JOB_SECURITY_REJECTED");
81
+ const cwd = this.options.cwd;
82
+ const rel = cwd ? path.relative(path.resolve(this.options.worktreeRoot), path.resolve(cwd)) : "";
83
+ if (!cwd || !path.isAbsolute(cwd) || rel === "" || (!rel.startsWith(".." + path.sep) && rel !== ".." && !path.isAbsolute(rel)))
84
+ throw new Error("trusted cwd must live outside the assigned worktree");
61
85
  const unbounded = isUnboundedLocalExecution(ctx.taskPackage);
62
86
  if (!unbounded && (!Number.isFinite(this.options.maxBudgetUsd) || this.options.maxBudgetUsd <= 0)) {
63
87
  throw new Error("refusing to start: no positive remaining contract budget was supplied");
@@ -70,64 +94,73 @@ export class ClaudeCodeAdapter {
70
94
  }
71
95
  const hookPath = this.requireTrustedHookPath();
72
96
  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);
117
- }
118
- else {
119
- args.push("--session-id", ctx.sessionId);
120
- }
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`);
97
+ try {
98
+ if (!this.options.createTools || !this.options.writeLocalMcpConfig)
99
+ throw new Error("job-local tool setup is required");
100
+ this.tools = await this.options.createTools(ctx, pkg, ctx.approvedTaskPackage ?? {});
101
+ if (this.cancelled)
102
+ throw new Error("adapter startup cancelled");
103
+ const endpoint = await this.tools.start();
104
+ if (this.cancelled)
105
+ throw new Error("adapter startup cancelled");
106
+ const mcpConfigPath = this.options.writeLocalMcpConfig(endpoint);
107
+ const maxBudgetUsd = unbounded
108
+ ? null
109
+ : Math.min(this.options.maxBudgetUsd, contractBudget(ctx.taskPackage));
110
+ const hookPolicy = buildHookPolicy({
111
+ taskPackage: ctx.taskPackage,
112
+ worktreeRoot: this.options.worktreeRoot,
113
+ registeredReadTools: this.options.registeredReadTools,
114
+ generatedPaths: [mcpConfigPath],
115
+ });
116
+ const settings = {
117
+ hooks: {
118
+ PreToolUse: [{ matcher: "*", hooks: [{ type: "command", command: hookPath, onFailure: "block" }] }],
119
+ },
120
+ };
121
+ const args = [
122
+ "--print",
123
+ "--input-format", "stream-json",
124
+ "--output-format", "stream-json",
125
+ "--verbose",
126
+ "--include-partial-messages",
127
+ "--no-chrome",
128
+ "--permission-mode", "default",
129
+ "--model", resolvedModelId,
130
+ ...(maxBudgetUsd === null ? [] : ["--max-budget-usd", String(maxBudgetUsd)]),
131
+ "--name", this.options.workItemLabel.trim().slice(0, 80) || `work-item-${ctx.bindingId.slice(0, 8)}`,
132
+ "--strict-mcp-config",
133
+ "--mcp-config", mcpConfigPath,
134
+ "--settings", JSON.stringify(settings),
135
+ "--setting-sources", "",
136
+ "--disable-slash-commands",
137
+ "--tools", "",
138
+ "--allowedTools", LOCAL_NAMES.join(","),
139
+ ];
140
+ if (ctx.resumeSessionId) {
141
+ args.push("--resume", ctx.resumeSessionId);
142
+ }
143
+ else {
144
+ args.push("--session-id", ctx.sessionId);
145
+ }
146
+ const env = {
147
+ ...allowlistedEnv(this.options.env),
148
+ [HOOK_POLICY_ENV_VAR]: encodeHookPolicy(hookPolicy),
149
+ };
150
+ assertAdmission();
151
+ const handle = this.options.processHost.spawn(this.options.binaryPath ?? "claude", args, {
152
+ cwd,
153
+ env,
154
+ });
155
+ this.handle = handle;
156
+ if (this.cancelled)
157
+ throw new Error("adapter startup cancelled");
158
+ handle.stdin.write(`${JSON.stringify({ type: "user", message: { role: "user", content: [{ type: "text", text: prompt }] } })}\n`);
159
+ }
160
+ catch (error) {
161
+ await this.cleanup();
162
+ throw error;
163
+ }
131
164
  }
132
165
  async *events() {
133
166
  const handle = this.requireHandle();
@@ -137,36 +170,41 @@ export class ClaudeCodeAdapter {
137
170
  exitInfo.signal = signal;
138
171
  });
139
172
  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))
173
+ try {
174
+ for await (const line of handle.stdout) {
175
+ let message;
176
+ try {
177
+ const value = JSON.parse(line);
178
+ if (!value || typeof value !== "object" || Array.isArray(value))
179
+ continue;
180
+ message = value;
181
+ }
182
+ catch {
145
183
  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);
184
+ }
185
+ // A nested agent message is not evidence that the executor has finished.
186
+ if (message.parent_tool_use_id != null)
187
+ continue;
188
+ let parsed = parseClaudeLine(line, this.options.worktreeRoot);
189
+ if (!parsed)
190
+ continue;
191
+ if (parsed.eventType === "completed") {
192
+ parsed = completionEvent(message.result, parsed.payload?.cost_usd);
193
+ }
194
+ if (isTerminalEventType(parsed.eventType))
195
+ sawTerminal = true;
196
+ yield parsed;
197
+ if (sawTerminal) {
198
+ handle.stdin.end();
199
+ return;
200
+ }
159
201
  }
160
- if (isTerminalEventType(parsed.eventType))
161
- sawTerminal = true;
162
- yield parsed;
163
- if (sawTerminal) {
164
- handle.stdin.end();
165
- return;
202
+ if (!sawTerminal) {
203
+ yield { eventType: "failed", summary: `claude process exited unexpectedly (code ${exitInfo.code ?? "unknown"})` };
166
204
  }
167
205
  }
168
- if (!sawTerminal) {
169
- yield { eventType: "failed", summary: `claude process exited unexpectedly (code ${exitInfo.code ?? "unknown"})` };
206
+ finally {
207
+ await this.cleanup();
170
208
  }
171
209
  }
172
210
  async command(command) {
@@ -184,9 +222,30 @@ export class ClaudeCodeAdapter {
184
222
  handle.stdin.write(`${JSON.stringify({ type: "user", message: { role: "user", content: [{ type: "text", text: JSON.stringify(payload) }] } })}\n`);
185
223
  }
186
224
  async cancel() {
187
- const handle = this.requireHandle();
188
- handle.stdin.end();
189
- handle.kill("SIGTERM");
225
+ this.cancelled = true;
226
+ await this.cleanup();
227
+ }
228
+ async cleanup() {
229
+ const tools = this.tools;
230
+ const handle = this.handle;
231
+ this.tools = undefined;
232
+ this.handle = null;
233
+ try {
234
+ tools?.revoke();
235
+ }
236
+ finally {
237
+ try {
238
+ handle?.stdin.end();
239
+ }
240
+ finally {
241
+ try {
242
+ handle?.kill("SIGKILL");
243
+ }
244
+ finally {
245
+ await tools?.close();
246
+ }
247
+ }
248
+ }
190
249
  }
191
250
  requireTrustedHookPath() {
192
251
  const hookPath = this.options.hookExecutablePath;
@@ -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,
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
+ }