mtech-ai-local-runner 0.1.111 → 0.1.113
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 +32 -49
- package/dist/src/adapters/claude-code/adapter.js +16 -7
- package/dist/src/adapters/claude-code/stream-parser.js +65 -4
- package/dist/src/adapters/runtime-failure.js +34 -0
- package/dist/src/asks/claude-mod.js +48 -87
- package/dist/src/asks/commands.js +17 -33
- package/dist/src/asks/doctor.js +3 -4
- package/dist/src/cli.js +3 -3
- package/dist/src/daemon/codex-app-host-entry.js +94 -0
- package/dist/src/daemon/codex-app-setup.js +125 -0
- package/dist/src/daemon/codex-app-tools-host.js +82 -0
- package/dist/src/daemon/execution-coordinator.js +7 -4
- package/dist/src/daemon/runtime.js +74 -11
- package/dist/src/safety/claude-production-host.js +14 -3
- package/dist/src/safety/claude-review-provider.js +9 -1
- package/dist/src/safety/d4-execution-limits.js +23 -11
- package/dist/src/safety/d4-host-composition.js +139 -0
- package/dist/src/safety/d4-reviewed-hosts.js +2 -0
- package/dist/src/safety/d4-runtime-release.js +9 -0
- package/dist/src/verification/p1-execution-host.js +4 -0
- package/package.json +1 -3
- package/claude-mod/.claude-plugin/marketplace.json +0 -1
- package/claude-mod/.claude-plugin/plugin.json +0 -1
- package/claude-mod/hooks/gateway.ts +0 -47
- package/claude-mod/hooks/hooks.json +0 -34
- package/claude-mod/hooks/model.ts +0 -268
- package/claude-mod/hooks/register.tsx +0 -460
- package/claude-mod/tsconfig.json +0 -3
- package/claude-mod/types/index.d.ts +0 -28
package/dist/src/asks/doctor.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { connect } from "node:net";
|
|
3
|
-
import {
|
|
3
|
+
import { statusOf } from "./commands.js";
|
|
4
4
|
import { hubSocketPath } from "./hub.js";
|
|
5
5
|
import { MAX_LINE_BYTES, encodeLine } from "./hub-protocol.js";
|
|
6
6
|
import { CLAUDE_PLUGIN_NAME } from "./integrations.js";
|
|
@@ -93,9 +93,8 @@ export async function asksDoctorReport(input) {
|
|
|
93
93
|
const [status, statuses] = await Promise.all([hubStatus(input.stateRoot), Promise.all(APPS.map(app => statusOf(app, input.env, input.stateRoot)))]);
|
|
94
94
|
const integrations = [];
|
|
95
95
|
for (const report of statuses) {
|
|
96
|
-
const mod = report.app === "claude" ? { mod: await claudeModStatusOf(input.env, input.stateRoot) } : {};
|
|
97
96
|
if (report.state !== "installed") {
|
|
98
|
-
integrations.push(
|
|
97
|
+
integrations.push(report);
|
|
99
98
|
continue;
|
|
100
99
|
}
|
|
101
100
|
if (report.app === "codex") {
|
|
@@ -103,7 +102,7 @@ export async function asksDoctorReport(input) {
|
|
|
103
102
|
continue;
|
|
104
103
|
}
|
|
105
104
|
const loaded = await claudePluginLoaded(input.env, input.run);
|
|
106
|
-
integrations.push({ ...report, ...
|
|
105
|
+
integrations.push({ ...report, ...(loaded === null ? {} : { loaded }), ...(loaded === false ? { action: CLAUDE_NOT_LOADED } : {}) });
|
|
107
106
|
}
|
|
108
107
|
return { mode: input.mode, hub: status === null ? { status: "not_running" } : { status: "running", ...status }, integrations };
|
|
109
108
|
}
|
package/dist/src/cli.js
CHANGED
|
@@ -36,7 +36,7 @@ import { access, mkdir, rm } from "node:fs/promises";
|
|
|
36
36
|
import { buildMessengerDaemon } from "./asks/messenger.js";
|
|
37
37
|
import { removeMenubarInstall, runMenubarCommand, setupMenubarApp } from "./menubar/commands.js";
|
|
38
38
|
import { runHookClient } from "./asks/hook-client.js";
|
|
39
|
-
import { installGuideIntegrations, installIntegrations, parseFeatures, parseSetupIntegrations, removeAsksInstall, removeGuideInstall,
|
|
39
|
+
import { installGuideIntegrations, installIntegrations, parseFeatures, parseSetupIntegrations, removeAsksInstall, removeGuideInstall, retireWorkRoomPane, runIntegrationsCommand } from "./asks/commands.js";
|
|
40
40
|
import { asksDoctorReport } from "./asks/doctor.js";
|
|
41
41
|
import { guideDoctorReport, guideReportShown } from "./guide/doctor.js";
|
|
42
42
|
import { runClaudeReleaseCommand } from "./commands/claude-release.js";
|
|
@@ -316,8 +316,8 @@ export async function main(argv, env, io = {}) {
|
|
|
316
316
|
const features = requestedFeatures ?? ["asks"];
|
|
317
317
|
const integrations = features.includes("asks") ? await installIntegrations(requestedIntegrations ?? [], env, config.stateRoot) : [];
|
|
318
318
|
const guide = features.includes("guide") ? await installGuideIntegrations(requestedIntegrations ?? [], env, config.stateRoot) : undefined;
|
|
319
|
-
// The Work Room pane
|
|
320
|
-
const workRoomPane =
|
|
319
|
+
// The Work Room pane mod is retired (owner decision 2026-11-12): every setup removes the copy an older runner installed.
|
|
320
|
+
const workRoomPane = await retireWorkRoomPane(env, config.stateRoot) ?? undefined;
|
|
321
321
|
// The menu bar app (Work Item b9218937) goes with every setup unless the person removed it.
|
|
322
322
|
const menubar = await setupMenubarApp(env, config.stateRoot);
|
|
323
323
|
// The LaunchAgent runs a copy under releases/<version>, never the npx cache that may have run setup.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { connectCodexAppSetup } from "./codex-app-tools-host.js";
|
|
3
|
+
const object = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
4
|
+
const blocked = () => ({ status: "blocked", creationBlocked: true, gatewayWritesBlocked: true });
|
|
5
|
+
/**
|
|
6
|
+
* One ephemeral, read-only callback invocation across the supported Node REPL/app tools.
|
|
7
|
+
* No network listener, durable inbox, execution aggregate, credentials or live-mode option.
|
|
8
|
+
* The authenticated supervisor dispatches only the returned exact official read call, then
|
|
9
|
+
* supplies its raw MCP response. Correlation IDs identify RPCs, never app/Work Sessions.
|
|
10
|
+
*/
|
|
11
|
+
export class CodexAppHostReadOnlyEntry {
|
|
12
|
+
connection;
|
|
13
|
+
scope;
|
|
14
|
+
started = false;
|
|
15
|
+
closed = false;
|
|
16
|
+
result;
|
|
17
|
+
waiting;
|
|
18
|
+
constructor(scope) {
|
|
19
|
+
this.scope = { ...scope };
|
|
20
|
+
const deny = async () => { throw new Error("Creation and Gateway writes are blocked in read-only host entry"); };
|
|
21
|
+
this.connection = connectCodexAppSetup({ approvedHostId: scope.hostId, approvedProjectId: scope.projectId,
|
|
22
|
+
tools: {
|
|
23
|
+
listProjects: args => this.request({ id: randomUUID(), tool: "list_projects", arguments: args }),
|
|
24
|
+
readThread: args => this.request({ id: randomUUID(), tool: "read_thread", arguments: args }),
|
|
25
|
+
createThread: deny,
|
|
26
|
+
}, claimDispatch: deny, report: deny });
|
|
27
|
+
}
|
|
28
|
+
start(input) {
|
|
29
|
+
if (this.started || this.closed)
|
|
30
|
+
throw new Error("Probe is single-use");
|
|
31
|
+
if (!object(input) || Object.keys(input).some(k => k !== "threadId") || (input.threadId !== undefined
|
|
32
|
+
&& (typeof input.threadId !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(input.threadId))))
|
|
33
|
+
throw new Error("Exact existing thread ID or empty discovery input required");
|
|
34
|
+
const threadId = input.threadId;
|
|
35
|
+
this.started = true;
|
|
36
|
+
void this.probe(threadId).then(result => { if (!this.closed)
|
|
37
|
+
this.result = result; }, () => { if (!this.closed)
|
|
38
|
+
this.result = blocked(); });
|
|
39
|
+
}
|
|
40
|
+
async next() {
|
|
41
|
+
// Flush the local promise chain only; never waits for an external tool or polls a server.
|
|
42
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
43
|
+
if (this.result)
|
|
44
|
+
return { kind: "done", result: { ...this.result } };
|
|
45
|
+
if (!this.waiting)
|
|
46
|
+
return { kind: this.started ? "awaiting_reply" : "idle" };
|
|
47
|
+
if (this.waiting.delivered)
|
|
48
|
+
return { kind: "awaiting_reply" };
|
|
49
|
+
this.waiting.delivered = true;
|
|
50
|
+
return { kind: "tool_call", call: structuredClone(this.waiting.call) };
|
|
51
|
+
}
|
|
52
|
+
reply(id, response) {
|
|
53
|
+
const pending = this.waiting;
|
|
54
|
+
if (this.closed || !pending?.delivered || pending.call.id !== id)
|
|
55
|
+
throw new Error("No matching outstanding official call");
|
|
56
|
+
this.waiting = undefined;
|
|
57
|
+
pending.resolve(response);
|
|
58
|
+
}
|
|
59
|
+
close() {
|
|
60
|
+
this.closed = true;
|
|
61
|
+
this.result = blocked();
|
|
62
|
+
const pending = this.waiting;
|
|
63
|
+
this.waiting = undefined;
|
|
64
|
+
pending?.reject(new Error("Host probe closed"));
|
|
65
|
+
}
|
|
66
|
+
/** No caller-controlled flag, receipt or copied approval can enable side effects here. */
|
|
67
|
+
execute(_input) { return { status: "creation_blocked" }; }
|
|
68
|
+
/** Local fixtures only: exercise the original adapter up to its denied write boundary. */
|
|
69
|
+
startAdapterCheck(input) {
|
|
70
|
+
if (this.started || this.closed)
|
|
71
|
+
throw new Error("Probe is single-use");
|
|
72
|
+
this.started = true;
|
|
73
|
+
void this.connection.adapter.execute(input).then(result => {
|
|
74
|
+
if (!this.closed)
|
|
75
|
+
this.result = { ...blocked(), adapterStatus: result.status };
|
|
76
|
+
}, () => { if (!this.closed)
|
|
77
|
+
this.result = blocked(); });
|
|
78
|
+
}
|
|
79
|
+
request(call) {
|
|
80
|
+
if (this.closed || this.waiting)
|
|
81
|
+
return Promise.reject(new Error("Probe cannot issue another call"));
|
|
82
|
+
return new Promise((resolve, reject) => { this.waiting = { call, delivered: false, resolve, reject }; });
|
|
83
|
+
}
|
|
84
|
+
async probe(threadId) {
|
|
85
|
+
const projects = await this.connection.host.listProjects();
|
|
86
|
+
if (!projects.some(p => p.hostId === this.scope.hostId && p.projectId === this.scope.projectId))
|
|
87
|
+
return blocked();
|
|
88
|
+
if (!threadId)
|
|
89
|
+
return { status: "connected_read_only", creationBlocked: true, gatewayWritesBlocked: true };
|
|
90
|
+
const observation = await this.connection.host.track({ threadId });
|
|
91
|
+
const verified = observation.status === "identity_observed";
|
|
92
|
+
return { ...blocked(), status: verified ? "connected_read_only" : "blocked", identity: verified ? "identity_observed" : "unverified" };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
3
|
+
const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
4
|
+
const text = (value) => typeof value === "string" && value.trim().length > 0;
|
|
5
|
+
const only = (value, allowed) => Object.keys(value).every(key => allowed.includes(key));
|
|
6
|
+
/**
|
|
7
|
+
* Consumes a fresh Gateway prepare receipt, never creates its own authority or binds a recipient.
|
|
8
|
+
* Gateway atomically grants a one-use dispatch claim before the external side effect. The local
|
|
9
|
+
* fence additionally blocks duplicate calls on this adapter, including lost claim acknowledgements.
|
|
10
|
+
* No concrete native endpoint is wired here. A caller must supply the supported authenticated host.
|
|
11
|
+
*/
|
|
12
|
+
export class CodexAppSetupAdapter {
|
|
13
|
+
deps;
|
|
14
|
+
attempted = new Set();
|
|
15
|
+
hostId;
|
|
16
|
+
projectId;
|
|
17
|
+
constructor(deps) {
|
|
18
|
+
this.deps = deps;
|
|
19
|
+
if (!text(deps.approvedHostId) || !text(deps.approvedProjectId))
|
|
20
|
+
throw new Error("Explicit approved host and project required");
|
|
21
|
+
this.hostId = deps.approvedHostId;
|
|
22
|
+
this.projectId = deps.approvedProjectId;
|
|
23
|
+
}
|
|
24
|
+
async execute(input) {
|
|
25
|
+
if (!this.validReceipt(input))
|
|
26
|
+
return { status: "rejected" };
|
|
27
|
+
if (!input.can_create || this.attempted.has(input.intent_id))
|
|
28
|
+
return { status: "skipped" };
|
|
29
|
+
if (input.setup_outcome !== "reserved" || input.target_ref !== null || input.authority_failure !== null
|
|
30
|
+
|| input.thread_id !== undefined || input.pending_handle !== undefined || input.client_link_id !== undefined)
|
|
31
|
+
return { status: "rejected" };
|
|
32
|
+
const args = this.createArguments(input.create_thread);
|
|
33
|
+
if (!args || createHash("sha256").update(args.prompt, "utf8").digest("hex") !== input.setup_prompt_sha256)
|
|
34
|
+
return { status: "rejected" };
|
|
35
|
+
const intentId = input.intent_id;
|
|
36
|
+
const expiresAt = input.expires_at;
|
|
37
|
+
const setupPromptSha256 = input.setup_prompt_sha256;
|
|
38
|
+
// Fence before any await. A discovery failure cannot cause an accidental concurrent retry.
|
|
39
|
+
this.attempted.add(intentId);
|
|
40
|
+
try {
|
|
41
|
+
const projects = await this.deps.host.listProjects();
|
|
42
|
+
if (!Array.isArray(projects) || !projects.some(project => record(project)
|
|
43
|
+
&& project.projectId === this.projectId && project.hostId === this.hostId))
|
|
44
|
+
return { status: "rejected" };
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return { status: "rejected" };
|
|
48
|
+
}
|
|
49
|
+
if (expiresAt <= Date.now() / 1000)
|
|
50
|
+
return { status: "rejected" };
|
|
51
|
+
let claim;
|
|
52
|
+
try {
|
|
53
|
+
claim = await this.deps.claimDispatch({ intent_id: intentId, host_id: this.hostId, project_id: this.projectId, outcome: "dispatch" });
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return { status: "uncertain", reportRecorded: false };
|
|
57
|
+
}
|
|
58
|
+
if (!this.validReceipt(claim) || claim.intent_id !== intentId)
|
|
59
|
+
return { status: "uncertain", reportRecorded: false };
|
|
60
|
+
if (!claim.can_create)
|
|
61
|
+
return { status: "skipped" };
|
|
62
|
+
if (claim.setup_prompt_sha256 !== setupPromptSha256 || createHash("sha256").update(args.prompt, "utf8").digest("hex") !== claim.setup_prompt_sha256
|
|
63
|
+
|| claim.setup_outcome !== "dispatching" || claim.target_ref !== null || claim.authority_failure !== null
|
|
64
|
+
|| claim.thread_id !== undefined || claim.pending_handle !== undefined || claim.client_link_id !== undefined
|
|
65
|
+
|| expiresAt <= Date.now() / 1000)
|
|
66
|
+
return { status: "uncertain", reportRecorded: false };
|
|
67
|
+
const report = { intent_id: intentId, host_id: this.hostId, project_id: this.projectId, outcome: "uncertain" };
|
|
68
|
+
try {
|
|
69
|
+
const result = await this.deps.host.createThread(args);
|
|
70
|
+
if (record(result)) {
|
|
71
|
+
if (text(result.threadId) && UUID.test(result.threadId) && result.hostId === this.hostId && result.clientThreadId === undefined) {
|
|
72
|
+
report.outcome = "created";
|
|
73
|
+
report.thread_id = result.threadId;
|
|
74
|
+
}
|
|
75
|
+
else if (text(result.clientThreadId) && result.threadId === undefined
|
|
76
|
+
&& (result.hostId === undefined || result.hostId === this.hostId)) {
|
|
77
|
+
report.outcome = "pending";
|
|
78
|
+
report.pending_handle = result.clientThreadId;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// The host may have created the thread before losing its acknowledgement. Never replay.
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
await this.deps.report(report);
|
|
87
|
+
return { status: report.outcome, reportRecorded: true };
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// Even a successful host response is not reconciled when Gateway's acknowledgement is lost.
|
|
91
|
+
return { status: "uncertain", reportRecorded: false };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
validReceipt(input) {
|
|
95
|
+
if (!record(input) || !only(input, ["intent_id", "host_id", "project_id", "can_create", "create_thread", "setup_outcome", "target_ref", "grants_execution", "expires_at", "authority_failure", "thread_id", "pending_handle", "client_link_id", "setup_prompt_sha256"])
|
|
96
|
+
|| !text(input.intent_id) || !UUID.test(input.intent_id) || input.host_id !== this.hostId || input.project_id !== this.projectId
|
|
97
|
+
|| typeof input.setup_prompt_sha256 !== "string" || !/^[0-9a-f]{64}$/.test(input.setup_prompt_sha256)
|
|
98
|
+
|| typeof input.can_create !== "boolean" || input.grants_execution !== false
|
|
99
|
+
|| typeof input.expires_at !== "number" || !Number.isSafeInteger(input.expires_at) || input.expires_at <= Date.now() / 1000
|
|
100
|
+
|| (input.authority_failure !== null && !text(input.authority_failure))
|
|
101
|
+
|| !text(input.setup_outcome) || (input.target_ref !== null && !text(input.target_ref)))
|
|
102
|
+
return false;
|
|
103
|
+
for (const key of ["thread_id", "client_link_id"]) {
|
|
104
|
+
if (input[key] !== undefined && (!text(input[key]) || !UUID.test(input[key])))
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
if (input.pending_handle !== undefined && (!text(input.pending_handle) || !/^[a-zA-Z0-9_:.-]{1,128}$/.test(input.pending_handle)))
|
|
108
|
+
return false;
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
createArguments(input) {
|
|
112
|
+
if (!record(input) || !only(input, ["prompt", "title", "target"]) || !text(input.prompt)
|
|
113
|
+
|| (input.title !== undefined && !text(input.title)))
|
|
114
|
+
return null;
|
|
115
|
+
const target = input.target;
|
|
116
|
+
if (!record(target) || !only(target, ["type", "projectId", "environment"])
|
|
117
|
+
|| target.type !== "project" || target.projectId !== this.projectId
|
|
118
|
+
|| !record(target.environment) || !only(target.environment, ["type"])
|
|
119
|
+
|| target.environment.type !== "local")
|
|
120
|
+
return null;
|
|
121
|
+
// Copy only validated fields before awaiting discovery; do not pass mutable caller objects.
|
|
122
|
+
return { prompt: input.prompt, ...(input.title === undefined ? {} : { title: input.title }),
|
|
123
|
+
target: { type: "project", projectId: this.projectId, environment: { type: "local" } } };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { CodexAppSetupAdapter } from "./codex-app-setup.js";
|
|
2
|
+
const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
3
|
+
const uuid = (value) => typeof value === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
|
|
4
|
+
/** The observed official host envelope has one JSON text block. Fail closed on schema drift. */
|
|
5
|
+
function data(value) {
|
|
6
|
+
if (!object(value) || (value.isError !== undefined && value.isError !== false)
|
|
7
|
+
|| !Array.isArray(value.content) || value.content.length !== 1)
|
|
8
|
+
throw new Error("Unsupported app tool envelope");
|
|
9
|
+
const block = value.content[0];
|
|
10
|
+
if (!object(block) || block.type !== "text" || typeof block.text !== "string")
|
|
11
|
+
throw new Error("Missing app tool JSON");
|
|
12
|
+
const parsed = JSON.parse(block.text);
|
|
13
|
+
if (!object(parsed))
|
|
14
|
+
throw new Error("Unsupported app tool result");
|
|
15
|
+
return parsed;
|
|
16
|
+
}
|
|
17
|
+
export class CodexAppToolsHost {
|
|
18
|
+
tools;
|
|
19
|
+
hostId;
|
|
20
|
+
constructor(tools, hostId) {
|
|
21
|
+
this.tools = tools;
|
|
22
|
+
this.hostId = hostId;
|
|
23
|
+
}
|
|
24
|
+
async listProjects() {
|
|
25
|
+
const result = data(await this.tools.listProjects({}));
|
|
26
|
+
if (result.schemaVersion !== 2 || !Array.isArray(result.projects))
|
|
27
|
+
throw new Error("Unsupported project schema");
|
|
28
|
+
return result.projects.filter((project) => object(project)
|
|
29
|
+
&& project.projectKind === "local" && typeof project.projectId === "string" && project.projectId.length > 0
|
|
30
|
+
&& project.hostId === this.hostId).map(project => ({ projectId: project.projectId, hostId: this.hostId }));
|
|
31
|
+
}
|
|
32
|
+
async createThread(args) {
|
|
33
|
+
// Only the existing adapter calls this after Gateway's durable dispatch claim.
|
|
34
|
+
return data(await this.tools.createThread(args));
|
|
35
|
+
}
|
|
36
|
+
/** Observation only: never binds a session, reports bound, starts a turn or retries creation. */
|
|
37
|
+
async track(receipt) {
|
|
38
|
+
const unverified = { status: "unverified", grantsExecution: false };
|
|
39
|
+
if (!object(receipt) || Object.keys(receipt).some(key => !["threadId", "pendingHandle"].includes(key)))
|
|
40
|
+
return unverified;
|
|
41
|
+
if (receipt.pendingHandle !== undefined) {
|
|
42
|
+
if (receipt.threadId !== undefined || typeof receipt.pendingHandle !== "string"
|
|
43
|
+
|| !/^[a-zA-Z0-9_:.-]{1,128}$/.test(receipt.pendingHandle))
|
|
44
|
+
return unverified;
|
|
45
|
+
// No exposed app tool resolves clientThreadId. In particular do not feed it to read_thread.
|
|
46
|
+
return { status: "pending_resolution_unsupported", pendingHandle: receipt.pendingHandle, grantsExecution: false };
|
|
47
|
+
}
|
|
48
|
+
if (!uuid(receipt.threadId))
|
|
49
|
+
return unverified;
|
|
50
|
+
const threadId = receipt.threadId;
|
|
51
|
+
try {
|
|
52
|
+
const result = data(await this.tools.readThread({ threadId, hostId: this.hostId, turnLimit: 1, includeOutputs: false, maxOutputCharsPerItem: 100 }));
|
|
53
|
+
if (result.schemaVersion !== 1 || !object(result.thread) || result.thread.id !== threadId
|
|
54
|
+
|| result.thread.hostId !== this.hostId || result.thread.kind !== "codex")
|
|
55
|
+
return unverified;
|
|
56
|
+
return { status: "identity_observed", threadId, hostId: this.hostId, grantsExecution: false };
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return unverified;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export function connectCodexAppSetup(deps) {
|
|
64
|
+
const host = new CodexAppToolsHost(deps.tools, deps.approvedHostId);
|
|
65
|
+
return { host, adapter: new CodexAppSetupAdapter({ ...deps, host }),
|
|
66
|
+
/** Pass normalized data from Gateway report(outcome: status), not an old prepare response. */
|
|
67
|
+
trackReceipt: async (receipt) => {
|
|
68
|
+
const unverified = { status: "unverified", grantsExecution: false };
|
|
69
|
+
if (!object(receipt) || !uuid(receipt.intent_id) || receipt.host_id !== deps.approvedHostId
|
|
70
|
+
|| receipt.project_id !== deps.approvedProjectId || receipt.can_create !== false
|
|
71
|
+
|| receipt.grants_execution !== false || receipt.authority_failure !== null
|
|
72
|
+
|| receipt.create_thread !== undefined)
|
|
73
|
+
return unverified;
|
|
74
|
+
if (receipt.setup_outcome === "pending" && receipt.thread_id === undefined && typeof receipt.pending_handle === "string") {
|
|
75
|
+
return host.track({ pendingHandle: receipt.pending_handle });
|
|
76
|
+
}
|
|
77
|
+
if (receipt.setup_outcome === "created" && uuid(receipt.thread_id)) {
|
|
78
|
+
return host.track({ threadId: receipt.thread_id });
|
|
79
|
+
}
|
|
80
|
+
return unverified;
|
|
81
|
+
} };
|
|
82
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { assertClaimedExecutionAttempt, assertBoundExecutionAttempt } from '../verification/continuation-runner-contract.js';
|
|
2
2
|
import { completionRejectionDiagnostic } from '../adapters/completion-report.js';
|
|
3
|
+
import { runtimeFailureDiagnostic } from '../adapters/runtime-failure.js';
|
|
3
4
|
import { assertCandidateReviewPackages, candidateReviewReport } from '../safety/candidate-review-report.js';
|
|
4
|
-
import {
|
|
5
|
+
import { isD4ExecutionSchema } from '../safety/d4-execution-limits.js';
|
|
5
6
|
import { publicationError } from '../publication/failure.js';
|
|
6
7
|
import { PublicationOutcomeUnknownError } from '../publication/publisher.js';
|
|
7
8
|
import { assertP1Bindings, hasP1, p1ModelEvent } from '../verification/p1-execution-host.js';
|
|
@@ -179,7 +180,7 @@ export class ExecutionCoordinator {
|
|
|
179
180
|
assertBoundExecutionAttempt(job, binding.taskPackage, binding.iterationId);
|
|
180
181
|
if (executionAttempt && binding.bindingReplayed !== false)
|
|
181
182
|
throw Error('CORRECTION_BINDING_RECONCILIATION_REQUIRED');
|
|
182
|
-
if (pkg.candidate_review_execution?.schema_version
|
|
183
|
+
if (isD4ExecutionSchema(pkg.candidate_review_execution?.schema_version)
|
|
183
184
|
&& (job.generation !== 1 || binding.bindingReplayed !== false))
|
|
184
185
|
throw Error('D4_BINDING_RECONCILIATION_REQUIRED');
|
|
185
186
|
binding = immutable(structuredClone(binding));
|
|
@@ -449,7 +450,7 @@ export class ExecutionCoordinator {
|
|
|
449
450
|
if (isP1 && !hostProof)
|
|
450
451
|
event = p1ModelEvent(event);
|
|
451
452
|
const previousStage = executionStage;
|
|
452
|
-
const d4StopAck = job.taskPackage.candidate_review_execution?.schema_version
|
|
453
|
+
const d4StopAck = isD4ExecutionSchema(job.taskPackage.candidate_review_execution?.schema_version)
|
|
453
454
|
&& ['failed', 'cancelled', 'blocked'].includes(event.eventType);
|
|
454
455
|
let payload = event.payload;
|
|
455
456
|
if (isTerminalEventType(event.eventType) && !d4StopAck) {
|
|
@@ -833,6 +834,8 @@ export class ExecutionCoordinator {
|
|
|
833
834
|
const reportDiagnostic = diagnostic && (status === "blocked"
|
|
834
835
|
? diagnostic.code === "CODEX_COMPLETION_REPORT_BLOCKED" : diagnostic.code !== "CODEX_COMPLETION_REPORT_BLOCKED") ? diagnostic : undefined;
|
|
835
836
|
const rejectedCompletion = completionRejectionDiagnostic(terminal.payload?.completion_rejection);
|
|
837
|
+
// Why the provider CLI stopped, as a bounded code; only on a failed job.
|
|
838
|
+
const runtimeFailure = status === 'failed' ? runtimeFailureDiagnostic(terminal.payload?.runtime_failure) : undefined;
|
|
836
839
|
const rejectedCounts = reportDiagnostic?.rejection_counts;
|
|
837
840
|
if (rejectedCompletion || rejectedCounts) {
|
|
838
841
|
try {
|
|
@@ -842,7 +845,7 @@ export class ExecutionCoordinator {
|
|
|
842
845
|
}
|
|
843
846
|
const securityReceipt = adapter.securityReceipt?.();
|
|
844
847
|
const reads = safeLocalReadObservations(adapter.localReadObservations?.());
|
|
845
|
-
const completionRequest = structuredClone({ jobId: job.id, generation: job.generation, leaseToken: job.leaseToken, status, result: { binding_id: binding.bindingId, event_type: terminal.eventType, summary: terminal.summary, ...(localFixPublication ? { local_fix_publication: structuredClone(localFixPublication) } : {}), ...(reason ? { reason } : {}), ...(reads ? { local_read_observations: reads } : {}), ...(failureDiagnostic ? { execution_failure: { ...failureDiagnostic } } : {}), ...(reportDiagnostic ? { completion_report: reportDiagnostic } : {}), ...(rejectedCompletion ? { completion_rejection: rejectedCompletion } : {}), ...(securityReceipt ? { security_receipt: securityReceipt } : {}) } });
|
|
848
|
+
const completionRequest = structuredClone({ jobId: job.id, generation: job.generation, leaseToken: job.leaseToken, status, result: { binding_id: binding.bindingId, event_type: terminal.eventType, summary: terminal.summary, ...(localFixPublication ? { local_fix_publication: structuredClone(localFixPublication) } : {}), ...(reason ? { reason } : {}), ...(reads ? { local_read_observations: reads } : {}), ...(failureDiagnostic ? { execution_failure: { ...failureDiagnostic } } : {}), ...(reportDiagnostic ? { completion_report: reportDiagnostic } : {}), ...(rejectedCompletion ? { completion_rejection: rejectedCompletion } : {}), ...(runtimeFailure ? { runtime_failure: runtimeFailure } : {}), ...(securityReceipt ? { security_receipt: securityReceipt } : {}) } });
|
|
846
849
|
await this.deps.state.save({ jobId: job.id, generation: job.generation, leaseToken: job.leaseToken,
|
|
847
850
|
phase: "terminal_pending", bindingId: binding.bindingId, terminalEventType: terminal.eventType,
|
|
848
851
|
completionRequest: structuredClone(completionRequest) });
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { composeReviewedD4Host } from '../safety/d4-host-composition.js';
|
|
2
|
+
import { protocolHash } from '../publication/candidate.js';
|
|
1
3
|
import { createClaudeProductionHost } from '../safety/claude-production-host.js';
|
|
2
4
|
import { adapterStatus } from '../adapters/status.js';
|
|
3
5
|
import { loadInstalledLocalFixHost } from '../publication/installed-host.js';
|
|
@@ -15,7 +17,7 @@ import { CodexPlugin } from "../adapters/codex/plugin.js";
|
|
|
15
17
|
import { buildCodexJobOptions } from "../adapters/codex/runtime-options.js";
|
|
16
18
|
import { AdapterRegistry } from "../adapters/registry.js";
|
|
17
19
|
import { ClaudeCodePlugin } from "../adapters/claude-code/adapter.js";
|
|
18
|
-
import { D4_OBSERVED_EXECUTION_CAPABILITY } from '../safety/d4-execution-limits.js';
|
|
20
|
+
import { D4_OBSERVED_EXECUTION_CAPABILITY, D4_RECORDED_EXECUTION_CAPABILITY } from '../safety/d4-execution-limits.js';
|
|
19
21
|
import { createClaudeCodeAdapterOptions } from "../adapters/claude-code/runtime-options.js";
|
|
20
22
|
import { ClaudeRunnerTokenStore, ensureClaudeKeychainStub } from "../safety/claude-runner-token.js";
|
|
21
23
|
import { WorkspaceManager } from "../workspace/workspace-manager.js";
|
|
@@ -76,6 +78,39 @@ export async function buildDaemon(input) {
|
|
|
76
78
|
pendingCodexClaim.job.lease_expires_at = expiresAt;
|
|
77
79
|
};
|
|
78
80
|
const gateway = new McpGatewayClient({ mcpUrl: input.gatewayMcpUrl, tokenProvider: input.tokenProvider, runnerId: input.runnerId, fetchFn: input.fetchFn });
|
|
81
|
+
// Only this daemon's currently executing claim may revalidate a D4 pre-spawn request.
|
|
82
|
+
let activeD4Job, activeD4Binding;
|
|
83
|
+
let d4Host = input.d4ExecutionHost, d4ProductionBinding;
|
|
84
|
+
if (!d4Host) {
|
|
85
|
+
const composition = await composeReviewedD4Host({ currentJob: () => {
|
|
86
|
+
const job = activeD4Job;
|
|
87
|
+
if (!job || !activeD4Binding)
|
|
88
|
+
return undefined;
|
|
89
|
+
const pkg = job.taskPackage, repository = pkg.repository;
|
|
90
|
+
return { descriptor: pkg.candidate_review_execution, context: { jobId: job.id, bindingId: activeD4Binding, generation: job.generation,
|
|
91
|
+
launchFingerprint: job.launchFingerprint, contractFingerprint: job.gatewayContractFingerprint, taskPackageFingerprint: job.gatewayTaskPackageFingerprint },
|
|
92
|
+
request: { adapterId: String(pkg.runtime_adapter), adapterVersion: String(pkg.runtime_adapter_version), jobId: job.id, generation: job.generation,
|
|
93
|
+
modelId: String(pkg.resolved_model_id), sourceSha: String(repository?.base_commit_sha), requirements: pkg.security_requirements,
|
|
94
|
+
launchFingerprint: job.launchFingerprint, contractFingerprint: job.gatewayContractFingerprint, taskPackageFingerprint: job.gatewayTaskPackageFingerprint } };
|
|
95
|
+
}, binaryIdentity: () => claude.binaryIdentity(), revalidateJob: async (descriptor, context) => {
|
|
96
|
+
const job = activeD4Job;
|
|
97
|
+
if (!job || context.jobId !== job.id || context.bindingId !== activeD4Binding || context.generation !== 1 || job.generation !== 1
|
|
98
|
+
|| context.launchFingerprint !== job.launchFingerprint || context.contractFingerprint !== job.gatewayContractFingerprint
|
|
99
|
+
|| context.taskPackageFingerprint !== job.gatewayTaskPackageFingerprint
|
|
100
|
+
|| protocolHash(job.taskPackage) !== job.gatewayTaskPackageFingerprint
|
|
101
|
+
|| protocolHash(job.taskPackage.candidate_review_execution) !== protocolHash(descriptor))
|
|
102
|
+
throw Error('D4_JOB_CONTEXT_REJECTED');
|
|
103
|
+
const { job: current } = await gateway.heartbeatLocalJob({ jobId: job.id, generation: job.generation, leaseToken: job.leaseToken });
|
|
104
|
+
if (activeD4Job !== job || activeD4Binding !== context.bindingId || current.id !== job.id || current.generation !== 1
|
|
105
|
+
|| !['claimed', 'shadowing', 'running'].includes(current.status) || !Number.isSafeInteger(current.lease_expires_at)
|
|
106
|
+
|| current.lease_expires_at <= Math.floor(Date.now() / 1000))
|
|
107
|
+
throw Error('D4_JOB_CONTEXT_REJECTED');
|
|
108
|
+
} });
|
|
109
|
+
d4Host = composition.host;
|
|
110
|
+
if (composition.status === 'composed')
|
|
111
|
+
d4ProductionBinding = composition.productionBinding;
|
|
112
|
+
process.stderr.write(`${JSON.stringify({ event: 'd4_host', status: composition.status })}\n`);
|
|
113
|
+
}
|
|
79
114
|
// The menu bar app (Work Item b9218937): counts and clickable notices; osascript stays the fallback.
|
|
80
115
|
let roomActivityEnabled = false;
|
|
81
116
|
const menubar = buildMenubar({ stateRoot: input.stateRoot, home: input.env.HOME ?? "", gatewayMcpUrl: input.gatewayMcpUrl,
|
|
@@ -121,10 +156,12 @@ export async function buildDaemon(input) {
|
|
|
121
156
|
options: async ({ worktreePath, jobId }) => createClaudeCodeAdapterOptions({
|
|
122
157
|
worktreePath, jobId, stateRoot: input.stateRoot, env: input.env,
|
|
123
158
|
maxBudgetUsd: Number.MAX_SAFE_INTEGER, hookExecutablePath: hook.hookScriptPath, hookNodeExecutablePath: hook.nodeExecutablePath,
|
|
124
|
-
securityHost:
|
|
159
|
+
securityHost: activeD4Job?.id === jobId && claudeProduction
|
|
160
|
+
? d4ProductionBinding ? claudeProduction.d4SecurityHost(d4ProductionBinding) : async () => { throw Error('D4_REVIEW_HOST_REQUIRED'); }
|
|
161
|
+
: claude.securityHost, binaryIdentity: await claude.binaryIdentity(),
|
|
125
162
|
pullRequestEvidence: (args, signal) => gateway.readPullRequestEvidence(args, signal),
|
|
126
163
|
readCandidateReviewContext: loopId => gateway.readCandidateReviewContext(loopId),
|
|
127
|
-
d4ExecutionHost:
|
|
164
|
+
d4ExecutionHost: d4Host,
|
|
128
165
|
}),
|
|
129
166
|
});
|
|
130
167
|
const codex = new CodexPlugin({
|
|
@@ -156,10 +193,15 @@ export async function buildDaemon(input) {
|
|
|
156
193
|
shadow: (job, signal) => gateway.localJobShadow({ workLoopId: job.workLoopId,
|
|
157
194
|
contractFingerprint: job.gatewayContractFingerprint ?? "",
|
|
158
195
|
taskPackageFingerprint: job.gatewayTaskPackageFingerprint ?? "" }, signal),
|
|
159
|
-
createBinding: async (job, signal) =>
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
196
|
+
createBinding: async (job, signal) => {
|
|
197
|
+
const binding = await gateway.createLocalJobBinding({
|
|
198
|
+
jobId: job.id, generation: job.generation, jobLeaseToken: job.leaseToken,
|
|
199
|
+
launchGrant: job.launchGrant, workLoopId: job.workLoopId, checkoutPath: job.checkoutPath, claimedJob: job,
|
|
200
|
+
}, signal);
|
|
201
|
+
if (activeD4Job?.id === job.id)
|
|
202
|
+
activeD4Binding = binding.bindingId;
|
|
203
|
+
return binding;
|
|
204
|
+
},
|
|
163
205
|
publicationRecoveryContext: bindingId => gateway.exportLocalPublicationRecoveryContext(bindingId),
|
|
164
206
|
syncPublicationCursor: (bindingId, cursor, job, receipt) => gateway.syncLocalPublicationCursor(bindingId, cursor, job, receipt),
|
|
165
207
|
confirmServerPublicationCompletion: (bindingId, cursor, job, receipt) => gateway.confirmServerPublicationCompletion(bindingId, cursor, job, receipt),
|
|
@@ -221,7 +263,22 @@ export async function buildDaemon(input) {
|
|
|
221
263
|
return { status: "claimed", job };
|
|
222
264
|
},
|
|
223
265
|
execute: async (job) => {
|
|
224
|
-
|
|
266
|
+
const claimed = job;
|
|
267
|
+
if (claimed.taskPackage.candidate_review_execution) {
|
|
268
|
+
if (activeD4Job)
|
|
269
|
+
throw Error('D4_JOB_CONTEXT_REJECTED');
|
|
270
|
+
activeD4Job = structuredClone(claimed);
|
|
271
|
+
activeD4Binding = undefined;
|
|
272
|
+
try {
|
|
273
|
+
await coordinator.execute(claimed);
|
|
274
|
+
}
|
|
275
|
+
finally {
|
|
276
|
+
activeD4Job = undefined;
|
|
277
|
+
activeD4Binding = undefined;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
else
|
|
281
|
+
await coordinator.execute(claimed);
|
|
225
282
|
},
|
|
226
283
|
requestSync: reason => { transport?.requestSync(reason); },
|
|
227
284
|
// Until now the heartbeat said this runner takes no jobs; tell the Gateway at once, and ask whether one waits.
|
|
@@ -298,10 +355,16 @@ export async function buildDaemon(input) {
|
|
|
298
355
|
}
|
|
299
356
|
const askInbox = await asks.advertisement();
|
|
300
357
|
const guidance = await guide.advertisement();
|
|
358
|
+
let d4Ready;
|
|
359
|
+
try {
|
|
360
|
+
if (d4Host && (!d4Host.ready || await d4Host.ready()))
|
|
361
|
+
d4Ready = d4Host;
|
|
362
|
+
}
|
|
363
|
+
catch { /* D4 readiness never disables other workflows. */ }
|
|
301
364
|
return {
|
|
302
365
|
deviceFingerprint: runnerDeviceFingerprint(input.gatewayMcpUrl),
|
|
303
366
|
displayName: runnerDisplayName(), packageVersion: packageVersion(),
|
|
304
|
-
capabilities: acceptingJobs(localRunnerCapabilities(probes, !!localFixHost, localFixHost?.verificationModes ?? localFixHost?.verificationMode, ['claude_code_cli', ...(production ? ['codex_cli'] : [])],
|
|
367
|
+
capabilities: acceptingJobs(localRunnerCapabilities(probes, !!localFixHost, localFixHost?.verificationModes ?? localFixHost?.verificationMode, ['claude_code_cli', ...(production ? ['codex_cli'] : [])], d4Ready), poller.accepting),
|
|
305
368
|
mode: "full",
|
|
306
369
|
...(sessionOpen ? { sessionOpen: { ...sessionOpen, ...(askInbox ? { ask_inbox: askInbox } : {}) } } : {}),
|
|
307
370
|
...(guidance ? { guidance } : {}),
|
|
@@ -471,7 +534,7 @@ export function localRunnerCapabilities(probes, trustedLocalFixHost = false, ver
|
|
|
471
534
|
return probes.map(probe => {
|
|
472
535
|
const status = adapterStatus(probe);
|
|
473
536
|
const authenticated = status.authenticated;
|
|
474
|
-
const capabilities = probe.capabilities.filter(value => value !== 'local_fix_publication_v1' && value !== 'local_fix_gateway_ci_v1' && value !== 'local_fix_gateway_publish_v1' && value !== 'pull_request_evidence_v1' && value !== D4_OBSERVED_EXECUTION_CAPABILITY);
|
|
537
|
+
const capabilities = probe.capabilities.filter(value => value !== 'local_fix_publication_v1' && value !== 'local_fix_gateway_ci_v1' && value !== 'local_fix_gateway_publish_v1' && value !== 'pull_request_evidence_v1' && value !== D4_OBSERVED_EXECUTION_CAPABILITY && value !== D4_RECORDED_EXECUTION_CAPABILITY);
|
|
475
538
|
if (trustedLocalFixHost && authenticated && isLocalFixAdapter(probe.id)) {
|
|
476
539
|
capabilities.push('local_fix_publication_v1');
|
|
477
540
|
if (modes.includes('gateway_ci_v1'))
|
|
@@ -484,7 +547,7 @@ export function localRunnerCapabilities(probes, trustedLocalFixHost = false, ver
|
|
|
484
547
|
capabilities.push('pull_request_evidence_v1');
|
|
485
548
|
const d4 = authenticated && probe.id === 'claude_code_cli' && d4Host;
|
|
486
549
|
if (d4)
|
|
487
|
-
capabilities.push(D4_OBSERVED_EXECUTION_CAPABILITY);
|
|
550
|
+
capabilities.push(...(d4.capabilities ?? [D4_OBSERVED_EXECUTION_CAPABILITY, D4_RECORDED_EXECUTION_CAPABILITY]));
|
|
488
551
|
return { adapter: probe.id, version: probe.version, ...status, capabilities, ...(d4 ? { runner_release_sha256: d4.runnerReleaseSha256, host_policy_sha256: d4.hostPolicySha256 } : {}) };
|
|
489
552
|
});
|
|
490
553
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
import { assertReviewedD4Request } from './d4-host-composition.js';
|
|
2
3
|
import { execFile, spawn } from 'node:child_process';
|
|
3
4
|
import { CLAUDE_TOKEN_FD, CLAUDE_TOKEN_FD_ENV, pathWithKeychainStub } from './claude-runner-token.js';
|
|
4
5
|
import { lstat, realpath } from 'node:fs/promises';
|
|
@@ -256,13 +257,15 @@ export function createClaudeProductionHost(input) {
|
|
|
256
257
|
};
|
|
257
258
|
const blocked = (code, executableVersion) => ({ available: false, authenticated: false, loginStatus: 'unknown', capabilities: [], reason: code,
|
|
258
259
|
...(executableVersion ? { executableVersion } : {}), admission: { checks: [{ id: CHECK_IDS[code] ?? 'release_copy', status: 'blocked', code }] } });
|
|
259
|
-
const
|
|
260
|
+
const createSecurityHost = (d4) => async (request) => {
|
|
260
261
|
if (closed)
|
|
261
262
|
throw Error('CLAUDE_HOST_CLOSED');
|
|
262
263
|
const requirements = requireClaudeRequirements(request.requirements, { adapterId: request.adapterId, adapterVersion: request.adapterVersion }), release = requirements.release;
|
|
264
|
+
if (d4)
|
|
265
|
+
await assertReviewedD4Request(d4, request);
|
|
263
266
|
const { release: synced, binary } = await verifiedCopy();
|
|
264
267
|
if (release.version !== synced.version || release.platform !== synced.platform || release.binary_sha256 !== synced.binary_sha256
|
|
265
|
-
|| !isDeepStrictEqual([...release.model_ids], [...synced.model_ids]) || release.platform !== platform || !release.model_ids.includes(request.modelId))
|
|
268
|
+
|| (!d4 && !isDeepStrictEqual([...release.model_ids], [...synced.model_ids])) || release.platform !== platform || !release.model_ids.includes(request.modelId))
|
|
266
269
|
throw Error('CLAUDE_RELEASE_BINDING_REJECTED');
|
|
267
270
|
const tools = claudeReleaseToolInventory(release);
|
|
268
271
|
await verifyClaudeCodeSignature(binary, input.codesign);
|
|
@@ -277,6 +280,8 @@ export function createClaudeProductionHost(input) {
|
|
|
277
280
|
verify: async (captured) => {
|
|
278
281
|
if (closed || captured.binarySha256 !== release.binary_sha256 || captured.securityMode !== CLAUDE_RELEASE_MODE)
|
|
279
282
|
throw Error('CLAUDE_RELEASE_BINDING_REJECTED');
|
|
283
|
+
if (d4)
|
|
284
|
+
await assertReviewedD4Request(d4, request);
|
|
280
285
|
await verifyClaudeReleaseCopy(input.home, release).catch(error => { probeMemo = undefined; throw error; });
|
|
281
286
|
return { version: 2, pin: captured, checks: CLAUDE_MANDATORY_GATES.map(gate => ({ gate, status: 'passed' })) };
|
|
282
287
|
} };
|
|
@@ -328,7 +333,13 @@ export function createClaudeProductionHost(input) {
|
|
|
328
333
|
const { binary } = await verifiedCopy();
|
|
329
334
|
return captureClaudeBinaryIdentity({ launchPath: binary, allowedRoots: [claudeReleaseRoot(input.home)] });
|
|
330
335
|
},
|
|
331
|
-
securityHost,
|
|
336
|
+
securityHost: createSecurityHost(),
|
|
337
|
+
/** A separate capability issued by reviewed composition; no task/env field can relax generic model equality. */
|
|
338
|
+
d4SecurityHost(binding) {
|
|
339
|
+
if (!binding)
|
|
340
|
+
throw Error('D4_HOST_INPUT_REJECTED');
|
|
341
|
+
return createSecurityHost(binding);
|
|
342
|
+
},
|
|
332
343
|
async close() { closed = true; abort.abort(); await pending; },
|
|
333
344
|
};
|
|
334
345
|
}
|
|
@@ -23,6 +23,14 @@ function textOf(content) {
|
|
|
23
23
|
const parts = content.filter((block) => record(block) && block.type === 'text' && typeof block.text === 'string').map(block => block.text);
|
|
24
24
|
return parts.length ? parts.join('') : null;
|
|
25
25
|
}
|
|
26
|
+
/** Tool evidence accepts text only. Never discard an unrecognized block and salvage a passing JSON object. */
|
|
27
|
+
function toolResultText(content) {
|
|
28
|
+
if (typeof content === 'string')
|
|
29
|
+
return content;
|
|
30
|
+
if (!Array.isArray(content) || content.length === 0 || !content.every((block) => record(block) && Object.keys(block).length === 2 && block.type === 'text' && typeof block.text === 'string'))
|
|
31
|
+
return 'CLAUDE_REVIEW_TOOL_RESULT_CONTENT_REJECTED';
|
|
32
|
+
return content.map(block => block.text).join('');
|
|
33
|
+
}
|
|
26
34
|
const REMINDER = /<system-reminder>[\s\S]*?<\/system-reminder>/g;
|
|
27
35
|
/** The user's own text. The real CLI prepends context in system-reminder tags, in its own block or the same one. */
|
|
28
36
|
function userText(content) {
|
|
@@ -131,7 +139,7 @@ export async function startClaudeReviewProvider(script) {
|
|
|
131
139
|
continue;
|
|
132
140
|
toolResultIds.push(block.tool_use_id);
|
|
133
141
|
if (!toolResults.has(block.tool_use_id))
|
|
134
|
-
toolResults.set(block.tool_use_id, { isError: block.is_error === true, text:
|
|
142
|
+
toolResults.set(block.tool_use_id, { isError: block.is_error === true, text: toolResultText(block.content) });
|
|
135
143
|
}
|
|
136
144
|
const firstUser = messages.find(message => message.role === 'user'), { text: firstUserText, reminders } = firstUser ? userText(firstUser.content) : { text: null, reminders: 0 };
|
|
137
145
|
// The scripted turn: the launched model, the exact review prompt as the first user text and an offered tool list.
|