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.
- package/README.md +56 -4
- package/dist/src/adapters/claude-code/adapter.js +162 -120
- 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/account-sources.js +113 -0
- package/dist/src/adapters/codex/adapter.js +217 -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 +43 -0
- package/dist/src/adapters/codex/exec-invocation.js +62 -0
- package/dist/src/adapters/codex/exec-launch-host.js +209 -0
- package/dist/src/adapters/codex/exec-observer.js +83 -0
- package/dist/src/adapters/codex/exec-preflight.js +41 -0
- package/dist/src/adapters/codex/exec-release-policy.js +44 -0
- package/dist/src/adapters/codex/exec-runtime-options.js +113 -0
- package/dist/src/adapters/codex/exec-tool-gate.js +81 -0
- package/dist/src/adapters/codex/global-instructions.js +32 -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/manual-policy-scope.js +67 -0
- package/dist/src/adapters/codex/native-policy.js +128 -0
- package/dist/src/adapters/codex/native-proof.js +14 -0
- package/dist/src/adapters/codex/native-session.js +356 -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/codex/unmanaged-scope.js +85 -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/auth/token-provider.js +2 -0
- package/dist/src/cli.js +98 -58
- package/dist/src/commands/doctor.js +7 -1
- package/dist/src/commands/setup.js +3 -0
- package/dist/src/config.js +9 -2
- package/dist/src/daemon/execution-coordinator.js +19 -2
- package/dist/src/daemon/manual-codex.js +82 -0
- package/dist/src/daemon/runtime.js +43 -43
- package/dist/src/gateway-client.js +3 -2
- 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-exec-production-host.js +249 -0
- package/dist/src/safety/codex-exec-releases.js +3 -0
- package/dist/src/safety/codex-manual-controller.js +72 -0
- package/dist/src/safety/codex-manual-profile-query.js +30 -0
- package/dist/src/safety/codex-manual-registered.js +30 -0
- package/dist/src/safety/codex-native-observation.js +68 -0
- package/dist/src/safety/codex-native-process.js +59 -0
- package/dist/src/safety/codex-production-host.js +267 -0
- package/dist/src/safety/codex-reviewed-manual-release.js +37 -0
- package/dist/src/safety/codex-runtime-host.js +36 -0
- package/dist/src/safety/codex-supported-releases.js +1 -0
- package/dist/src/safety/hook-cli.js +39 -9
- package/dist/src/safety/hook-command.js +156 -0
- package/dist/src/safety/hook-policy-builder.js +6 -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 +211 -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 +49 -0
- package/dist/src/safety/stable-file-read.js +33 -0
- package/dist/src/safety/subprocess-sandbox.js +62 -0
- package/native/bin/codex-observe-darwin-arm64 +0 -0
- 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.
|
|
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
|
-
-
|
|
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
|
|
50
|
-
npx mtech-ai-local-runner
|
|
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 {
|
|
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 {
|
|
8
|
-
import { HOOK_POLICY_ENV_VAR } from "../../safety/hook-
|
|
9
|
-
|
|
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
|
|
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
|
|
91
|
+
const hookCommand = this.requireTrustedHookCommand();
|
|
72
92
|
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
|
-
|
|
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
|
-
|
|
119
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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 (
|
|
161
|
-
|
|
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
|
-
|
|
169
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
handle.kill("SIGTERM");
|
|
221
|
+
this.cancelled = true;
|
|
222
|
+
await this.cleanup();
|
|
190
223
|
}
|
|
191
|
-
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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, 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
|
+
}
|