mtech-ai-local-runner 0.1.45 → 0.1.47
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 +34 -0
- package/dist/src/adapters/claude-code/adapter.js +57 -36
- package/dist/src/adapters/claude-code/runtime-options.js +114 -33
- package/dist/src/adapters/discovery.js +39 -7
- package/dist/src/adapters/status.js +9 -0
- package/dist/src/cli.js +2 -1
- package/dist/src/config.js +22 -2
- package/dist/src/daemon/runtime.js +29 -6
- package/dist/src/safety/claude-binary-identity.js +81 -0
- package/dist/src/safety/hook-cli.js +0 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,3 +1,27 @@
|
|
|
1
|
+
## 0.1.47: Claude security host wiring remains gated
|
|
2
|
+
|
|
3
|
+
Claude now shares the job security session and checks a live opaque executable
|
|
4
|
+
identity before launch. Account/control overlaps and aliases fail before control
|
|
5
|
+
creation; preparation failure and cancellation revoke authority. A trusted
|
|
6
|
+
host/probe/identity tuple is an in-process composition seam, not an environment
|
|
7
|
+
switch or a receipt loaded from disk. Fake-data tests do not enable production:
|
|
8
|
+
exact-release/native-policy, managed-input and credential/IPC/network evidence
|
|
9
|
+
and production verifier registration remain outstanding. Existing login, settings
|
|
10
|
+
and executable path are preserved. This release neither upgrades nor starts an
|
|
11
|
+
installed daemon. See `docs/ai-ops-gateway/claude-execution-admission-remaining.md`
|
|
12
|
+
in the Gateway repository for the consolidated verification boundary.
|
|
13
|
+
|
|
14
|
+
## 0.1.46: login and execution are separate
|
|
15
|
+
|
|
16
|
+
A locally logged-in account may still be blocked by security admission. New
|
|
17
|
+
`login_status`, `execution_status` and `execution_reason_codes` show those facts
|
|
18
|
+
separately; the legacy `authenticated` field remains an eligibility conjunction.
|
|
19
|
+
Malformed/unreadable Claude auth output is unknown, not a request to log in again.
|
|
20
|
+
Claude also rechecks task source identity after MCP preparation before spawning.
|
|
21
|
+
This release does not enable Claude execution, update an installed daemon or
|
|
22
|
+
change existing account settings. See the Gateway repository's
|
|
23
|
+
`docs/ai-ops-gateway/claude-execution-admission-remaining.md` for remaining gates.
|
|
24
|
+
|
|
1
25
|
## 0.1.45: pin official Codex alpha.11 outside the app
|
|
2
26
|
|
|
3
27
|
Manual and daemon Codex registration now require the reviewed official standalone
|
|
@@ -640,3 +664,13 @@ advertises `local_fix_gateway_publish_v1`. Publishing/installation/DEV activatio
|
|
|
640
664
|
and an exact approved Fix contract/launch are still required before real use.
|
|
641
665
|
An unresolved canonical PR approval or publication receipt pauses delivery; do
|
|
642
666
|
not rerun the model or delete recovery state to force a PR.
|
|
667
|
+
|
|
668
|
+
### Login and execution diagnostics
|
|
669
|
+
|
|
670
|
+
`login_status` reports local account evidence (`authenticated`, `unauthenticated`,
|
|
671
|
+
`unknown`); it does not certify remote token validity. `execution_status` reports
|
|
672
|
+
`ready` or `blocked`, with bounded `execution_reason_codes`. For compatibility,
|
|
673
|
+
the older `authenticated` field still means execution eligibility. A logged-in
|
|
674
|
+
Claude account can remain blocked by unverified security admission. Do not log out
|
|
675
|
+
or change credentials just because that legacy field is false. Unknown status is
|
|
676
|
+
not evidence of logout. These diagnostics do not bypass per-job admission.
|
|
@@ -74,45 +74,49 @@ export class ClaudeCodeAdapter {
|
|
|
74
74
|
throw new Error("adapter has already been started");
|
|
75
75
|
this.started = true;
|
|
76
76
|
this.p1 = ctx.approvedTaskPackage?.verification?.mode === 'runner_observed_v1';
|
|
77
|
-
const generation = ctx.generation;
|
|
78
|
-
if (!Number.isSafeInteger(generation) || generation === undefined || generation < 1)
|
|
79
|
-
throw new Error("JOB_SECURITY_REJECTED");
|
|
80
|
-
const assertAdmission = () => {
|
|
81
|
-
requireJobSecurity(this.options.securityPolicy, { adapterId: "claude_code_cli", adapterVersion: "1", jobId: this.options.jobId ?? ctx.bindingId, modelId: resolvedModelId, generation });
|
|
82
|
-
if (ctx.approvedTaskPackage?.resolved_provider !== "anthropic" || ctx.approvedTaskPackage?.resolved_model_id !== resolvedModelId
|
|
83
|
-
|| ctx.approvedTaskPackage?.repository?.base_commit_sha !== this.options.securityPolicy.pin.sourceSha)
|
|
84
|
-
throw new Error("SECURITY_REQUIREMENTS_REJECTED");
|
|
85
|
-
requireSecurityRequirements(ctx.approvedTaskPackage?.security_requirements, { adapterId: "claude_code_cli", adapterVersion: "1", requirementsSha256: this.options.securityPolicy.pin.requirementsSha256 });
|
|
86
|
-
};
|
|
87
|
-
assertAdmission();
|
|
88
|
-
if (!ctx.securityBinding)
|
|
89
|
-
throw new Error('SECURITY_REQUIREMENTS_REJECTED');
|
|
90
|
-
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 });
|
|
91
|
-
const pkg = ctx.taskPackage;
|
|
92
|
-
const repos = pkg.repositories;
|
|
93
|
-
if (!Array.isArray(repos) || repos.length !== 1 || repos[0]?.base_commit_sha !== this.options.securityPolicy.pin.sourceSha)
|
|
94
|
-
throw new Error("JOB_SECURITY_REJECTED");
|
|
95
|
-
const cwd = this.options.cwd;
|
|
96
|
-
const rel = cwd ? path.relative(path.resolve(this.options.worktreeRoot), path.resolve(cwd)) : "";
|
|
97
|
-
if (!cwd || !path.isAbsolute(cwd) || rel === "" || (!rel.startsWith(".." + path.sep) && rel !== ".." && !path.isAbsolute(rel)))
|
|
98
|
-
throw new Error("trusted cwd must live outside the assigned worktree");
|
|
99
|
-
const unbounded = isUnboundedLocalExecution(ctx.taskPackage);
|
|
100
|
-
if (!unbounded && (!Number.isFinite(this.options.maxBudgetUsd) || this.options.maxBudgetUsd <= 0)) {
|
|
101
|
-
throw new Error("refusing to start: no positive remaining contract budget was supplied");
|
|
102
|
-
}
|
|
103
|
-
if (!this.options.worktreeRoot) {
|
|
104
|
-
throw new Error("refusing to start: no assigned worktree was supplied");
|
|
105
|
-
}
|
|
106
|
-
if (!ctx.sessionId) {
|
|
107
|
-
throw new Error("refusing to start: no runner-generated session ID was supplied");
|
|
108
|
-
}
|
|
109
|
-
const hookCommand = this.requireTrustedHookCommand();
|
|
110
|
-
const prompt = compilePrompt(ctx.taskPackage, { workItemLabel: this.options.workItemLabel });
|
|
111
|
-
if (this.cancelled)
|
|
112
|
-
throw new Error('adapter startup cancelled');
|
|
113
77
|
let acquired;
|
|
114
78
|
this.startupAcquisition = new Promise(resolve => { acquired = resolve; });
|
|
115
79
|
try {
|
|
80
|
+
const generation = ctx.generation;
|
|
81
|
+
if (!Number.isSafeInteger(generation) || generation === undefined || generation < 1)
|
|
82
|
+
throw new Error("JOB_SECURITY_REJECTED");
|
|
83
|
+
if (this.options.prepareSecurity)
|
|
84
|
+
await this.options.prepareSecurity(ctx);
|
|
85
|
+
if (this.cancelled)
|
|
86
|
+
throw new Error('adapter startup cancelled');
|
|
87
|
+
const assertAdmission = () => {
|
|
88
|
+
const repositories = ctx.taskPackage?.repositories;
|
|
89
|
+
if (!Array.isArray(repositories) || repositories.length !== 1 || repositories[0]?.base_commit_sha !== this.options.securityPolicy?.pin.sourceSha)
|
|
90
|
+
throw new Error('JOB_SECURITY_REJECTED');
|
|
91
|
+
requireJobSecurity(this.options.securityPolicy, { adapterId: "claude_code_cli", adapterVersion: "1", jobId: this.options.jobId ?? ctx.bindingId, modelId: resolvedModelId, generation });
|
|
92
|
+
if (ctx.approvedTaskPackage?.resolved_provider !== "anthropic" || ctx.approvedTaskPackage?.resolved_model_id !== resolvedModelId
|
|
93
|
+
|| ctx.approvedTaskPackage?.repository?.base_commit_sha !== this.options.securityPolicy.pin.sourceSha)
|
|
94
|
+
throw new Error("SECURITY_REQUIREMENTS_REJECTED");
|
|
95
|
+
requireSecurityRequirements(ctx.approvedTaskPackage?.security_requirements, { adapterId: "claude_code_cli", adapterVersion: "1", requirementsSha256: this.options.securityPolicy.pin.requirementsSha256 });
|
|
96
|
+
};
|
|
97
|
+
assertAdmission();
|
|
98
|
+
if (!ctx.securityBinding)
|
|
99
|
+
throw new Error('SECURITY_REQUIREMENTS_REJECTED');
|
|
100
|
+
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 });
|
|
101
|
+
const pkg = ctx.taskPackage;
|
|
102
|
+
const cwd = this.options.cwd;
|
|
103
|
+
const rel = cwd ? path.relative(path.resolve(this.options.worktreeRoot), path.resolve(cwd)) : "";
|
|
104
|
+
if (!cwd || !path.isAbsolute(cwd) || rel === "" || (!rel.startsWith(".." + path.sep) && rel !== ".." && !path.isAbsolute(rel)))
|
|
105
|
+
throw new Error("trusted cwd must live outside the assigned worktree");
|
|
106
|
+
const unbounded = isUnboundedLocalExecution(ctx.taskPackage);
|
|
107
|
+
if (!unbounded && (!Number.isFinite(this.options.maxBudgetUsd) || this.options.maxBudgetUsd <= 0)) {
|
|
108
|
+
throw new Error("refusing to start: no positive remaining contract budget was supplied");
|
|
109
|
+
}
|
|
110
|
+
if (!this.options.worktreeRoot) {
|
|
111
|
+
throw new Error("refusing to start: no assigned worktree was supplied");
|
|
112
|
+
}
|
|
113
|
+
if (!ctx.sessionId) {
|
|
114
|
+
throw new Error("refusing to start: no runner-generated session ID was supplied");
|
|
115
|
+
}
|
|
116
|
+
const hookCommand = this.requireTrustedHookCommand();
|
|
117
|
+
const prompt = compilePrompt(ctx.taskPackage, { workItemLabel: this.options.workItemLabel });
|
|
118
|
+
if (this.cancelled)
|
|
119
|
+
throw new Error('adapter startup cancelled');
|
|
116
120
|
if (!this.options.createTools || !this.options.writeLocalMcpConfig)
|
|
117
121
|
throw new Error("job-local tool setup is required");
|
|
118
122
|
this.tools = await this.options.createTools(ctx, pkg, ctx.approvedTaskPackage ?? {});
|
|
@@ -166,6 +170,9 @@ export class ClaudeCodeAdapter {
|
|
|
166
170
|
...allowlistedEnv(this.options.env),
|
|
167
171
|
[HOOK_POLICY_ENV_VAR]: encodeHookPolicy(hookPolicy),
|
|
168
172
|
};
|
|
173
|
+
await this.options.assertCurrentInputs?.();
|
|
174
|
+
if (this.cancelled)
|
|
175
|
+
throw new Error('adapter startup cancelled');
|
|
169
176
|
assertAdmission();
|
|
170
177
|
const handle = this.options.processHost.spawn(this.options.binaryPath ?? "claude", args, {
|
|
171
178
|
cwd,
|
|
@@ -259,6 +266,7 @@ export class ClaudeCodeAdapter {
|
|
|
259
266
|
if (this.cancelPromise)
|
|
260
267
|
return this.cancelPromise;
|
|
261
268
|
this.localReadObservations();
|
|
269
|
+
this.revokeSecurity();
|
|
262
270
|
this.revokeTools(this.tools);
|
|
263
271
|
const cleanup = (async () => { await this.startupAcquisition; await this.cleanup(); })();
|
|
264
272
|
// This bounds cleanup confirmation, never admitted model runtime. Unknown
|
|
@@ -269,6 +277,18 @@ export class ClaudeCodeAdapter {
|
|
|
269
277
|
});
|
|
270
278
|
return this.cancelPromise;
|
|
271
279
|
}
|
|
280
|
+
securityRevoked = false;
|
|
281
|
+
revokeSecurity() {
|
|
282
|
+
if (this.securityRevoked)
|
|
283
|
+
return;
|
|
284
|
+
this.securityRevoked = true;
|
|
285
|
+
try {
|
|
286
|
+
this.options.revokeSecurity?.();
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
this.revokeFailed = true;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
272
292
|
revokeTools(tools) {
|
|
273
293
|
if (!tools || this.revokedTools === tools)
|
|
274
294
|
return;
|
|
@@ -291,6 +311,7 @@ export class ClaudeCodeAdapter {
|
|
|
291
311
|
this.tools = undefined;
|
|
292
312
|
this.handle = null;
|
|
293
313
|
const operation = Promise.resolve().then(async () => {
|
|
314
|
+
this.revokeSecurity();
|
|
294
315
|
this.revokeTools(tools);
|
|
295
316
|
let failed = this.revokeFailed;
|
|
296
317
|
try {
|
|
@@ -1,15 +1,52 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { mkdir, realpath, rm } from 'node:fs/promises';
|
|
4
|
+
import { lstat } from 'node:fs/promises';
|
|
5
|
+
import { JobSecuritySession } from '../../safety/job-security-session.js';
|
|
6
|
+
import { assertCurrentClaudeBinary, claudeBinaryLaunchPath } from '../../safety/claude-binary-identity.js';
|
|
4
7
|
import { writeFileSync } from 'node:fs';
|
|
5
8
|
import { NodeProcessGroupHost } from '../../process/node-process-group-host.js';
|
|
6
9
|
import { JobToolSession } from '../../safety/job-tool-session.js';
|
|
7
10
|
import { jobToolPolicyOptions } from '../../safety/job-tool-policy.js';
|
|
8
11
|
import { requireJobSecurity } from '../../safety/job-security-policy.js';
|
|
9
12
|
const inside = (root, file) => file === root || file.startsWith(root + path.sep);
|
|
13
|
+
/** Resolve the existing prefix without creating a path through an account alias. */
|
|
14
|
+
async function canonicalFuture(file) {
|
|
15
|
+
let parent = file;
|
|
16
|
+
const tail = [];
|
|
17
|
+
for (;;) {
|
|
18
|
+
try {
|
|
19
|
+
await lstat(parent);
|
|
20
|
+
return path.join(await realpath(parent), ...tail);
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
if (error.code !== 'ENOENT')
|
|
24
|
+
throw error;
|
|
25
|
+
const next = path.dirname(parent);
|
|
26
|
+
if (next === parent)
|
|
27
|
+
throw error;
|
|
28
|
+
tail.unshift(path.basename(parent));
|
|
29
|
+
parent = next;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
10
33
|
/** Wiring only. No release verifier or admission certificate exists here. */
|
|
11
34
|
export function createClaudeCodeAdapterOptions(input) {
|
|
35
|
+
if (input.securityHost && (input.securityPolicy || input.nativeBinding || input.commandRegistry) || !!input.securityHost !== !!input.binaryIdentity)
|
|
36
|
+
throw Error('CLAUDE_SECURITY_AUTHORITY_CONFLICT');
|
|
37
|
+
const security = new JobSecuritySession(input.securityHost);
|
|
38
|
+
let securityPolicy = input.securityPolicy, disposed = false;
|
|
12
39
|
const checkout = path.resolve(input.worktreePath), state = path.resolve(input.stateRoot);
|
|
40
|
+
const account = input.env.CLAUDE_CONFIG_DIR ?? (input.env.HOME ? path.join(input.env.HOME, '.claude') : undefined);
|
|
41
|
+
if (account !== undefined && (!path.isAbsolute(account) || /[\r\n\0]/.test(account)))
|
|
42
|
+
throw Error('CLAUDE_ACCOUNT_CONTEXT_UNAVAILABLE');
|
|
43
|
+
const assertOutsideAccount = (storage, source, auth = account) => { if (auth && [storage, source].some(root => inside(path.resolve(auth), root) || inside(root, path.resolve(auth))))
|
|
44
|
+
throw Error('CLAUDE_ACCOUNT_CONTROL_OVERLAP'); };
|
|
45
|
+
assertOutsideAccount(state, checkout);
|
|
46
|
+
const revoke = () => { disposed = true; security.revoke(); securityPolicy?.revoke(); };
|
|
47
|
+
const binary = input.binaryIdentity ? claudeBinaryLaunchPath(input.binaryIdentity) : input.binaryPath;
|
|
48
|
+
if (input.binaryPath && binary !== input.binaryPath)
|
|
49
|
+
throw Error('CLAUDE_BINARY_IDENTITY_REJECTED');
|
|
13
50
|
if (inside(checkout, state))
|
|
14
51
|
throw new Error('Claude job controls must be outside the checkout');
|
|
15
52
|
const control = path.join(state, 'claude-jobs', randomUUID());
|
|
@@ -22,47 +59,91 @@ export function createClaudeCodeAdapterOptions(input) {
|
|
|
22
59
|
env.TMPDIR = path.join(control, 'tmp'); // Claude owns credential access. Preserve the account location by path only;
|
|
23
60
|
// never read/copy its files or send this environment to scoped workers.
|
|
24
61
|
// https://code.claude.com/docs/en/authentication#log-in-with-multiple-accounts
|
|
25
|
-
env.CLAUDE_CONFIG_DIR =
|
|
62
|
+
env.CLAUDE_CONFIG_DIR = account;
|
|
26
63
|
if (env.CLAUDE_CONFIG_DIR && !path.isAbsolute(env.CLAUDE_CONFIG_DIR))
|
|
27
64
|
throw new Error('Claude account configuration path must be absolute');
|
|
28
65
|
return {
|
|
29
|
-
processHost: new NodeProcessGroupHost(), worktreeRoot: checkout, jobId: input.jobId, securityPolicy
|
|
30
|
-
|
|
66
|
+
processHost: new NodeProcessGroupHost(), worktreeRoot: checkout, jobId: input.jobId, get securityPolicy() { return securityPolicy; },
|
|
67
|
+
...(input.securityHost ? { prepareSecurity: async (ctx) => {
|
|
68
|
+
try {
|
|
69
|
+
if (disposed || !ctx.securityBinding || !account)
|
|
70
|
+
throw Error('JOB_SECURITY_SESSION_REJECTED');
|
|
71
|
+
const approved = ctx.approvedTaskPackage, repo = approved?.repository;
|
|
72
|
+
securityPolicy = await security.authorize({ adapterId: 'claude_code_cli', adapterVersion: '1', jobId: input.jobId, generation: ctx.generation, modelId: ctx.resolvedModelId,
|
|
73
|
+
sourceSha: String(repo?.base_commit_sha ?? ''), requirements: approved?.security_requirements, ...ctx.securityBinding });
|
|
74
|
+
if (disposed)
|
|
75
|
+
throw Error('JOB_SECURITY_SESSION_REJECTED');
|
|
76
|
+
await assertCurrentClaudeBinary(input.binaryIdentity, binary, securityPolicy.pin.binarySha256);
|
|
77
|
+
return securityPolicy;
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
revoke();
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
} } : {}),
|
|
84
|
+
revokeSecurity: revoke,
|
|
85
|
+
assertCurrentInputs: async () => { if (disposed)
|
|
86
|
+
throw Error('JOB_SECURITY_SESSION_REJECTED'); if (input.binaryIdentity) {
|
|
87
|
+
if (!securityPolicy)
|
|
88
|
+
throw Error('JOB_SECURITY_SESSION_REJECTED');
|
|
89
|
+
await assertCurrentClaudeBinary(input.binaryIdentity, binary, securityPolicy.pin.binarySha256);
|
|
90
|
+
} },
|
|
91
|
+
cwd, env, binaryPath: binary, hookExecutablePath: input.hookExecutablePath, hookNodeExecutablePath: input.hookNodeExecutablePath,
|
|
31
92
|
maxBudgetUsd: input.maxBudgetUsd ?? 1, workItemLabel: input.workItemLabel ?? input.jobId,
|
|
32
93
|
registeredReadTools: [], writeMcpConfig: () => { throw new Error('Gateway relay is not a model tool endpoint'); },
|
|
33
94
|
createTools: async (ctx, pkg, approved) => {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
await
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
95
|
+
if (disposed)
|
|
96
|
+
throw Error('JOB_SECURITY_SESSION_REJECTED');
|
|
97
|
+
const policyOptions = jobToolPolicyOptions(checkout, pkg, approved, input.securityHost ? security.commands() : input.commandRegistry ?? [], { runtimeAdapter: 'claude_code_cli', runtimeAdapterVersion: '1' });
|
|
98
|
+
const canonicalAccount = account ? await canonicalFuture(account) : undefined;
|
|
99
|
+
assertOutsideAccount(await canonicalFuture(state), await canonicalFuture(checkout), canonicalAccount);
|
|
100
|
+
assertOutsideAccount(await canonicalFuture(control), await canonicalFuture(checkout), canonicalAccount);
|
|
101
|
+
let owned = false;
|
|
102
|
+
try {
|
|
103
|
+
await mkdir(state, { recursive: true, mode: 0o700 });
|
|
104
|
+
const realState = await realpath(state), realCheckout = await realpath(checkout);
|
|
105
|
+
assertOutsideAccount(realState, realCheckout, account ? await canonicalFuture(account) : undefined);
|
|
106
|
+
if (realState !== state || inside(realCheckout, realState))
|
|
107
|
+
throw new Error('Claude job controls must be outside the checkout');
|
|
108
|
+
await mkdir(path.dirname(control), { recursive: true, mode: 0o700 });
|
|
109
|
+
if (await realpath(path.dirname(control)) !== path.dirname(control))
|
|
110
|
+
throw Error('CLAUDE_CONTROL_ALIAS_REJECTED');
|
|
111
|
+
await mkdir(control, { mode: 0o700 });
|
|
112
|
+
owned = true;
|
|
113
|
+
for (const directory of [home, cwd, env.TMPDIR])
|
|
114
|
+
await mkdir(directory, { mode: 0o700 });
|
|
115
|
+
let active = true;
|
|
116
|
+
const tools = new JobToolSession({
|
|
117
|
+
policyOptions,
|
|
118
|
+
binding: () => {
|
|
119
|
+
const binding = input.securityHost ? security.binding() : input.nativeBinding?.();
|
|
120
|
+
return binding && binding.jobId === input.jobId && binding.generation === securityPolicy?.pin.generation ? binding : null;
|
|
121
|
+
},
|
|
122
|
+
active: () => {
|
|
123
|
+
if (!active)
|
|
124
|
+
return false;
|
|
125
|
+
try {
|
|
126
|
+
requireJobSecurity(securityPolicy, { adapterId: 'claude_code_cli', adapterVersion: '1', jobId: input.jobId, modelId: ctx.resolvedModelId, generation: ctx.generation, sourceSha: String(approved.repository.base_commit_sha) });
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
return { start: () => tools.start(), revoke: () => { active = false; tools.revoke(); }, close: async () => { active = false; revoke(); try {
|
|
135
|
+
await tools.close();
|
|
54
136
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
finally {
|
|
137
|
+
finally {
|
|
138
|
+
await rm(control, { recursive: true, force: true });
|
|
139
|
+
} } };
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
revoke();
|
|
143
|
+
if (owned)
|
|
64
144
|
await rm(control, { recursive: true, force: true });
|
|
65
|
-
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
66
147
|
},
|
|
67
148
|
writeLocalMcpConfig: endpoint => {
|
|
68
149
|
writeFileSync(config, JSON.stringify({ mcpServers: { ai_local: { type: 'http', url: endpoint.url, headers: { Authorization: `Bearer ${endpoint.token}` } } } }), { mode: 0o600, flag: 'wx' });
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { adapterStatus } from './status.js';
|
|
1
2
|
import { createCodexProductionHost } from '../safety/codex-production-host.js';
|
|
2
3
|
import { execFile } from 'node:child_process';
|
|
3
4
|
import { promisify } from 'node:util';
|
|
@@ -20,22 +21,51 @@ export async function discoverAdapters(env, command = run, adapterIds = ['claude
|
|
|
20
21
|
if (!executableVersion)
|
|
21
22
|
throw new Error('invalid version');
|
|
22
23
|
let authenticated = false;
|
|
24
|
+
let loginStatus = 'unknown';
|
|
25
|
+
const parseClaudeStatus = (stdout) => {
|
|
26
|
+
try {
|
|
27
|
+
const value = JSON.parse(stdout);
|
|
28
|
+
if (value && typeof value === 'object' && !Array.isArray(value) && typeof value.loggedIn === 'boolean') {
|
|
29
|
+
authenticated = value.loggedIn;
|
|
30
|
+
loginStatus = authenticated ? 'authenticated' : 'unauthenticated';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch { /* Unknown evidence stays closed; never expose raw output. */ }
|
|
34
|
+
};
|
|
23
35
|
try {
|
|
24
|
-
const auth = await command(binary, codex ? ['login', 'status'] : ['auth', 'status']);
|
|
25
|
-
|
|
36
|
+
const auth = await command(binary, codex ? ['login', 'status'] : ['auth', 'status', '--json']);
|
|
37
|
+
if (codex) {
|
|
38
|
+
authenticated = /^Logged in using ChatGPT\s*$/i.test([auth.stdout, auth.stderr ?? ''].join('\n').trim());
|
|
39
|
+
loginStatus = authenticated ? 'authenticated' : 'unauthenticated';
|
|
40
|
+
}
|
|
41
|
+
else
|
|
42
|
+
parseClaudeStatus(auth.stdout);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
// Claude exits 1 for a known logged-out JSON response. Other failures
|
|
46
|
+
// (including timeout/permission errors) do not prove logout.
|
|
47
|
+
if (!codex && error && typeof error === 'object' && error.code === 1) {
|
|
48
|
+
const stdout = error.stdout;
|
|
49
|
+
if (typeof stdout === 'string') {
|
|
50
|
+
parseClaudeStatus(stdout);
|
|
51
|
+
if (authenticated) {
|
|
52
|
+
authenticated = false;
|
|
53
|
+
loginStatus = 'unknown';
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
26
57
|
}
|
|
27
|
-
catch { /* Missing login is diagnostic, never an API-key fallback. */ }
|
|
28
58
|
if (codex) {
|
|
29
59
|
const compatibility = inspectCodexCompatibility({ cliVersion: executableVersion });
|
|
30
60
|
const admission = await diagnoseCliHostAdmission(id, binary, env);
|
|
31
|
-
return { ...base, available: compatibility.available, authenticated, executableVersion, capabilities: compatibility.capabilities, admission,
|
|
61
|
+
return { ...base, available: compatibility.available, authenticated, loginStatus, executableVersion, capabilities: compatibility.capabilities, admission,
|
|
32
62
|
reason: !authenticated ? 'Codex requires a ChatGPT account login; API-key authentication is unsupported.' : compatibility.reason };
|
|
33
63
|
}
|
|
34
64
|
const admission = await diagnoseCliHostAdmission(id, binary, env);
|
|
35
|
-
return { ...base, available: admission.available, authenticated, executableVersion, capabilities: admission.capabilities, admission, reason: !authenticated ? 'Claude CLI login is required.' : 'Claude CLI security isolation is unverified; execution is unsupported.' };
|
|
65
|
+
return { ...base, available: admission.available, authenticated, loginStatus, executableVersion, capabilities: admission.capabilities, admission, reason: loginStatus === 'unknown' ? 'Claude CLI login status could not be verified.' : !authenticated ? 'Claude CLI login is required.' : 'Claude CLI security isolation is unverified; execution is unsupported.' };
|
|
36
66
|
}
|
|
37
67
|
catch {
|
|
38
|
-
return { ...base, available: false, authenticated: false, capabilities: [], reason: codex ? 'Codex CLI is missing or its version could not be verified.' : 'Claude CLI is missing or its version could not be verified.' };
|
|
68
|
+
return { ...base, available: false, authenticated: false, loginStatus: 'unknown', capabilities: [], reason: codex ? 'Codex CLI is missing or its version could not be verified.' : 'Claude CLI is missing or its version could not be verified.' };
|
|
39
69
|
}
|
|
40
70
|
}));
|
|
41
71
|
}
|
|
@@ -56,6 +86,8 @@ export function adapterDiagnosticsWithRemedies(adapters) {
|
|
|
56
86
|
else
|
|
57
87
|
action = 'Codex is ready for an explicitly selected approved launch.';
|
|
58
88
|
}
|
|
89
|
+
else if (adapter.loginStatus === 'unknown')
|
|
90
|
+
action = 'Claude login status is unknown; inspect the local status diagnostic before changing credentials. Execution remains blocked.';
|
|
59
91
|
else if (!adapter.available && adapter.executableVersion && adapter.authenticated)
|
|
60
92
|
action = 'Claude CLI security isolation is unverified; wait for a reviewed release. Login alone does not grant execution.';
|
|
61
93
|
else if (!adapter.available)
|
|
@@ -64,7 +96,7 @@ export function adapterDiagnosticsWithRemedies(adapters) {
|
|
|
64
96
|
action = 'Run claude auth login, then rerun doctor.';
|
|
65
97
|
else
|
|
66
98
|
action = 'Claude is ready for an explicitly selected approved launch.';
|
|
67
|
-
return { ...adapter, action };
|
|
99
|
+
return { ...adapter, ...adapterStatus(adapter), action };
|
|
68
100
|
});
|
|
69
101
|
}
|
|
70
102
|
/** Setup/doctor use the same reviewed Codex probe as daemon; no legacy vendor
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Diagnostics never grant capabilities or replace the host admission gate. */
|
|
2
|
+
export function adapterStatus(probe) {
|
|
3
|
+
const login_status = probe.loginStatus ?? (probe.authenticated ? 'authenticated' : 'unknown');
|
|
4
|
+
const ready = probe.available && probe.authenticated && login_status === 'authenticated';
|
|
5
|
+
const codes = probe.admission?.checks.filter(c => c.status === 'blocked').map(c => c.code).filter(c => /^[A-Z][A-Z0-9_]{0,95}$/.test(c)).slice(0, 20) ?? [];
|
|
6
|
+
if (!ready && !codes.length)
|
|
7
|
+
codes.push(!probe.available ? 'EXECUTION_ADMISSION_UNAVAILABLE' : login_status === 'unknown' ? 'LOGIN_STATUS_UNVERIFIED' : 'LOGIN_REQUIRED');
|
|
8
|
+
return { authenticated: ready, login_status, execution_status: ready ? 'ready' : 'blocked', execution_reason_codes: ready ? [] : [...new Set(codes)] };
|
|
9
|
+
}
|
package/dist/src/cli.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { adapterStatus } from './adapters/status.js';
|
|
2
3
|
import { recoverLocalFixPublication } from './publication/recovery.js';
|
|
3
4
|
import { loadInstalledLocalFixHost } from './publication/installed-host.js';
|
|
4
5
|
import { FileDaemonStateStore } from './state-store.js';
|
|
@@ -194,7 +195,7 @@ export async function main(argv, env, io = {}) {
|
|
|
194
195
|
const registered = await gateway.registerLocalRunner({
|
|
195
196
|
deviceFingerprint: createHash("sha256").update(`${process.getuid?.() ?? 0}|${runnerDisplayName()}|${config.gatewayMcpUrl}`).digest("hex"),
|
|
196
197
|
displayName: runnerDisplayName(), packageVersion: "0.1.11",
|
|
197
|
-
capabilities: adapters.map(probe => ({ adapter: probe.id, version: probe.version, capabilities: probe.capabilities,
|
|
198
|
+
capabilities: adapters.map(probe => ({ adapter: probe.id, version: probe.version, capabilities: probe.capabilities, ...adapterStatus(probe) })),
|
|
198
199
|
});
|
|
199
200
|
await localStore.save({ schemaVersion: 1, gatewayMcpUrl: config.gatewayMcpUrl, runnerId: registered.runner.id, workspaceRoot: config.workspaceRoot, concurrency: 1 });
|
|
200
201
|
await launchd.install(process.argv[1], config.stateRoot);
|
package/dist/src/config.js
CHANGED
|
@@ -43,6 +43,25 @@ function parseWorkLoopId(args) {
|
|
|
43
43
|
}
|
|
44
44
|
return value;
|
|
45
45
|
}
|
|
46
|
+
/*
|
|
47
|
+
* The LaunchAgent starts the daemon as `run --state-root=<root>`, with the root setup used (Work Item 2ae45a4f).
|
|
48
|
+
* launchd does not pass setup's XDG_STATE_HOME, so the argument, not the environment, decides the root whenever it
|
|
49
|
+
* is given. Only one absolute, normalized path is accepted.
|
|
50
|
+
*/
|
|
51
|
+
function parseStateRootArgument(args) {
|
|
52
|
+
const prefix = "--state-root=";
|
|
53
|
+
const found = args.filter((arg) => arg.startsWith(prefix));
|
|
54
|
+
if (found.length === 0)
|
|
55
|
+
return undefined;
|
|
56
|
+
if (found.length > 1)
|
|
57
|
+
throw new Error("--state-root may be given only once");
|
|
58
|
+
const value = found[0].slice(prefix.length);
|
|
59
|
+
if (!value || !path.isAbsolute(value) || value === "/" || value.includes("\0")
|
|
60
|
+
|| path.normalize(value) !== value || value.endsWith("/")) {
|
|
61
|
+
throw new Error("--state-root must be a normalized absolute path");
|
|
62
|
+
}
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
46
65
|
function parseAbsolutePath(value, label, required) {
|
|
47
66
|
if (!value) {
|
|
48
67
|
if (required)
|
|
@@ -98,8 +117,9 @@ export function loadConfig(argv, env) {
|
|
|
98
117
|
if (!mode || !ALL_MODES.includes(mode)) {
|
|
99
118
|
throw new Error(`mode must be one of ${ALL_MODES.join(", ")}`);
|
|
100
119
|
}
|
|
101
|
-
const
|
|
102
|
-
|
|
120
|
+
const isServiceMode = SERVICE_MODES.includes(mode);
|
|
121
|
+
const stateRoot = (isServiceMode ? parseStateRootArgument(rest) : undefined) ?? defaultStateRootFromEnv(env);
|
|
122
|
+
if (isServiceMode) {
|
|
103
123
|
let manualJobId;
|
|
104
124
|
if (mode === 'manual-codex' || mode === 'publication-recover') {
|
|
105
125
|
if (rest.length !== 1 || !rest[0].startsWith('--job-id=') || !UUID_PATTERN.test(rest[0].slice(9)))
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { adapterStatus } from '../adapters/status.js';
|
|
1
2
|
import { loadInstalledLocalFixHost } from '../publication/installed-host.js';
|
|
2
3
|
import { createCodexDaemonHost } from '../safety/codex-daemon-host.js';
|
|
3
4
|
import { reconcileTerminalState } from './terminal-recovery.js';
|
|
@@ -25,6 +26,9 @@ import { buildTerminalEvidenceCollector } from "../safety/terminal-evidence.js";
|
|
|
25
26
|
import { packagedHookCommandParts } from "../safety/hook-command.js";
|
|
26
27
|
import { composeReviewedP1Host } from '../verification/p1-host-composition.js';
|
|
27
28
|
export async function buildDaemon(input) {
|
|
29
|
+
const claudeParts = [input.claudeSecurityHost, input.claudeSupportedProbe, input.claudeBinaryIdentity].filter(Boolean).length;
|
|
30
|
+
if (claudeParts !== 0 && claudeParts !== 3)
|
|
31
|
+
throw new Error('CLAUDE_SUPPORTED_HOST_REQUIRED');
|
|
28
32
|
if (input.codexSupportedProbe && !input.codexSecurityHost)
|
|
29
33
|
throw new Error('CODEX_SUPPORTED_HOST_REQUIRED');
|
|
30
34
|
const production = !input.codexSecurityHost && !input.codexSupportedProbe ? createCodexDaemonHost({ runnerId: input.runnerId, binary: input.env.AI_GATEWAY_LOCAL_RUNNER_CODEX_BINARY ?? 'codex', home: input.env.HOME ?? '', codexHome: input.env.CODEX_HOME, searchPath: input.env.PATH }) : undefined;
|
|
@@ -51,11 +55,12 @@ export async function buildDaemon(input) {
|
|
|
51
55
|
// The same parts the CLI self-test ran before starting this daemon (verifyPackagedHook).
|
|
52
56
|
const hook = packagedHookCommandParts();
|
|
53
57
|
const plugin = new ClaudeCodePlugin({
|
|
54
|
-
probe: async () => (await discoverAdapters(input.env, undefined, ['claude_code_cli']))[0],
|
|
58
|
+
probe: async () => input.claudeSupportedProbe ? input.claudeSupportedProbe() : (await discoverAdapters(input.env, undefined, ['claude_code_cli']))[0],
|
|
55
59
|
options: ({ worktreePath, jobId }) => createClaudeCodeAdapterOptions({
|
|
56
60
|
worktreePath, jobId, stateRoot: input.stateRoot, env: input.env,
|
|
57
61
|
maxBudgetUsd: Number.MAX_SAFE_INTEGER, hookExecutablePath: hook.hookScriptPath, hookNodeExecutablePath: hook.nodeExecutablePath,
|
|
58
62
|
binaryPath: input.env.AI_GATEWAY_LOCAL_RUNNER_CLAUDE_BINARY,
|
|
63
|
+
securityHost: input.claudeSecurityHost, binaryIdentity: input.claudeBinaryIdentity,
|
|
59
64
|
}),
|
|
60
65
|
});
|
|
61
66
|
const codex = new CodexPlugin({
|
|
@@ -64,7 +69,7 @@ export async function buildDaemon(input) {
|
|
|
64
69
|
});
|
|
65
70
|
const registry = new AdapterRegistry([plugin, codex]);
|
|
66
71
|
const probes = await registry.probeAll();
|
|
67
|
-
const versions = Object.fromEntries(probes.filter((probe) => probe.
|
|
72
|
+
const versions = Object.fromEntries(probes.filter((probe) => adapterStatus(probe).authenticated).map((probe) => [probe.id, probe.version]));
|
|
68
73
|
const state = new FileDaemonStateStore(input.stateRoot);
|
|
69
74
|
const coordinator = new ExecutionCoordinator({
|
|
70
75
|
localFixHost,
|
|
@@ -108,6 +113,23 @@ export async function buildDaemon(input) {
|
|
|
108
113
|
const poller = new JobPoller({
|
|
109
114
|
reportError: (diagnostic) => { process.stderr.write(`${JSON.stringify({ event: "poller_failed", ...diagnostic })}\n`); },
|
|
110
115
|
heartbeat: async () => {
|
|
116
|
+
if (input.claudeSupportedProbe) {
|
|
117
|
+
let current;
|
|
118
|
+
try {
|
|
119
|
+
current = await plugin.probe();
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
current = { available: false, authenticated: false, capabilities: [] };
|
|
123
|
+
}
|
|
124
|
+
const ready = adapterStatus(current).authenticated && !!current.executableVersion && plugin.capabilities.every(c => current.capabilities.includes(c));
|
|
125
|
+
const index = probes.findIndex(p => p.id === 'claude_code_cli');
|
|
126
|
+
if (index >= 0)
|
|
127
|
+
probes[index] = { id: 'claude_code_cli', version: '1', ...current, available: ready };
|
|
128
|
+
if (ready)
|
|
129
|
+
versions.claude_code_cli = '1';
|
|
130
|
+
else
|
|
131
|
+
delete versions.claude_code_cli;
|
|
132
|
+
}
|
|
111
133
|
let current;
|
|
112
134
|
try {
|
|
113
135
|
current = await codex.probe();
|
|
@@ -115,10 +137,10 @@ export async function buildDaemon(input) {
|
|
|
115
137
|
catch {
|
|
116
138
|
current = { available: false, authenticated: false, capabilities: [] };
|
|
117
139
|
}
|
|
118
|
-
const ready = current.
|
|
140
|
+
const ready = adapterStatus(current).authenticated && !!current.executableVersion && codex.capabilities.every(c => current.capabilities.includes(c));
|
|
119
141
|
const index = probes.findIndex(p => p.id === 'codex_cli');
|
|
120
142
|
if (index >= 0)
|
|
121
|
-
probes[index] = { id: 'codex_cli', version: '1', ...current,
|
|
143
|
+
probes[index] = { id: 'codex_cli', version: '1', ...current, available: ready };
|
|
122
144
|
if (ready)
|
|
123
145
|
versions.codex_cli = '1';
|
|
124
146
|
else
|
|
@@ -222,7 +244,8 @@ export async function reconcilePreparationState(previous, deps) {
|
|
|
222
244
|
/** Vendor capability text cannot claim a host-owned publication facility. */
|
|
223
245
|
export function localRunnerCapabilities(probes, trustedLocalFixHost = false, verificationMode) {
|
|
224
246
|
return probes.map(probe => {
|
|
225
|
-
const
|
|
247
|
+
const status = adapterStatus(probe);
|
|
248
|
+
const authenticated = status.authenticated;
|
|
226
249
|
const capabilities = probe.capabilities.filter(value => value !== 'local_fix_publication_v1' && value !== 'local_fix_gateway_ci_v1' && value !== 'local_fix_gateway_publish_v1');
|
|
227
250
|
if (trustedLocalFixHost && authenticated && probe.id === 'codex_cli') {
|
|
228
251
|
capabilities.push('local_fix_publication_v1');
|
|
@@ -231,6 +254,6 @@ export function localRunnerCapabilities(probes, trustedLocalFixHost = false, ver
|
|
|
231
254
|
if (verificationMode === 'gateway_publish_v1')
|
|
232
255
|
capabilities.push('local_fix_gateway_publish_v1');
|
|
233
256
|
}
|
|
234
|
-
return { adapter: probe.id, version: probe.version,
|
|
257
|
+
return { adapter: probe.id, version: probe.version, ...status, capabilities };
|
|
235
258
|
});
|
|
236
259
|
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { lstat, realpath, readlink } from 'node:fs/promises';
|
|
3
|
+
import { measureCommandFile } from './registered-command-integrity.js';
|
|
4
|
+
const live = new WeakMap();
|
|
5
|
+
const rejected = () => Error('CLAUDE_BINARY_IDENTITY_REJECTED');
|
|
6
|
+
const inside = (root, file) => file === root || file.startsWith(root + path.sep);
|
|
7
|
+
const validPath = (value) => typeof value === 'string' && path.isAbsolute(value) && path.resolve(value) === value && value !== '/' && value.length <= 4096 && !/[\r\n\0]/.test(value);
|
|
8
|
+
/** Measurement only, never a release certificate or atomic path-based exec.
|
|
9
|
+
* Explicit roots are supplied by the trusted host, not task/config JSON. */
|
|
10
|
+
async function measure(input) {
|
|
11
|
+
if (!validPath(input.launchPath) || !Array.isArray(input.allowedRoots) || !input.allowedRoots.length || input.allowedRoots.length > 8 || !input.allowedRoots.every(validPath))
|
|
12
|
+
throw rejected();
|
|
13
|
+
const roots = [...new Set(input.allowedRoots)], uid = process.getuid?.();
|
|
14
|
+
if (uid === undefined)
|
|
15
|
+
throw rejected();
|
|
16
|
+
for (const root of roots) {
|
|
17
|
+
const s = await lstat(root, { bigint: true });
|
|
18
|
+
if (await realpath(root) !== root || !s.isDirectory() || (s.mode & 18n) !== 0n || (s.uid !== BigInt(uid) && s.uid !== 0n))
|
|
19
|
+
throw rejected();
|
|
20
|
+
}
|
|
21
|
+
const allowed = (file) => roots.some(root => inside(root, file));
|
|
22
|
+
const target = await realpath(input.launchPath);
|
|
23
|
+
if (!allowed(input.launchPath) || !allowed(target))
|
|
24
|
+
throw rejected();
|
|
25
|
+
const inspect = async () => {
|
|
26
|
+
const stamps = [];
|
|
27
|
+
for (const file of [...new Set([input.launchPath, target])]) {
|
|
28
|
+
const root = roots.filter(r => inside(r, file)).sort((a, b) => b.length - a.length)[0];
|
|
29
|
+
const parts = [root];
|
|
30
|
+
let current = root;
|
|
31
|
+
for (const part of path.relative(root, file).split(path.sep).filter(Boolean)) {
|
|
32
|
+
current = path.join(current, part);
|
|
33
|
+
parts.push(current);
|
|
34
|
+
}
|
|
35
|
+
for (let i = 0; i < parts.length; i++) {
|
|
36
|
+
const name = parts[i], s = await lstat(name, { bigint: true }), link = s.isSymbolicLink(), last = i === parts.length - 1;
|
|
37
|
+
if ((s.uid !== BigInt(uid) && s.uid !== 0n) || (!link && (s.mode & 18n) !== 0n) || !allowed(await realpath(name))
|
|
38
|
+
|| (!last && !s.isDirectory() && !link) || (last && !link && (!s.isFile() || s.nlink !== 1n || (s.mode & 73n) === 0n)))
|
|
39
|
+
throw rejected();
|
|
40
|
+
stamps.push([name, s.dev.toString(), s.ino.toString(), s.uid.toString(), s.mode.toString(), link ? await readlink(name) : null,
|
|
41
|
+
last || link ? s.mtimeNs.toString() : null, last || link ? s.ctimeNs.toString() : null, last ? s.nlink.toString() : null]);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (await realpath(input.launchPath) !== target)
|
|
45
|
+
throw rejected();
|
|
46
|
+
return JSON.stringify(stamps);
|
|
47
|
+
};
|
|
48
|
+
const before = await inspect(), sha256 = await measureCommandFile(target), after = await inspect();
|
|
49
|
+
if (before !== after)
|
|
50
|
+
throw rejected();
|
|
51
|
+
return { sha256, stamp: after };
|
|
52
|
+
}
|
|
53
|
+
export async function captureClaudeBinaryIdentity(input) {
|
|
54
|
+
try {
|
|
55
|
+
const copy = structuredClone(input), measured = await measure(copy), identity = Object.freeze({ sha256: measured.sha256 });
|
|
56
|
+
live.set(identity, { input: copy, measured });
|
|
57
|
+
return identity;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
throw rejected();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export function claudeBinaryLaunchPath(identity) {
|
|
64
|
+
const value = live.get(identity);
|
|
65
|
+
if (!value)
|
|
66
|
+
throw rejected();
|
|
67
|
+
return value.input.launchPath;
|
|
68
|
+
}
|
|
69
|
+
export async function assertCurrentClaudeBinary(identity, launchPath, sha256) {
|
|
70
|
+
try {
|
|
71
|
+
const value = live.get(identity);
|
|
72
|
+
if (!value || launchPath !== value.input.launchPath || sha256 !== value.measured.sha256)
|
|
73
|
+
throw rejected();
|
|
74
|
+
const current = await measure(value.input);
|
|
75
|
+
if (current.sha256 !== sha256 || current.stamp !== value.measured.stamp)
|
|
76
|
+
throw rejected();
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
throw rejected();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
File without changes
|