mtech-ai-local-runner 0.1.110 → 0.1.112
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 +30 -0
- package/dist/src/adapters/claude-code/adapter.js +121 -10
- package/dist/src/adapters/claude-code/runtime-options.js +1 -0
- package/dist/src/adapters/claude-code/stream-parser.js +65 -4
- package/dist/src/adapters/runtime-failure.js +34 -0
- package/dist/src/daemon/execution-coordinator.js +15 -4
- package/dist/src/daemon/runtime.js +65 -9
- package/dist/src/gateway-client.js +101 -0
- package/dist/src/menubar/channel.js +3 -3
- package/dist/src/menubar/panel.js +24 -5
- package/dist/src/menubar/protocol.js +43 -2
- package/dist/src/menubar/room-panel.js +115 -0
- package/dist/src/menubar/wiring.js +2 -1
- package/dist/src/safety/candidate-review-report.js +4 -1
- package/dist/src/safety/claude-review-provider.js +9 -1
- package/dist/src/safety/d4-execution-limits.js +74 -7
- package/dist/src/safety/d4-host-composition.js +100 -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/native/app/AI Gateway.app/Contents/Info.plist +2 -2
- package/native/app/AI Gateway.app/Contents/MacOS/AIGatewayMenubar +0 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,3 +1,33 @@
|
|
|
1
|
+
## 0.1.112: a failed Claude job says why it stopped
|
|
2
|
+
|
|
3
|
+
When a Claude job fails, the runner now tells the Gateway why, as one code: `auth` (the Claude account's sign-in was
|
|
4
|
+
refused or expired), `usage_limit`, `model_unavailable`, `rate_limited`, `provider_error`, `budget_exceeded`,
|
|
5
|
+
`cli_error` or `unknown` (Gateway Work Item 2ee28765). The Sentry "AI fix failed" card then says, for example, "Claude
|
|
6
|
+
stopped before the first answer: usage limit reached on the runner's Claude account." The runner reads only the Claude
|
|
7
|
+
CLI's error kind and HTTP status; it never sends the CLI's own text. If the card says `auth`, sign Claude in again on
|
|
8
|
+
this Mac (or renew the runner's token with `setup --claude-token`) and retry.
|
|
9
|
+
|
|
10
|
+
0.1.112 also carries the D4 work merged after 0.1.111 (PRs #943, #949, #957). Reviewed-host registration stays empty,
|
|
11
|
+
so D4 execution stays closed.
|
|
12
|
+
|
|
13
|
+
## 0.1.111: Work Rooms in the menu bar app
|
|
14
|
+
|
|
15
|
+
The menu bar window now has a **Rooms** tab (Gateway Work Item b35d65cb); the menu's **Show Rooms (n unread)…** opens
|
|
16
|
+
it. Pick a Room, then a thread: it shows the newest 20 messages and every unread one, with unread ones marked.
|
|
17
|
+
**Mark read (n)** marks them read. Write a reply and press **Review reply**; it is posted only after you confirm the
|
|
18
|
+
members it goes to ("Post to 3 members: …?"). If the members changed meanwhile, the window shows the new list and asks
|
|
19
|
+
again. A Room notice opens the Rooms tab at that Room while the runner is connected.
|
|
20
|
+
|
|
21
|
+
- For the Rooms tab the runner calls only `work_room_read`, `work_room_receipt_report` and `work_room_write` operation
|
|
22
|
+
`post`, one at a time, with your own runner sign-in. Room creation, members, new threads and outcome shares stay in
|
|
23
|
+
your AI session; archived Rooms are read-only.
|
|
24
|
+
- Room names, titles and messages stay in memory: neither the runner nor the app writes them to disk or a log.
|
|
25
|
+
Notifications stay generic.
|
|
26
|
+
- A reply is at most 8,000 bytes, the Gateway's own limit.
|
|
27
|
+
|
|
28
|
+
0.1.111 also carries the D4 bounded CLI review wiring merged after 0.1.110 (PR #938). Nothing supplies a D4 execution
|
|
29
|
+
host yet, so D4 execution stays closed.
|
|
30
|
+
|
|
1
31
|
## 0.1.110: Claude returns its report through `--json-schema`
|
|
2
32
|
|
|
3
33
|
Claude jobs now pass `--json-schema`, and the model returns its completion report by calling Claude Code's
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { assertD4PackageLimitSupport, assertCurrentD4LimitSupport } from '../../safety/d4-execution-limits.js';
|
|
1
|
+
import { assertD4PackageLimitSupport, assertCurrentD4LimitSupport, D4ObservedLimitWatchdog, D4CliUsageTracker } from '../../safety/d4-execution-limits.js';
|
|
2
2
|
import { CLAUDE_TOKEN_FD, CLAUDE_TOKEN_FD_ENV, pathWithKeychainStub } from "../../safety/claude-runner-token.js";
|
|
3
3
|
import { candidateReviewCompletion } from '../../safety/candidate-review-report.js';
|
|
4
4
|
import path from "node:path";
|
|
@@ -7,6 +7,7 @@ import { CLAUDE_LAUNCH_OPT_OUTS, CLAUDE_RELEASE_MODE, CLAUDE_TRANSPORT, claudeCa
|
|
|
7
7
|
import { createJobSecurityReceipt } from '../../safety/job-security-receipt.js';
|
|
8
8
|
import { isTerminalEventType } from "../../protocol.js";
|
|
9
9
|
import { parseClaudeLine } from "./stream-parser.js";
|
|
10
|
+
import { runtimeFailure } from "../runtime-failure.js";
|
|
10
11
|
import { p1ModelEvent } from '../../verification/p1-execution-host.js';
|
|
11
12
|
import { compilePrompt } from "./prompt-compiler.js";
|
|
12
13
|
import { CLAUDE_COMPLETION_SCHEMA } from "../completion-report.js";
|
|
@@ -104,6 +105,9 @@ export class ClaudeCodeAdapter {
|
|
|
104
105
|
started = false;
|
|
105
106
|
p1 = false;
|
|
106
107
|
candidateReview = false;
|
|
108
|
+
d4Limits;
|
|
109
|
+
d4Guard;
|
|
110
|
+
d4Usage = new D4CliUsageTracker();
|
|
107
111
|
startupAcquisition;
|
|
108
112
|
revokedTools;
|
|
109
113
|
revokeFailed = false;
|
|
@@ -135,7 +139,9 @@ export class ClaudeCodeAdapter {
|
|
|
135
139
|
let acquired;
|
|
136
140
|
this.startupAcquisition = new Promise(resolve => { acquired = resolve; });
|
|
137
141
|
try {
|
|
138
|
-
assertD4PackageLimitSupport(ctx.taskPackage, ctx.approvedTaskPackage, ctx.resumeSessionId);
|
|
142
|
+
this.d4Limits = assertD4PackageLimitSupport(ctx.taskPackage, ctx.approvedTaskPackage, ctx.resumeSessionId);
|
|
143
|
+
if (this.d4Limits && !this.options.d4ExecutionHost)
|
|
144
|
+
throw Error('D4_REVIEW_HOST_REQUIRED');
|
|
139
145
|
const generation = ctx.generation;
|
|
140
146
|
if (!Number.isSafeInteger(generation) || generation === undefined || generation < 1)
|
|
141
147
|
throw new Error("JOB_SECURITY_REJECTED");
|
|
@@ -160,8 +166,26 @@ export class ClaudeCodeAdapter {
|
|
|
160
166
|
assertAdmission();
|
|
161
167
|
const toolInventory = claudeJobToolInventory(this.options.securityPolicy);
|
|
162
168
|
this.candidateReview = toolInventory === 'local-tools-d4-review-v1';
|
|
163
|
-
if (this.candidateReview)
|
|
169
|
+
if (this.candidateReview && !this.d4Limits)
|
|
164
170
|
assertCurrentD4LimitSupport();
|
|
171
|
+
if (this.d4Limits && !this.candidateReview)
|
|
172
|
+
throw Error('D4_REVIEW_INVENTORY_REJECTED');
|
|
173
|
+
const assertD4Host = async () => {
|
|
174
|
+
if (!this.d4Limits)
|
|
175
|
+
return;
|
|
176
|
+
// Recheck both immutable packages around asynchronous preparation; equality alone is not host authority.
|
|
177
|
+
assertD4PackageLimitSupport(ctx.taskPackage, ctx.approvedTaskPackage, ctx.resumeSessionId);
|
|
178
|
+
const descriptor = ctx.approvedTaskPackage.candidate_review_execution;
|
|
179
|
+
const profile = descriptor.profile, host = this.options.d4ExecutionHost;
|
|
180
|
+
if (ctx.generation !== 1 || resolvedModelId !== profile.model_id || host.runnerReleaseSha256 !== profile.runner_release_sha256 || host.hostPolicySha256 !== profile.host_policy_sha256
|
|
181
|
+
|| this.options.securityPolicy?.pin.toolsSha256 !== profile.tools_sha256 || this.options.securityPolicy?.pin.requirementsSha256 !== profile.cli_security_fingerprint
|
|
182
|
+
|| this.options.securityPolicy?.pin.securityMode !== CLAUDE_RELEASE_MODE)
|
|
183
|
+
throw Error('D4_REVIEW_HOST_REQUIRED');
|
|
184
|
+
await host.assertCurrent(structuredClone(descriptor), { jobId: this.options.jobId ?? ctx.bindingId, bindingId: ctx.bindingId, generation: ctx.generation,
|
|
185
|
+
launchFingerprint: ctx.securityBinding?.launchFingerprint ?? '', contractFingerprint: ctx.securityBinding?.contractFingerprint ?? '', taskPackageFingerprint: ctx.securityBinding?.taskPackageFingerprint ?? '' });
|
|
186
|
+
};
|
|
187
|
+
if (this.d4Limits)
|
|
188
|
+
await assertD4Host();
|
|
165
189
|
if (!ctx.securityBinding)
|
|
166
190
|
throw new Error('SECURITY_REQUIREMENTS_REJECTED');
|
|
167
191
|
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 });
|
|
@@ -171,7 +195,8 @@ export class ClaudeCodeAdapter {
|
|
|
171
195
|
if (!cwd || !path.isAbsolute(cwd) || rel === "" || (!rel.startsWith(".." + path.sep) && rel !== ".." && !path.isAbsolute(rel)))
|
|
172
196
|
throw new Error("trusted cwd must live outside the assigned worktree");
|
|
173
197
|
const unbounded = isUnboundedLocalExecution(ctx.taskPackage);
|
|
174
|
-
|
|
198
|
+
const recordOnly = !!this.d4Limits && !('max_total_tokens' in this.d4Limits);
|
|
199
|
+
if (!unbounded && !recordOnly && (!Number.isFinite(this.options.maxBudgetUsd) || this.options.maxBudgetUsd <= 0)) {
|
|
175
200
|
throw new Error("refusing to start: no positive remaining contract budget was supplied");
|
|
176
201
|
}
|
|
177
202
|
if (!this.options.worktreeRoot) {
|
|
@@ -194,9 +219,9 @@ export class ClaudeCodeAdapter {
|
|
|
194
219
|
if (this.cancelled)
|
|
195
220
|
throw new Error("adapter startup cancelled");
|
|
196
221
|
const mcpConfigPath = this.options.writeLocalMcpConfig(endpoint);
|
|
197
|
-
const maxBudgetUsd = unbounded
|
|
222
|
+
const maxBudgetUsd = unbounded || recordOnly
|
|
198
223
|
? null
|
|
199
|
-
: Math.min(this.options.maxBudgetUsd, contractBudget(ctx.taskPackage));
|
|
224
|
+
: Math.min(this.options.maxBudgetUsd, this.d4Limits && 'max_estimated_cost_microusd' in this.d4Limits ? this.d4Limits.max_estimated_cost_microusd / 1_000_000 : contractBudget(ctx.taskPackage));
|
|
200
225
|
const hookPolicy = buildHookPolicy({
|
|
201
226
|
taskPackage: ctx.taskPackage,
|
|
202
227
|
worktreeRoot: this.options.worktreeRoot,
|
|
@@ -227,12 +252,43 @@ export class ClaudeCodeAdapter {
|
|
|
227
252
|
if (this.cancelled)
|
|
228
253
|
throw new Error('adapter startup cancelled');
|
|
229
254
|
assertAdmission();
|
|
255
|
+
if (this.d4Limits)
|
|
256
|
+
await assertD4Host();
|
|
257
|
+
if (this.d4Limits)
|
|
258
|
+
await this.options.assertCurrentInputs?.();
|
|
259
|
+
if (this.cancelled)
|
|
260
|
+
throw new Error('adapter startup cancelled');
|
|
261
|
+
if (this.d4Limits) {
|
|
262
|
+
assertD4PackageLimitSupport(ctx.taskPackage, ctx.approvedTaskPackage, ctx.resumeSessionId);
|
|
263
|
+
assertAdmission();
|
|
264
|
+
}
|
|
230
265
|
const handle = this.options.processHost.spawn(this.options.binaryPath ?? "claude", args, {
|
|
231
266
|
cwd,
|
|
232
267
|
env,
|
|
233
268
|
...(claudeToken === null ? {} : { secretFd3: claudeToken }),
|
|
234
269
|
});
|
|
235
270
|
this.handle = handle;
|
|
271
|
+
if (this.d4Limits) {
|
|
272
|
+
this.d4Guard = new D4ObservedLimitWatchdog(this.d4Limits, { revoke: () => {
|
|
273
|
+
this.localReadObservations();
|
|
274
|
+
this.revokeSecurity();
|
|
275
|
+
this.revokeTools(this.tools);
|
|
276
|
+
if (this.revokeFailed)
|
|
277
|
+
throw Error('D4_STOP_UNCONFIRMED');
|
|
278
|
+
} });
|
|
279
|
+
try {
|
|
280
|
+
this.d4Guard.attach(handle);
|
|
281
|
+
}
|
|
282
|
+
catch (error) {
|
|
283
|
+
// We own this spawn but cannot certify its process group. Stop the child best-effort;
|
|
284
|
+
// cleanup remains unconfirmed and must never turn into a passing review.
|
|
285
|
+
try {
|
|
286
|
+
handle.kill('SIGKILL');
|
|
287
|
+
}
|
|
288
|
+
catch { /* Preserve the failed ownership proof. */ }
|
|
289
|
+
throw error;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
236
292
|
if (this.cancelled)
|
|
237
293
|
throw new Error("adapter startup cancelled");
|
|
238
294
|
handle.stdin.write(claudeUserInput(prompt));
|
|
@@ -256,8 +312,12 @@ export class ClaudeCodeAdapter {
|
|
|
256
312
|
exitInfo.signal = signal;
|
|
257
313
|
});
|
|
258
314
|
let sawTerminal = false;
|
|
315
|
+
// The CLI names an API error's kind on the assistant message before the result (stream-parser.ts); a later
|
|
316
|
+
// assistant message without one replaces it.
|
|
317
|
+
let assistantError;
|
|
259
318
|
try {
|
|
260
|
-
|
|
319
|
+
const lines = this.d4Guard ? this.d4Lines(handle, this.d4Guard) : handle.stdout;
|
|
320
|
+
for await (const line of lines) {
|
|
261
321
|
let message;
|
|
262
322
|
try {
|
|
263
323
|
const value = JSON.parse(line);
|
|
@@ -271,7 +331,32 @@ export class ClaudeCodeAdapter {
|
|
|
271
331
|
// A nested agent message is not evidence that the executor has finished.
|
|
272
332
|
if (message.parent_tool_use_id != null)
|
|
273
333
|
continue;
|
|
274
|
-
|
|
334
|
+
if (this.d4Guard && message.type === 'assistant') {
|
|
335
|
+
const observed = this.d4Usage.assistant(message);
|
|
336
|
+
if (observed) {
|
|
337
|
+
await this.d4Guard.observe(observed);
|
|
338
|
+
if (this.d4Guard.diagnostic().status !== 'active') {
|
|
339
|
+
yield { eventType: 'failed', summary: this.d4Guard.diagnostic().reason_code ?? 'D4_USAGE_UNAVAILABLE' };
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
let d4Usage;
|
|
345
|
+
if (this.d4Guard && message.type === 'result') {
|
|
346
|
+
try {
|
|
347
|
+
d4Usage = this.d4Usage.final(message);
|
|
348
|
+
await this.d4Guard.complete(d4Usage);
|
|
349
|
+
}
|
|
350
|
+
catch (error) {
|
|
351
|
+
if (this.d4Guard.diagnostic().status === 'active')
|
|
352
|
+
await this.d4Guard.complete().catch(() => { });
|
|
353
|
+
yield { eventType: 'failed', summary: this.d4Guard.diagnostic().reason_code ?? 'D4_USAGE_UNAVAILABLE' };
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if (message.type === "assistant")
|
|
358
|
+
assistantError = typeof message.error === "string" ? message.error : undefined;
|
|
359
|
+
let parsed = parseClaudeLine(line, this.options.worktreeRoot, { p1: this.p1, assistantError });
|
|
275
360
|
if (!parsed)
|
|
276
361
|
continue;
|
|
277
362
|
if (parsed.eventType === "completed") {
|
|
@@ -279,6 +364,8 @@ export class ClaudeCodeAdapter {
|
|
|
279
364
|
parsed = structuredCompletionEvent(message, parsed.payload?.cost_usd);
|
|
280
365
|
if (this.candidateReview)
|
|
281
366
|
parsed = candidateReviewCompletion(parsed);
|
|
367
|
+
if (d4Usage)
|
|
368
|
+
parsed = { ...parsed, payload: { ...parsed.payload, d4_usage: { ...d4Usage, policy: this.d4Limits && 'max_total_tokens' in this.d4Limits ? 'observed_stop_possible_overshoot' : 'record_only' } } };
|
|
282
369
|
if (this.p1)
|
|
283
370
|
parsed = p1ModelEvent({ ...parsed, payload: { ...parsed.payload, usage: observedUsage } });
|
|
284
371
|
}
|
|
@@ -291,14 +378,24 @@ export class ClaudeCodeAdapter {
|
|
|
291
378
|
}
|
|
292
379
|
}
|
|
293
380
|
if (!sawTerminal) {
|
|
294
|
-
yield { eventType: "failed", summary: `claude process exited unexpectedly (code ${exitInfo.code ?? "unknown"})
|
|
381
|
+
yield { eventType: "failed", summary: `claude process exited unexpectedly (code ${exitInfo.code ?? "unknown"})`,
|
|
382
|
+
payload: { runtime_failure: runtimeFailure("cli_error") } };
|
|
295
383
|
}
|
|
296
384
|
}
|
|
385
|
+
catch (error) {
|
|
386
|
+
if (!this.d4Guard)
|
|
387
|
+
throw error;
|
|
388
|
+
if (this.d4Guard.diagnostic().status === 'active')
|
|
389
|
+
await this.d4Guard.complete().catch(() => { });
|
|
390
|
+
yield { eventType: 'failed', summary: this.d4Guard.diagnostic().reason_code ?? 'D4_USAGE_UNAVAILABLE' };
|
|
391
|
+
}
|
|
297
392
|
finally {
|
|
298
393
|
await this.cleanup();
|
|
299
394
|
}
|
|
300
395
|
}
|
|
301
396
|
async command(command) {
|
|
397
|
+
if (this.candidateReview)
|
|
398
|
+
throw Error('D4_SUPERVISOR_CONTINUATION_REJECTED');
|
|
302
399
|
if (!ALLOWED_SUPERVISOR_COMMANDS.has(command.command)) {
|
|
303
400
|
throw new Error(`unsupported supervisor command: ${command.command} is not allowed for this adapter`);
|
|
304
401
|
}
|
|
@@ -394,7 +491,9 @@ export class ClaudeCodeAdapter {
|
|
|
394
491
|
failed = true;
|
|
395
492
|
}
|
|
396
493
|
try {
|
|
397
|
-
if (
|
|
494
|
+
if (this.d4Guard)
|
|
495
|
+
await this.d4Guard.cancel();
|
|
496
|
+
else if (handle?.terminateOwned)
|
|
398
497
|
await handle.terminateOwned();
|
|
399
498
|
else
|
|
400
499
|
handle?.kill('SIGKILL');
|
|
@@ -415,6 +514,18 @@ export class ClaudeCodeAdapter {
|
|
|
415
514
|
this.cleanupPromise = operation;
|
|
416
515
|
return operation;
|
|
417
516
|
}
|
|
517
|
+
/** Race a silent CLI against the owned watchdog; stop evidence never depends on another stdout line. */
|
|
518
|
+
async *d4Lines(handle, guard) {
|
|
519
|
+
const iterator = handle.stdout[Symbol.asyncIterator]();
|
|
520
|
+
for (;;) {
|
|
521
|
+
const next = await Promise.race([iterator.next(), guard.stopped().then(() => null)]);
|
|
522
|
+
if (next === null)
|
|
523
|
+
throw Error(guard.diagnostic().reason_code ?? 'D4_STOP_UNCONFIRMED');
|
|
524
|
+
if (next.done)
|
|
525
|
+
return;
|
|
526
|
+
yield next.value;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
418
529
|
/** Claude Code treats a hook it cannot run as no objection, so the hook runs through Node. */
|
|
419
530
|
requireTrustedHookCommand() {
|
|
420
531
|
return formatHookCommand({
|
|
@@ -73,6 +73,7 @@ export function createClaudeCodeAdapterOptions(input) {
|
|
|
73
73
|
throw new Error('Claude account configuration path must be absolute');
|
|
74
74
|
const tokenStore = new ClaudeRunnerTokenStore({ stateRoot: state });
|
|
75
75
|
return {
|
|
76
|
+
d4ExecutionHost: input.d4ExecutionHost,
|
|
76
77
|
processHost: new NodeProcessGroupHost(), worktreeRoot: checkout, jobId: input.jobId, claudeToken: () => tokenStore.load(), keychainStub: () => ensureClaudeKeychainStub(state), get securityPolicy() { return securityPolicy; },
|
|
77
78
|
...(input.securityHost ? { prepareSecurity: async (ctx) => {
|
|
78
79
|
try {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isAbsolute, relative, resolve } from "node:path";
|
|
2
2
|
import { MAX_EVENT_SUMMARY_CHARS } from "../../protocol.js";
|
|
3
3
|
import { p1ModelEvent } from '../../verification/p1-execution-host.js';
|
|
4
|
+
import { runtimeFailure } from "../runtime-failure.js";
|
|
4
5
|
const EDIT_TOOL_NAMES = new Set(["Edit", "Write", "MultiEdit"]);
|
|
5
6
|
function bounded(summary) {
|
|
6
7
|
return summary.length > MAX_EVENT_SUMMARY_CHARS ? summary.slice(0, MAX_EVENT_SUMMARY_CHARS) : summary;
|
|
@@ -69,7 +70,66 @@ function parseSystemMessage(message) {
|
|
|
69
70
|
}
|
|
70
71
|
return null;
|
|
71
72
|
}
|
|
72
|
-
|
|
73
|
+
/*
|
|
74
|
+
* How Claude Code 2.1.308 (the listed release) says why a turn failed. An API error comes as a top-level assistant
|
|
75
|
+
* message with `error`, a coarse kind such as "authentication_failed", then a result with subtype "success",
|
|
76
|
+
* is_error true, `api_error_status` and, for some causes, `api_error`, a finer kind such as "usage_limit_reached".
|
|
77
|
+
* The CLI's own stops end with an error_* subtype. Only these identifiers and the status are read; the result text and
|
|
78
|
+
* `errors` hold provider wording and are never copied. Kinds this table does not name fall through to the status.
|
|
79
|
+
*/
|
|
80
|
+
const AUTH_KINDS = new Set(["authentication_failed", "oauth_org_not_allowed", "account_on_hold", "verification_required",
|
|
81
|
+
"cloud_credential_error"]);
|
|
82
|
+
const AUTH_API_ERRORS = new Set(["invalid_credential_header", "org_disabled_credential", "api_key_auth_disabled",
|
|
83
|
+
"provider_credentials", "gateway_signin_required", "gateway_session_expired"]);
|
|
84
|
+
const USAGE_API_ERRORS = new Set(["usage_limit_reached", "model_requires_usage_credits", "long_context_credits_required"]);
|
|
85
|
+
const KIND_CODES = new Map([["billing_error", "usage_limit"], ["rate_limit", "rate_limited"],
|
|
86
|
+
["model_not_found", "model_unavailable"], ["overloaded", "provider_error"], ["server_error", "provider_error"],
|
|
87
|
+
["max_output_tokens", "cli_error"]]);
|
|
88
|
+
const FAILURE_SUMMARIES = {
|
|
89
|
+
auth: "execution failed: Claude account sign-in refused",
|
|
90
|
+
usage_limit: "execution failed: Claude usage limit reached",
|
|
91
|
+
model_unavailable: "execution failed: model unavailable to the Claude account",
|
|
92
|
+
rate_limited: "execution failed: rate limited",
|
|
93
|
+
provider_error: "execution failed: provider API error",
|
|
94
|
+
budget_exceeded: "execution failed: budget exceeded",
|
|
95
|
+
cli_error: "execution failed: Claude CLI error",
|
|
96
|
+
unknown: "execution failed",
|
|
97
|
+
};
|
|
98
|
+
/** `assistantError` is the `error` of the last top-level assistant message before this result, if it had one. */
|
|
99
|
+
export function classifyClaudeFailure(message, assistantError) {
|
|
100
|
+
const subtype = String(message.subtype ?? "");
|
|
101
|
+
// The CLI ended the turn itself; no API error can be the cause.
|
|
102
|
+
if (subtype === "error_max_budget_usd" || subtype === "error_max_budget_exceeded")
|
|
103
|
+
return "budget_exceeded";
|
|
104
|
+
if (subtype === "error_max_turns")
|
|
105
|
+
return "cli_error";
|
|
106
|
+
const apiError = typeof message.api_error === "string" ? message.api_error : "";
|
|
107
|
+
if (AUTH_API_ERRORS.has(apiError))
|
|
108
|
+
return "auth";
|
|
109
|
+
if (USAGE_API_ERRORS.has(apiError))
|
|
110
|
+
return "usage_limit";
|
|
111
|
+
const kind = assistantError ?? "";
|
|
112
|
+
if (AUTH_KINDS.has(kind))
|
|
113
|
+
return "auth";
|
|
114
|
+
const mapped = KIND_CODES.get(kind);
|
|
115
|
+
if (mapped)
|
|
116
|
+
return mapped;
|
|
117
|
+
const status = message.api_error_status;
|
|
118
|
+
if (status === 401 || status === 403)
|
|
119
|
+
return "auth";
|
|
120
|
+
if (status === 402)
|
|
121
|
+
return "usage_limit";
|
|
122
|
+
if (status === 404)
|
|
123
|
+
return "model_unavailable";
|
|
124
|
+
if (status === 429)
|
|
125
|
+
return "rate_limited";
|
|
126
|
+
if (typeof status === "number" && Number.isInteger(status) && status >= 500 && status <= 599)
|
|
127
|
+
return "provider_error";
|
|
128
|
+
if (kind === "invalid_request" || subtype === "error_during_execution" || message.terminal_reason === "turn_setup_failed")
|
|
129
|
+
return "cli_error";
|
|
130
|
+
return "unknown";
|
|
131
|
+
}
|
|
132
|
+
function parseResultMessage(message, assistantError) {
|
|
73
133
|
const isError = message.is_error === true;
|
|
74
134
|
const costUsd = typeof message.total_cost_usd === "number" ? message.total_cost_usd : undefined;
|
|
75
135
|
if (!isError) {
|
|
@@ -90,8 +150,9 @@ function parseResultMessage(message) {
|
|
|
90
150
|
return { eventType: "failed", summary: "execution failed: structured output missing",
|
|
91
151
|
payload: { ...(costUsd !== undefined ? { cost_usd: costUsd } : {}), completion_rejection: { version: "completion-rejection-v1", code } } };
|
|
92
152
|
}
|
|
93
|
-
const
|
|
94
|
-
return { eventType: "failed", summary
|
|
153
|
+
const code = classifyClaudeFailure(message, assistantError);
|
|
154
|
+
return { eventType: "failed", summary: FAILURE_SUMMARIES[code],
|
|
155
|
+
payload: { ...(costUsd !== undefined ? { cost_usd: costUsd } : {}), runtime_failure: runtimeFailure(code, message.api_error_status) } };
|
|
95
156
|
}
|
|
96
157
|
export function parseClaudeLine(line, worktreeRoot, options = {}) {
|
|
97
158
|
let message;
|
|
@@ -110,7 +171,7 @@ export function parseClaudeLine(line, worktreeRoot, options = {}) {
|
|
|
110
171
|
case "user":
|
|
111
172
|
return parseUserMessage(message.message ?? {});
|
|
112
173
|
case "result":
|
|
113
|
-
return parseResultMessage(message);
|
|
174
|
+
return parseResultMessage(message, options.assistantError);
|
|
114
175
|
default:
|
|
115
176
|
return null;
|
|
116
177
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Why a provider CLI stopped, as one bounded code (no provider text). The Claude parser sets it on a failed terminal
|
|
3
|
+
* event; the daemon copies it into the job completion so the Gateway can tell an account problem from a code problem.
|
|
4
|
+
*
|
|
5
|
+
* - auth: the CLI's account was signed out, expired or refused.
|
|
6
|
+
* - usage_limit: the account's usage limit or credit ran out.
|
|
7
|
+
* - model_unavailable: the account cannot use the requested model.
|
|
8
|
+
* - rate_limited: the provider throttled the account for a short time.
|
|
9
|
+
* - provider_error: the provider's API failed or was overloaded.
|
|
10
|
+
* - budget_exceeded: the job's own cost budget ran out.
|
|
11
|
+
* - cli_error: the CLI stopped on its own error, not the account's or the provider's.
|
|
12
|
+
* - unknown: an error the CLI reported without a recognisable kind.
|
|
13
|
+
*/
|
|
14
|
+
export const RUNTIME_FAILURE_CODES = ["auth", "usage_limit", "model_unavailable", "rate_limited", "provider_error",
|
|
15
|
+
"budget_exceeded", "cli_error", "unknown"];
|
|
16
|
+
export function runtimeFailure(code, httpStatus) {
|
|
17
|
+
return { version: "runtime-failure-v1", code, ...(isHttpErrorStatus(httpStatus) ? { http_status: httpStatus } : {}) };
|
|
18
|
+
}
|
|
19
|
+
/** The exact v1 shape or undefined; copies nothing else. */
|
|
20
|
+
export function runtimeFailureDiagnostic(value) {
|
|
21
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
22
|
+
return undefined;
|
|
23
|
+
const d = value;
|
|
24
|
+
if (d.version !== "runtime-failure-v1" || !RUNTIME_FAILURE_CODES.includes(d.code))
|
|
25
|
+
return undefined;
|
|
26
|
+
if (Object.keys(d).some(key => !["version", "code", "http_status"].includes(key)))
|
|
27
|
+
return undefined;
|
|
28
|
+
if (d.http_status !== undefined && !isHttpErrorStatus(d.http_status))
|
|
29
|
+
return undefined;
|
|
30
|
+
return runtimeFailure(d.code, d.http_status);
|
|
31
|
+
}
|
|
32
|
+
function isHttpErrorStatus(value) {
|
|
33
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 400 && value <= 599;
|
|
34
|
+
}
|
|
@@ -1,6 +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';
|
|
5
|
+
import { isD4ExecutionSchema } from '../safety/d4-execution-limits.js';
|
|
4
6
|
import { publicationError } from '../publication/failure.js';
|
|
5
7
|
import { PublicationOutcomeUnknownError } from '../publication/publisher.js';
|
|
6
8
|
import { assertP1Bindings, hasP1, p1ModelEvent } from '../verification/p1-execution-host.js';
|
|
@@ -178,6 +180,9 @@ export class ExecutionCoordinator {
|
|
|
178
180
|
assertBoundExecutionAttempt(job, binding.taskPackage, binding.iterationId);
|
|
179
181
|
if (executionAttempt && binding.bindingReplayed !== false)
|
|
180
182
|
throw Error('CORRECTION_BINDING_RECONCILIATION_REQUIRED');
|
|
183
|
+
if (isD4ExecutionSchema(pkg.candidate_review_execution?.schema_version)
|
|
184
|
+
&& (job.generation !== 1 || binding.bindingReplayed !== false))
|
|
185
|
+
throw Error('D4_BINDING_RECONCILIATION_REQUIRED');
|
|
181
186
|
binding = immutable(structuredClone(binding));
|
|
182
187
|
assertCandidateReviewPackages(pkg, binding.taskPackage);
|
|
183
188
|
if (isP1)
|
|
@@ -445,8 +450,10 @@ export class ExecutionCoordinator {
|
|
|
445
450
|
if (isP1 && !hostProof)
|
|
446
451
|
event = p1ModelEvent(event);
|
|
447
452
|
const previousStage = executionStage;
|
|
453
|
+
const d4StopAck = isD4ExecutionSchema(job.taskPackage.candidate_review_execution?.schema_version)
|
|
454
|
+
&& ['failed', 'cancelled', 'blocked'].includes(event.eventType);
|
|
448
455
|
let payload = event.payload;
|
|
449
|
-
if (isTerminalEventType(event.eventType)) {
|
|
456
|
+
if (isTerminalEventType(event.eventType) && !d4StopAck) {
|
|
450
457
|
await fenced(observeCleanup());
|
|
451
458
|
executionStage = 'terminal_evidence';
|
|
452
459
|
requireControlFence();
|
|
@@ -456,6 +463,8 @@ export class ExecutionCoordinator {
|
|
|
456
463
|
if (isP1 && event.eventType === 'completed' && p1Result)
|
|
457
464
|
payload = { ...payload, candidate_commit_sha: p1Result.candidate_commit_sha, snapshot_fingerprint: p1Result.snapshot_fingerprint, changed_paths: p1Result.changed_paths, verification_receipt_ids: [...p1ReceiptIds] };
|
|
458
465
|
}
|
|
466
|
+
if (d4StopAck)
|
|
467
|
+
await fenced(observeCleanup());
|
|
459
468
|
// A vendor report cannot supply host observations, even when its shape looks valid.
|
|
460
469
|
if (payload) {
|
|
461
470
|
payload = { ...payload };
|
|
@@ -463,10 +472,10 @@ export class ExecutionCoordinator {
|
|
|
463
472
|
delete payload.local_fix_publication;
|
|
464
473
|
}
|
|
465
474
|
// Memory use is bound to this binding's bundle receipt by the runner, never taken from the model (WI 35a84e52).
|
|
466
|
-
payload = withMemoryUsage(payload, binding.taskPackage, isTerminalEventType(event.eventType));
|
|
475
|
+
payload = d4StopAck ? undefined : withMemoryUsage(payload, binding.taskPackage, isTerminalEventType(event.eventType));
|
|
467
476
|
if (event.eventType === "completed" && localFixPublication)
|
|
468
477
|
payload = { ...payload, local_fix_publication: structuredClone(localFixPublication) };
|
|
469
|
-
if (isTerminalEventType(event.eventType) || event.eventType === 'blocked' || event.eventType === 'scope_conflict') {
|
|
478
|
+
if (!d4StopAck && (isTerminalEventType(event.eventType) || event.eventType === 'blocked' || event.eventType === 'scope_conflict')) {
|
|
470
479
|
const reads = safeLocalReadObservations(adapter.localReadObservations?.());
|
|
471
480
|
if (reads)
|
|
472
481
|
payload = { ...payload, local_read_observations: reads };
|
|
@@ -825,6 +834,8 @@ export class ExecutionCoordinator {
|
|
|
825
834
|
const reportDiagnostic = diagnostic && (status === "blocked"
|
|
826
835
|
? diagnostic.code === "CODEX_COMPLETION_REPORT_BLOCKED" : diagnostic.code !== "CODEX_COMPLETION_REPORT_BLOCKED") ? diagnostic : undefined;
|
|
827
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;
|
|
828
839
|
const rejectedCounts = reportDiagnostic?.rejection_counts;
|
|
829
840
|
if (rejectedCompletion || rejectedCounts) {
|
|
830
841
|
try {
|
|
@@ -834,7 +845,7 @@ export class ExecutionCoordinator {
|
|
|
834
845
|
}
|
|
835
846
|
const securityReceipt = adapter.securityReceipt?.();
|
|
836
847
|
const reads = safeLocalReadObservations(adapter.localReadObservations?.());
|
|
837
|
-
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 } : {}) } });
|
|
838
849
|
await this.deps.state.save({ jobId: job.id, generation: job.generation, leaseToken: job.leaseToken,
|
|
839
850
|
phase: "terminal_pending", bindingId: binding.bindingId, terminalEventType: terminal.eventType,
|
|
840
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,6 +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";
|
|
20
|
+
import { D4_OBSERVED_EXECUTION_CAPABILITY, D4_RECORDED_EXECUTION_CAPABILITY } from '../safety/d4-execution-limits.js';
|
|
18
21
|
import { createClaudeCodeAdapterOptions } from "../adapters/claude-code/runtime-options.js";
|
|
19
22
|
import { ClaudeRunnerTokenStore, ensureClaudeKeychainStub } from "../safety/claude-runner-token.js";
|
|
20
23
|
import { WorkspaceManager } from "../workspace/workspace-manager.js";
|
|
@@ -75,6 +78,27 @@ export async function buildDaemon(input) {
|
|
|
75
78
|
pendingCodexClaim.job.lease_expires_at = expiresAt;
|
|
76
79
|
};
|
|
77
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;
|
|
84
|
+
if (!d4Host) {
|
|
85
|
+
const composition = await composeReviewedD4Host({ binaryIdentity: () => claude.binaryIdentity(), revalidateJob: async (descriptor, context) => {
|
|
86
|
+
const job = activeD4Job;
|
|
87
|
+
if (!job || context.jobId !== job.id || context.bindingId !== activeD4Binding || context.generation !== 1 || job.generation !== 1
|
|
88
|
+
|| context.launchFingerprint !== job.launchFingerprint || context.contractFingerprint !== job.gatewayContractFingerprint
|
|
89
|
+
|| context.taskPackageFingerprint !== job.gatewayTaskPackageFingerprint
|
|
90
|
+
|| protocolHash(job.taskPackage) !== job.gatewayTaskPackageFingerprint
|
|
91
|
+
|| protocolHash(job.taskPackage.candidate_review_execution) !== protocolHash(descriptor))
|
|
92
|
+
throw Error('D4_JOB_CONTEXT_REJECTED');
|
|
93
|
+
const { job: current } = await gateway.heartbeatLocalJob({ jobId: job.id, generation: job.generation, leaseToken: job.leaseToken });
|
|
94
|
+
if (activeD4Job !== job || activeD4Binding !== context.bindingId || current.id !== job.id || current.generation !== 1
|
|
95
|
+
|| !['claimed', 'shadowing', 'running'].includes(current.status) || !Number.isSafeInteger(current.lease_expires_at)
|
|
96
|
+
|| current.lease_expires_at <= Math.floor(Date.now() / 1000))
|
|
97
|
+
throw Error('D4_JOB_CONTEXT_REJECTED');
|
|
98
|
+
} });
|
|
99
|
+
d4Host = composition.host;
|
|
100
|
+
process.stderr.write(`${JSON.stringify({ event: 'd4_host', status: composition.status })}\n`);
|
|
101
|
+
}
|
|
78
102
|
// The menu bar app (Work Item b9218937): counts and clickable notices; osascript stays the fallback.
|
|
79
103
|
let roomActivityEnabled = false;
|
|
80
104
|
const menubar = buildMenubar({ stateRoot: input.stateRoot, home: input.env.HOME ?? "", gatewayMcpUrl: input.gatewayMcpUrl,
|
|
@@ -123,6 +147,7 @@ export async function buildDaemon(input) {
|
|
|
123
147
|
securityHost: claude.securityHost, binaryIdentity: await claude.binaryIdentity(),
|
|
124
148
|
pullRequestEvidence: (args, signal) => gateway.readPullRequestEvidence(args, signal),
|
|
125
149
|
readCandidateReviewContext: loopId => gateway.readCandidateReviewContext(loopId),
|
|
150
|
+
d4ExecutionHost: d4Host,
|
|
126
151
|
}),
|
|
127
152
|
});
|
|
128
153
|
const codex = new CodexPlugin({
|
|
@@ -154,10 +179,15 @@ export async function buildDaemon(input) {
|
|
|
154
179
|
shadow: (job, signal) => gateway.localJobShadow({ workLoopId: job.workLoopId,
|
|
155
180
|
contractFingerprint: job.gatewayContractFingerprint ?? "",
|
|
156
181
|
taskPackageFingerprint: job.gatewayTaskPackageFingerprint ?? "" }, signal),
|
|
157
|
-
createBinding: async (job, signal) =>
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
182
|
+
createBinding: async (job, signal) => {
|
|
183
|
+
const binding = await gateway.createLocalJobBinding({
|
|
184
|
+
jobId: job.id, generation: job.generation, jobLeaseToken: job.leaseToken,
|
|
185
|
+
launchGrant: job.launchGrant, workLoopId: job.workLoopId, checkoutPath: job.checkoutPath, claimedJob: job,
|
|
186
|
+
}, signal);
|
|
187
|
+
if (activeD4Job?.id === job.id)
|
|
188
|
+
activeD4Binding = binding.bindingId;
|
|
189
|
+
return binding;
|
|
190
|
+
},
|
|
161
191
|
publicationRecoveryContext: bindingId => gateway.exportLocalPublicationRecoveryContext(bindingId),
|
|
162
192
|
syncPublicationCursor: (bindingId, cursor, job, receipt) => gateway.syncLocalPublicationCursor(bindingId, cursor, job, receipt),
|
|
163
193
|
confirmServerPublicationCompletion: (bindingId, cursor, job, receipt) => gateway.confirmServerPublicationCompletion(bindingId, cursor, job, receipt),
|
|
@@ -219,7 +249,22 @@ export async function buildDaemon(input) {
|
|
|
219
249
|
return { status: "claimed", job };
|
|
220
250
|
},
|
|
221
251
|
execute: async (job) => {
|
|
222
|
-
|
|
252
|
+
const claimed = job;
|
|
253
|
+
if (claimed.taskPackage.candidate_review_execution) {
|
|
254
|
+
if (activeD4Job)
|
|
255
|
+
throw Error('D4_JOB_CONTEXT_REJECTED');
|
|
256
|
+
activeD4Job = structuredClone(claimed);
|
|
257
|
+
activeD4Binding = undefined;
|
|
258
|
+
try {
|
|
259
|
+
await coordinator.execute(claimed);
|
|
260
|
+
}
|
|
261
|
+
finally {
|
|
262
|
+
activeD4Job = undefined;
|
|
263
|
+
activeD4Binding = undefined;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
else
|
|
267
|
+
await coordinator.execute(claimed);
|
|
223
268
|
},
|
|
224
269
|
requestSync: reason => { transport?.requestSync(reason); },
|
|
225
270
|
// Until now the heartbeat said this runner takes no jobs; tell the Gateway at once, and ask whether one waits.
|
|
@@ -296,10 +341,16 @@ export async function buildDaemon(input) {
|
|
|
296
341
|
}
|
|
297
342
|
const askInbox = await asks.advertisement();
|
|
298
343
|
const guidance = await guide.advertisement();
|
|
344
|
+
let d4Ready;
|
|
345
|
+
try {
|
|
346
|
+
if (d4Host && (!d4Host.ready || await d4Host.ready()))
|
|
347
|
+
d4Ready = d4Host;
|
|
348
|
+
}
|
|
349
|
+
catch { /* D4 readiness never disables other workflows. */ }
|
|
299
350
|
return {
|
|
300
351
|
deviceFingerprint: runnerDeviceFingerprint(input.gatewayMcpUrl),
|
|
301
352
|
displayName: runnerDisplayName(), packageVersion: packageVersion(),
|
|
302
|
-
capabilities: acceptingJobs(localRunnerCapabilities(probes, !!localFixHost, localFixHost?.verificationModes ?? localFixHost?.verificationMode, ['claude_code_cli', ...(production ? ['codex_cli'] : [])]), poller.accepting),
|
|
353
|
+
capabilities: acceptingJobs(localRunnerCapabilities(probes, !!localFixHost, localFixHost?.verificationModes ?? localFixHost?.verificationMode, ['claude_code_cli', ...(production ? ['codex_cli'] : [])], d4Ready), poller.accepting),
|
|
303
354
|
mode: "full",
|
|
304
355
|
...(sessionOpen ? { sessionOpen: { ...sessionOpen, ...(askInbox ? { ask_inbox: askInbox } : {}) } } : {}),
|
|
305
356
|
...(guidance ? { guidance } : {}),
|
|
@@ -462,12 +513,14 @@ export async function reconcilePreparationState(previous, deps) {
|
|
|
462
513
|
return true;
|
|
463
514
|
}
|
|
464
515
|
/** Vendor capability text cannot claim a host-owned publication facility. */
|
|
465
|
-
export function localRunnerCapabilities(probes, trustedLocalFixHost = false, verificationMode, pullRequestEvidenceAdapters = []) {
|
|
516
|
+
export function localRunnerCapabilities(probes, trustedLocalFixHost = false, verificationMode, pullRequestEvidenceAdapters = [], d4Host) {
|
|
517
|
+
if (d4Host && (!/^[a-f0-9]{64}$/.test(d4Host.runnerReleaseSha256) || !/^[a-f0-9]{64}$/.test(d4Host.hostPolicySha256) || typeof d4Host.assertCurrent !== 'function'))
|
|
518
|
+
throw Error('D4_REVIEW_HOST_REQUIRED');
|
|
466
519
|
const modes = verificationMode === undefined ? [] : typeof verificationMode === 'string' ? [verificationMode] : verificationMode;
|
|
467
520
|
return probes.map(probe => {
|
|
468
521
|
const status = adapterStatus(probe);
|
|
469
522
|
const authenticated = status.authenticated;
|
|
470
|
-
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');
|
|
523
|
+
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);
|
|
471
524
|
if (trustedLocalFixHost && authenticated && isLocalFixAdapter(probe.id)) {
|
|
472
525
|
capabilities.push('local_fix_publication_v1');
|
|
473
526
|
if (modes.includes('gateway_ci_v1'))
|
|
@@ -478,6 +531,9 @@ export function localRunnerCapabilities(probes, trustedLocalFixHost = false, ver
|
|
|
478
531
|
// The host owns the Gateway call behind local_pull_request_read, so only the host can claim it (Work Decision 842179e6).
|
|
479
532
|
if (authenticated && pullRequestEvidenceAdapters.includes(probe.id))
|
|
480
533
|
capabilities.push('pull_request_evidence_v1');
|
|
481
|
-
|
|
534
|
+
const d4 = authenticated && probe.id === 'claude_code_cli' && d4Host;
|
|
535
|
+
if (d4)
|
|
536
|
+
capabilities.push(...(d4.capabilities ?? [D4_OBSERVED_EXECUTION_CAPABILITY, D4_RECORDED_EXECUTION_CAPABILITY]));
|
|
537
|
+
return { adapter: probe.id, version: probe.version, ...status, capabilities, ...(d4 ? { runner_release_sha256: d4.runnerReleaseSha256, host_policy_sha256: d4.hostPolicySha256 } : {}) };
|
|
482
538
|
});
|
|
483
539
|
}
|