mtech-ai-local-runner 0.1.8 → 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.
- package/README.md +45 -4
- package/dist/src/adapters/claude-code/adapter.js +157 -98
- package/dist/src/adapters/claude-code/completion-report.js +2 -64
- package/dist/src/adapters/claude-code/prompt-compiler.js +1 -60
- package/dist/src/adapters/claude-code/runtime-options.js +72 -0
- package/dist/src/adapters/codex/adapter.js +170 -0
- package/dist/src/adapters/codex/app-server-events.js +84 -0
- package/dist/src/adapters/codex/compatibility.js +45 -0
- package/dist/src/adapters/codex/config-builder.js +40 -0
- package/dist/src/adapters/codex/host-admission.js +3 -0
- package/dist/src/adapters/codex/inspection-rpc.js +48 -0
- package/dist/src/adapters/codex/job-rpc.js +189 -0
- package/dist/src/adapters/codex/native-policy.js +112 -0
- package/dist/src/adapters/codex/native-proof.js +14 -0
- package/dist/src/adapters/codex/native-session.js +375 -0
- package/dist/src/adapters/codex/plugin.js +26 -0
- package/dist/src/adapters/codex/pre-model-gate.js +112 -0
- package/dist/src/adapters/codex/prompt-compiler.js +13 -0
- package/dist/src/adapters/codex/runtime-options.js +163 -0
- package/dist/src/adapters/codex/scratch.js +28 -0
- package/dist/src/adapters/codex/stdio-rpc.js +113 -0
- package/dist/src/adapters/codex/stream-parser.js +85 -0
- package/dist/src/adapters/codex/supported-admission.js +97 -0
- package/dist/src/adapters/codex/tool-dispatcher.js +20 -0
- package/dist/src/adapters/completion-report.js +64 -0
- package/dist/src/adapters/discovery.js +76 -0
- package/dist/src/adapters/prompt-compiler.js +60 -0
- package/dist/src/adapters/registry.js +10 -5
- package/dist/src/cli.js +50 -58
- package/dist/src/commands/doctor.js +7 -1
- package/dist/src/commands/setup.js +3 -0
- package/dist/src/daemon/execution-coordinator.js +19 -2
- package/dist/src/daemon/runtime.js +34 -41
- package/dist/src/process/node-process-group-host.js +74 -0
- package/dist/src/safety/cli-host-admission.js +74 -0
- package/dist/src/safety/cli-security-modes.js +4 -0
- package/dist/src/safety/codex-production-host.js +267 -0
- package/dist/src/safety/codex-supported-releases.js +1 -0
- package/dist/src/safety/hook-cli.js +21 -7
- package/dist/src/safety/hook-policy-builder.js +4 -11
- package/dist/src/safety/job-security-policy.js +62 -0
- package/dist/src/safety/job-security-receipt.js +20 -0
- package/dist/src/safety/job-security-session.js +133 -0
- package/dist/src/safety/job-tool-dispatcher.js +52 -0
- package/dist/src/safety/job-tool-policy.js +61 -0
- package/dist/src/safety/job-tool-session.js +82 -0
- package/dist/src/safety/local-tool-policy.js +101 -0
- package/dist/src/safety/local-tool-server.js +174 -0
- package/dist/src/safety/local-tool-worker.js +57 -0
- package/dist/src/safety/pre-tool-use-hook.js +3 -39
- package/dist/src/safety/registered-command-integrity.js +52 -0
- package/dist/src/safety/security-requirements.js +38 -0
- package/dist/src/safety/subprocess-sandbox.js +62 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,12 +10,53 @@ 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.
|
|
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
|
|
14
55
|
|
|
15
56
|
- Report safe preparation stage/tool/error categories and settle known failures instead of silently reclaiming the same job forever.
|
|
16
57
|
- Bound preparation to 15 minutes, retain terminal receipts across connection failures, and halt on unknown binding outcomes until reconciled.
|
|
17
58
|
- Pair with Gateway cancellation propagation and three-delivery preparation bounds. Actual approved unbounded employee-local Claude execution remains unbounded.
|
|
18
|
-
-
|
|
59
|
+
- npm publication and daemon upgrade are separate actions. Keep a stopped runner stopped until its cancelled job is confirmed terminal on Gateway.
|
|
19
60
|
|
|
20
61
|
## 0.1.7
|
|
21
62
|
|
|
@@ -46,8 +87,8 @@ The relay binds only to loopback. Local capabilities are stored in private mode-
|
|
|
46
87
|
Existing employees can upgrade their installed daemon with:
|
|
47
88
|
|
|
48
89
|
```bash
|
|
49
|
-
npx mtech-ai-local-runner
|
|
50
|
-
npx mtech-ai-local-runner
|
|
90
|
+
npx mtech-ai-local-runner setup
|
|
91
|
+
npx mtech-ai-local-runner doctor
|
|
51
92
|
```
|
|
52
93
|
|
|
53
94
|
### Cancellation and preparation failures
|
|
@@ -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
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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 (
|
|
161
|
-
|
|
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
|
-
|
|
169
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
2
|
-
|
|
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
|
-
|
|
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
|
+
}
|