ai-foreman 1.7.20 → 1.7.22
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 +12 -2
- package/dist/activity.d.ts +3 -0
- package/dist/activity.js +27 -8
- package/dist/adapters/claude.d.ts +19 -0
- package/dist/adapters/claude.js +59 -7
- package/dist/adapters/codex.d.ts +20 -0
- package/dist/adapters/codex.js +99 -5
- package/dist/adapters/recovering.d.ts +9 -0
- package/dist/adapters/recovering.js +10 -0
- package/dist/adapters/types.d.ts +15 -0
- package/dist/agentRun.d.ts +7 -0
- package/dist/agentRun.js +24 -13
- package/dist/branch/currentGuard.d.ts +1 -0
- package/dist/branch/currentGuard.js +1 -0
- package/dist/branch/dirtyBaseline.d.ts +5 -0
- package/dist/branch/dirtyBaseline.js +69 -0
- package/dist/branch/finalization.js +6 -1
- package/dist/branch/git.d.ts +1 -0
- package/dist/branch/git.js +15 -1
- package/dist/branch/resume.js +2 -2
- package/dist/branch/runner.d.ts +2 -0
- package/dist/branch/runner.js +90 -4
- package/dist/buildAdmission.js +2 -0
- package/dist/buildAssignment.d.ts +33 -0
- package/dist/buildAssignment.js +215 -0
- package/dist/buildInterventions.d.ts +90 -0
- package/dist/buildInterventions.js +334 -0
- package/dist/buildOwnershipReconciliation.d.ts +19 -0
- package/dist/buildOwnershipReconciliation.js +41 -0
- package/dist/buildOwnershipRepair.d.ts +33 -0
- package/dist/buildOwnershipRepair.js +111 -0
- package/dist/buildRuns.d.ts +6 -0
- package/dist/buildRuns.js +23 -17
- package/dist/buildSyntheticWork.d.ts +2 -0
- package/dist/buildSyntheticWork.js +30 -0
- package/dist/buildWorkAdmission.d.ts +17 -0
- package/dist/buildWorkAdmission.js +57 -0
- package/dist/buildWorkAuthorization.d.ts +2 -0
- package/dist/buildWorkAuthorization.js +23 -0
- package/dist/buildWorkContext.d.ts +3 -0
- package/dist/buildWorkContext.js +6 -0
- package/dist/buildWorkMigration.d.ts +9 -0
- package/dist/buildWorkMigration.js +215 -0
- package/dist/buildWorkReconciliation.d.ts +12 -0
- package/dist/buildWorkReconciliation.js +70 -0
- package/dist/builderGuidanceFollowup.d.ts +15 -0
- package/dist/builderGuidanceFollowup.js +34 -0
- package/dist/cli/doctor.js +7 -2
- package/dist/cli/graph.d.ts +2 -0
- package/dist/cli/graph.js +75 -0
- package/dist/cli/manager.d.ts +8 -0
- package/dist/cli/manager.js +235 -13
- package/dist/cli/recovery.js +3 -2
- package/dist/cli/runtimeAuthPrompt.js +28 -8
- package/dist/cli/start.js +192 -50
- package/dist/cli/status.js +29 -7
- package/dist/cli/tickets.d.ts +4 -0
- package/dist/cli/tickets.js +37 -13
- package/dist/continuity.d.ts +11 -0
- package/dist/continuity.js +22 -6
- package/dist/foreman.d.ts +10 -2
- package/dist/foreman.js +330 -39
- package/dist/graph/boundary.d.ts +15 -0
- package/dist/graph/boundary.js +50 -0
- package/dist/graph/bridge.d.ts +9 -0
- package/dist/graph/bridge.js +146 -0
- package/dist/graph/bridge.py +205 -0
- package/dist/graph/config.d.ts +11 -0
- package/dist/graph/config.js +40 -0
- package/dist/graph/corpus.d.ts +16 -0
- package/dist/graph/corpus.js +188 -0
- package/dist/graph/derived.d.ts +17 -0
- package/dist/graph/derived.js +73 -0
- package/dist/graph/host.d.ts +4 -0
- package/dist/graph/host.js +160 -0
- package/dist/graph/install.d.ts +3 -0
- package/dist/graph/install.js +50 -0
- package/dist/graph/lifecycle.d.ts +11 -0
- package/dist/graph/lifecycle.js +124 -0
- package/dist/graph/maintenance.d.ts +43 -0
- package/dist/graph/maintenance.js +301 -0
- package/dist/graph/manager.d.ts +9 -0
- package/dist/graph/manager.js +92 -0
- package/dist/graph/preparation.d.ts +41 -0
- package/dist/graph/preparation.js +43 -0
- package/dist/graph/read.d.ts +27 -0
- package/dist/graph/read.js +152 -0
- package/dist/graph/semantic.d.ts +17 -0
- package/dist/graph/semantic.js +27 -0
- package/dist/graph/session.d.ts +8 -0
- package/dist/graph/session.js +92 -0
- package/dist/graph/sources.d.ts +4 -0
- package/dist/graph/sources.js +11 -0
- package/dist/graph/storage.d.ts +38 -0
- package/dist/graph/storage.js +159 -0
- package/dist/graph/turn.d.ts +17 -0
- package/dist/graph/turn.js +360 -0
- package/dist/graph/util.d.ts +5 -0
- package/dist/graph/util.js +51 -0
- package/dist/handoffs.d.ts +1 -0
- package/dist/handoffs.js +5 -1
- package/dist/humanDecision.d.ts +21 -2
- package/dist/humanDecision.js +169 -11
- package/dist/index.js +2 -0
- package/dist/installOwnership.d.ts +46 -0
- package/dist/installOwnership.js +440 -0
- package/dist/interviews.d.ts +3 -1
- package/dist/interviews.js +7 -0
- package/dist/linuxProbeInventory.d.ts +12 -0
- package/dist/linuxProbeInventory.js +48 -0
- package/dist/log.d.ts +1 -1
- package/dist/log.js +7 -1
- package/dist/managerEvidence.d.ts +25 -0
- package/dist/managerEvidence.js +326 -0
- package/dist/managerEvidenceArtifacts.d.ts +38 -0
- package/dist/managerEvidenceArtifacts.js +118 -0
- package/dist/managerPacket.d.ts +2 -0
- package/dist/managerPacket.js +25 -1
- package/dist/markers.d.ts +1 -1
- package/dist/markers.js +2 -2
- package/dist/observability.js +1 -1
- package/dist/processIdentity.js +7 -0
- package/dist/projectDiagnostics.js +32 -8
- package/dist/providerPhase.d.ts +13 -0
- package/dist/providerPhase.js +26 -0
- package/dist/qaAuthorizedDisposition.d.ts +24 -0
- package/dist/qaAuthorizedDisposition.js +26 -0
- package/dist/qaBuildGate.d.ts +4 -0
- package/dist/qaBuildGate.js +216 -0
- package/dist/qaBuilderCoverage.d.ts +5 -0
- package/dist/qaBuilderCoverage.js +78 -0
- package/dist/qaContractCoverage.d.ts +10 -0
- package/dist/qaContractCoverage.js +55 -0
- package/dist/qaContractDelivery.d.ts +23 -0
- package/dist/qaContractDelivery.js +145 -0
- package/dist/qaContractFreshness.d.ts +15 -0
- package/dist/qaContractFreshness.js +36 -0
- package/dist/qaDeliveryJournal.d.ts +10 -0
- package/dist/qaDeliveryJournal.js +16 -1
- package/dist/qaEffectiveConfig.d.ts +20 -0
- package/dist/qaEffectiveConfig.js +40 -0
- package/dist/qaEvidenceReader.d.ts +14 -0
- package/dist/qaEvidenceReader.js +176 -0
- package/dist/qaFailureDelivery.d.ts +2 -0
- package/dist/qaFailureDelivery.js +107 -10
- package/dist/qaHandbackMigration.d.ts +1 -1
- package/dist/qaHandbackMigration.js +3 -3
- package/dist/qaPreparation.d.ts +26 -0
- package/dist/qaPreparation.js +150 -0
- package/dist/qaPreparationInputs.d.ts +8 -0
- package/dist/qaPreparationInputs.js +47 -0
- package/dist/qaPreparationMetrics.d.ts +145 -0
- package/dist/qaPreparationMetrics.js +120 -0
- package/dist/qaPreparationPolicy.d.ts +5 -0
- package/dist/qaPreparationPolicy.js +51 -0
- package/dist/qaPreparationStore.d.ts +157 -0
- package/dist/qaPreparationStore.js +412 -0
- package/dist/qaProtocolV2.d.ts +28 -0
- package/dist/qaProtocolV2.js +12 -0
- package/dist/qaReview.d.ts +6 -0
- package/dist/qaReview.js +234 -24
- package/dist/qaTimeline.d.ts +18 -0
- package/dist/qaTimeline.js +123 -0
- package/dist/qaVerificationContract.d.ts +17 -0
- package/dist/qaVerificationContract.js +236 -0
- package/dist/readinessCleanup.js +12 -5
- package/dist/recoveryGuidance.d.ts +9 -0
- package/dist/recoveryGuidance.js +13 -0
- package/dist/recoveryPolicy.js +1 -1
- package/dist/runtimeAuth.d.ts +1 -0
- package/dist/runtimeAuth.js +5 -1
- package/dist/runtimeReadiness.d.ts +1 -0
- package/dist/runtimeReadiness.js +13 -1
- package/dist/sessionIdentity.d.ts +2 -0
- package/dist/sessionIdentity.js +14 -0
- package/dist/sessionLifecycle.js +3 -3
- package/dist/sources/sourceRegistry.js +7 -0
- package/dist/stateTransfer.js +50 -1
- package/dist/statusReporter.d.ts +1 -0
- package/dist/statusReporter.js +1 -0
- package/dist/supervisedStart.js +19 -8
- package/dist/terminalStyle.d.ts +21 -0
- package/dist/terminalStyle.js +50 -0
- package/dist/ticketPopulation.d.ts +1 -0
- package/dist/ticketPopulation.js +5 -1
- package/dist/tickets/commands.d.ts +1 -1
- package/dist/tickets/commands.js +13 -11
- package/dist/tickets/details.js +2 -2
- package/dist/tickets/queue.js +1 -1
- package/dist/tickets/stateDb.d.ts +4 -1
- package/dist/tickets/stateDb.js +18 -2
- package/dist/tickets/ticketSchema.d.ts +198 -0
- package/dist/tickets/ticketSchema.js +2 -0
- package/dist/tickets/validate.js +4 -1
- package/dist/workflowDb.d.ts +51 -5
- package/dist/workflowDb.js +338 -7
- package/dist/workflowReader.d.ts +109 -2
- package/dist/workflowReader.js +48 -4
- package/package.json +85 -8
- package/scripts/copy-graph-assets.mjs +3 -0
- package/scripts/graph-benchmark.mjs +20 -0
- package/scripts/graph-evaluation.mjs +85 -0
package/README.md
CHANGED
|
@@ -131,7 +131,7 @@ Provider sessions are historical after import. Resume with `rafi build:resume <p
|
|
|
131
131
|
| `--branch-prefix <prefix>` | `feature` / `rafi` / another valid Git prefix | `feature` | Prefix for generated ticket branches. Explicit prefixes are preserved verbatim. |
|
|
132
132
|
| `--max-branch-depth <n>` | positive integer | `5` | Maximum selected branch/PR stack depth (root PR is depth 1). |
|
|
133
133
|
| `--show-session-cost` / `--hide-session-cost` | flags | saved role preferences | Show authoritative provider cost or trustworthy cumulative tokens for Builder and QA; does not change context occupancy display. |
|
|
134
|
-
| `--auto-compact-threshold <percent>` | integer `1`–`99` | saved Builder value, then `
|
|
134
|
+
| `--auto-compact-threshold <percent>` | integer `1`–`99` | saved Builder value, then `65` | Initial run threshold override. A newer persistent settings revision wins at the next safe boundary. |
|
|
135
135
|
|
|
136
136
|
Auto-detected tracker files:
|
|
137
137
|
|
|
@@ -182,7 +182,7 @@ Owner-only recovery packets under `.foreman/qa-report-recovery/` bind the review
|
|
|
182
182
|
|
|
183
183
|
### `ai-foreman manager`
|
|
184
184
|
|
|
185
|
-
Manager
|
|
185
|
+
Manager provides project-wide diagnostic conversations and explicitly authorized, scoped controls over retained build runs. It starts with a verified live run when one exists, otherwise the most recently updated run; `--run <id>` changes only that initial focus. Each question refreshes the catalog, active state, deterministic aggregates, and relevant run details.
|
|
186
186
|
|
|
187
187
|
Permanent sanitized run summaries keep timing, counts, provider/model configuration, usage coverage, failure category, and terminal Git facts useful after detailed spans expire. Detailed spans retain the configured 30-day policy. Missing historical facts are reported as partial rather than reconstructed or treated as zero.
|
|
188
188
|
|
|
@@ -193,6 +193,16 @@ ai-foreman manager ./my-project
|
|
|
193
193
|
ai-foreman manager ./my-project --run <run-id> --ask "Compare this run with the last five successful runs"
|
|
194
194
|
```
|
|
195
195
|
|
|
196
|
+
Full QA history is available without a provider through `--ask '/qa-work <run>'`, `/qa-attempts <run> <work>`, `/qa-report <run> <work> <attempt> [occurrence]`, and `/qa-timeline <run> <work>`. One-shot output expands all pinned pages and artifacts before exiting, independently of bounded model packets. `/qa-export <run> <work> <attempt> [occurrence]` exports exact protected report bytes with their identity and digest. Interactive `/more <cursor>` and `/artifact <handle>` continuations belong to that session; a later invocation retrieves fresh evidence. Reads do not migrate workflow state.
|
|
197
|
+
|
|
198
|
+
Explicit `/guide-builder`, `/guide-qa`, and `/guide-both` commands take a run, work ID, and exact advice. The owning process delivers one-use guidance at its next safe boundary. `/pause <run> <work> [run]` waits for the active turn to finish; withdraw the pause to allow continuation. `/request-attempt <run> <work> <reason>` grants one existing-ledger attempt authorization for the current failed review or waiting Builder guidance after an unfinalized pass. Guidance alone never grants attempts. Inspect truthful delivery and verification receipts with `/qa-instruction <run> <work> <instruction>`.
|
|
199
|
+
|
|
200
|
+
If QA passes while Builder guidance is waiting, explicit resume applies that guidance within the existing approved ticket scope and remaining attempt allowance, then performs a complete fresh QA review. This works on the current branch and in isolated ticket worktrees. The follow-up consumes the same durable allowance as QA remediation; neither a fresh review nor a restart resets it. When the allowance is exhausted, authorize one scoped follow-up with `/request-attempt`, then resume. Changed requirements need renewed approval, and uncertain submissions require reconciliation before another Builder turn. Saving guidance alone does not start work.
|
|
201
|
+
|
|
202
|
+
Ownership schema 4 requires admitted work before Builder/QA dispatch. Legacy QA records are retained in quarantine when authorization cannot be proved. Both resume aliases assess conflicts before selecting recovery mode. Use `/qa-conflicts <run>` and `/qa-repair-plan <run> <work>` to inspect the source mapping, then submit an explicit `/qa-repair <JSON>` decision with the displayed revision, source digest, approved scope, complete path mapping, and operator attestation. Stop the existing owner first. Repairs preserve source and historical packets and require fresh verification; uncertain provider dispatch needs separate reconciliation.
|
|
203
|
+
|
|
204
|
+
The upgrade stores a coherent database backup and recovery packet copy under `.rafi/backups/work-admission-v4/<database digest>/` before committing its guards. Rollback means pausing execution and preserving newer evidence for forward repair. Older writers are rejected against upgraded databases; do not down-migrate or restore a backup without accounting for subsequent provider work.
|
|
205
|
+
|
|
196
206
|
### GitHub PR Failure Recovery
|
|
197
207
|
|
|
198
208
|
When `--create-pr` cannot prepare GitHub, push a branch, or create a PR, Foreman records a structured failure and blocks the affected ticket cleanly. Blocked worktrees are retained automatically so you can repair the environment and retry without losing the ticket session.
|
package/dist/activity.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ export interface ActivityReporterOptions {
|
|
|
14
14
|
usefulProgressWarningMs?: number;
|
|
15
15
|
/** Override the safe automatic TTY rendering mode for tests or embedding hosts. */
|
|
16
16
|
ttyMode?: ActivityTtyMode;
|
|
17
|
+
/** Environment used for color policy; useful for isolated hosts and tests. */
|
|
18
|
+
colorEnvironment?: NodeJS.ProcessEnv;
|
|
17
19
|
}
|
|
18
20
|
export type ActivityTtyMode = "auto" | "cursor" | "records";
|
|
19
21
|
/** One process-local live view shared by nested RAFI operations. */
|
|
@@ -30,6 +32,7 @@ export declare class ActivityReporter {
|
|
|
30
32
|
private usefulProgressWarningPrinted;
|
|
31
33
|
private pauseStartedAt;
|
|
32
34
|
private readonly ttyMode;
|
|
35
|
+
private readonly colorEnvironment;
|
|
33
36
|
private readonly commandStartedAt;
|
|
34
37
|
private readonly phases;
|
|
35
38
|
private nextPhaseId;
|
package/dist/activity.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
2
|
import { AsyncQueue } from "./util/asyncQueue.js";
|
|
3
|
+
import { accent, error, info, sanitizeTerminalText, success, warning } from "./terminalStyle.js";
|
|
3
4
|
const FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
4
5
|
/** One process-local live view shared by nested RAFI operations. */
|
|
5
6
|
export class ActivityReporter {
|
|
@@ -15,6 +16,7 @@ export class ActivityReporter {
|
|
|
15
16
|
usefulProgressWarningPrinted = false;
|
|
16
17
|
pauseStartedAt = 0;
|
|
17
18
|
ttyMode;
|
|
19
|
+
colorEnvironment;
|
|
18
20
|
commandStartedAt;
|
|
19
21
|
phases = new Map();
|
|
20
22
|
nextPhaseId = 1;
|
|
@@ -43,6 +45,7 @@ export class ActivityReporter {
|
|
|
43
45
|
this.quietWarningMs = options.quietWarningMs ?? 60_000;
|
|
44
46
|
this.usefulProgressWarningMs = options.usefulProgressWarningMs ?? 300_000;
|
|
45
47
|
this.ttyMode = resolveTtyMode(options.ttyMode);
|
|
48
|
+
this.colorEnvironment = options.colorEnvironment ?? process.env;
|
|
46
49
|
this.commandStartedAt = this.now();
|
|
47
50
|
}
|
|
48
51
|
begin(label) {
|
|
@@ -94,15 +97,16 @@ export class ActivityReporter {
|
|
|
94
97
|
}
|
|
95
98
|
note(message) {
|
|
96
99
|
this.clearLine();
|
|
97
|
-
this.output.write(`${
|
|
100
|
+
this.output.write(`${sanitizeTerminalText(message).replace(/[\r\t]+/g, " ").slice(0, 500)}\n`);
|
|
98
101
|
this.render(true);
|
|
99
102
|
}
|
|
100
103
|
writePersistent(text) {
|
|
101
104
|
if (!text)
|
|
102
105
|
return;
|
|
103
106
|
this.clearLine();
|
|
104
|
-
|
|
105
|
-
|
|
107
|
+
const safeText = sanitizeTerminalText(text);
|
|
108
|
+
this.output.write(safeText);
|
|
109
|
+
if (!safeText.endsWith("\n"))
|
|
106
110
|
this.output.write("\n");
|
|
107
111
|
this.render(true);
|
|
108
112
|
}
|
|
@@ -197,7 +201,14 @@ export class ActivityReporter {
|
|
|
197
201
|
const quiet = quietFor >= this.quietWarningMs ? `provider quiet ${formatDuration(quietFor)}; RAFI is responsive` : this.detail;
|
|
198
202
|
const body = [phase?.label, provider || undefined, phase ? quiet : undefined, this.agentStatusLine].filter(Boolean).join(" — ");
|
|
199
203
|
if (this.output.isTTY) {
|
|
200
|
-
const
|
|
204
|
+
const color = { stream: this.output, env: this.colorEnvironment };
|
|
205
|
+
const status = phase?.label.toLowerCase() ?? "";
|
|
206
|
+
const marker = status.includes("fail") || status.includes("error") ? error("!", color)
|
|
207
|
+
: status.includes("complete") || status.includes("success") ? success("✓", color)
|
|
208
|
+
: status.includes("warn") || status.includes("quiet") ? warning("!", color)
|
|
209
|
+
: info(FRAMES[this.frame++ % FRAMES.length], color);
|
|
210
|
+
const safeBody = sanitizeTerminalText(body);
|
|
211
|
+
const line = `${marker} ${accent("RAFI", color)} working: ${safeBody} (${formatDuration(now - this.commandStartedAt)})`;
|
|
201
212
|
const semanticKey = semanticStatusKey(body);
|
|
202
213
|
if (this.ttyMode === "records") {
|
|
203
214
|
if (semanticKey === this.lastSemanticKey)
|
|
@@ -223,7 +234,8 @@ export class ActivityReporter {
|
|
|
223
234
|
return;
|
|
224
235
|
if (force || now - this.lastHeartbeatAt >= this.heartbeatMs) {
|
|
225
236
|
this.lastHeartbeatAt = now;
|
|
226
|
-
|
|
237
|
+
const color = { stream: this.output, env: this.colorEnvironment };
|
|
238
|
+
this.output.write(`[${new Date(now).toISOString()}] ${accent("rafi", color)} working: ${sanitizeTerminalText(body)} (${formatDuration(now - this.commandStartedAt)})\n`);
|
|
227
239
|
this.lastSemanticKey = semanticKey;
|
|
228
240
|
}
|
|
229
241
|
}
|
|
@@ -286,8 +298,15 @@ export function reportBuilderEvent(event) {
|
|
|
286
298
|
reporter.pulse();
|
|
287
299
|
break;
|
|
288
300
|
case "completed":
|
|
289
|
-
|
|
290
|
-
|
|
301
|
+
if (event.completionKnown !== false)
|
|
302
|
+
reporter.completedWork();
|
|
303
|
+
if (event.status === "unknown")
|
|
304
|
+
reporter.update("tool outcome unknown", event.name, { immediate: false });
|
|
305
|
+
else if (event.status === "timed_out")
|
|
306
|
+
reporter.update("tool timed out", event.name);
|
|
307
|
+
else if (event.status === "cancelled")
|
|
308
|
+
reporter.update("tool cancelled", event.name);
|
|
309
|
+
else if (isFailedToolCompletion(event))
|
|
291
310
|
reporter.update("tool failed", event.name);
|
|
292
311
|
else
|
|
293
312
|
reporter.pulse();
|
|
@@ -341,7 +360,7 @@ function isFailedToolCompletion(event) {
|
|
|
341
360
|
|| (typeof event.exitCode === "number" && Number.isFinite(event.exitCode) && event.exitCode !== 0);
|
|
342
361
|
}
|
|
343
362
|
function clean(value, maximum) {
|
|
344
|
-
return value.replace(/[\r\n\t]+/g, " ").replace(/\s+/g, " ").trim().slice(0, maximum);
|
|
363
|
+
return sanitizeTerminalText(value).replace(/[\r\n\t]+/g, " ").replace(/\s+/g, " ").trim().slice(0, maximum);
|
|
345
364
|
}
|
|
346
365
|
function semanticStatusKey(body) {
|
|
347
366
|
return body
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ProviderPhaseBarrier, type ProviderTurnPurpose } from "../providerPhase.js";
|
|
1
2
|
import type { SDKSessionInfo, PermissionResult } from "@anthropic-ai/claude-agent-sdk";
|
|
2
3
|
/** Lazy-load the Claude Agent SDK. Throws an actionable error if not installed. */
|
|
3
4
|
export declare function requireClaudeSDK(): Promise<typeof import("@anthropic-ai/claude-agent-sdk")>;
|
|
@@ -43,6 +44,12 @@ export declare function claudeApiRetryEvent(message: {
|
|
|
43
44
|
export declare class ClaudeAdapter implements BuilderAdapter {
|
|
44
45
|
private readonly opts;
|
|
45
46
|
readonly agent: "claude";
|
|
47
|
+
graphRuntimeSettings(): {
|
|
48
|
+
model: string | undefined;
|
|
49
|
+
effort: import("./types.js").EffortLevel | undefined;
|
|
50
|
+
fast: boolean | undefined;
|
|
51
|
+
runtimeExecutable: string | undefined;
|
|
52
|
+
};
|
|
46
53
|
private readonly inbox;
|
|
47
54
|
private readonly eventQueue;
|
|
48
55
|
private readonly query;
|
|
@@ -71,9 +78,20 @@ export declare class ClaudeAdapter implements BuilderAdapter {
|
|
|
71
78
|
private activeProviderTurnId?;
|
|
72
79
|
private activeProviderTurnSpanId?;
|
|
73
80
|
private readonly toolSpans;
|
|
81
|
+
private readonly toolCalls;
|
|
82
|
+
private readonly completedToolCalls;
|
|
83
|
+
private readonly unknownToolResults;
|
|
74
84
|
private pending?;
|
|
75
85
|
private questionTrace?;
|
|
76
86
|
private responseOnlyTurn;
|
|
87
|
+
readonly phaseBarrier: ProviderPhaseBarrier;
|
|
88
|
+
contractCapabilities(): {
|
|
89
|
+
sameSessionAcceptance: boolean;
|
|
90
|
+
nativeCompactionBarrier: boolean;
|
|
91
|
+
};
|
|
92
|
+
enableContractEnforcement(): void;
|
|
93
|
+
contractCompactionSequence(): number;
|
|
94
|
+
acceptContractDelivery(sequence: number): void;
|
|
77
95
|
private terminalResult?;
|
|
78
96
|
private streamEnded;
|
|
79
97
|
private closed;
|
|
@@ -86,6 +104,7 @@ export declare class ClaudeAdapter implements BuilderAdapter {
|
|
|
86
104
|
handback?: boolean;
|
|
87
105
|
responseOnly?: boolean;
|
|
88
106
|
logicalActionId?: string;
|
|
107
|
+
purpose?: ProviderTurnPurpose;
|
|
89
108
|
}): Promise<TurnResult>;
|
|
90
109
|
private sendTurnInternal;
|
|
91
110
|
private captureStderr;
|
package/dist/adapters/claude.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ProviderPhaseBarrier } from "../providerPhase.js";
|
|
1
2
|
import { OperationDeadline } from "../util/deadline.js";
|
|
2
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
3
4
|
import { dirname, join } from "node:path";
|
|
@@ -27,6 +28,7 @@ const DEFAULT_PROVIDER_IDLE_TIMEOUT_MS = 30 * 60_000;
|
|
|
27
28
|
*/
|
|
28
29
|
export function buildClaudeQueryOptions(opts) {
|
|
29
30
|
const qaReadOnly = opts.sessionRole === "qa" || opts.sandboxMode === "read-only";
|
|
31
|
+
const confinedBuilder = opts.sessionRole === "builder";
|
|
30
32
|
const base = {
|
|
31
33
|
cwd: opts.cwd,
|
|
32
34
|
pathToClaudeCodeExecutable: opts.runtimeExecutable,
|
|
@@ -41,18 +43,24 @@ export function buildClaudeQueryOptions(opts) {
|
|
|
41
43
|
// settings in those roles.
|
|
42
44
|
settingSources: qaReadOnly ? ["user"] : ["user", "project", "local"],
|
|
43
45
|
...(qaReadOnly ? { disallowedTools: ["Write", "Edit", "NotebookEdit"] } : {}),
|
|
46
|
+
...(confinedBuilder && !qaReadOnly ? { sandbox: {
|
|
47
|
+
enabled: true, failIfUnavailable: true, autoAllowBashIfSandboxed: false, allowUnsandboxedCommands: false,
|
|
48
|
+
network: opts.networkAccess === true ? { allowedDomains: ["*"] } : { allowedDomains: [], deniedDomains: ["*"] },
|
|
49
|
+
filesystem: { allowWrite: [opts.cwd] },
|
|
50
|
+
} } : {}),
|
|
44
51
|
...(qaReadOnly ? { sandbox: {
|
|
45
52
|
enabled: true,
|
|
46
53
|
failIfUnavailable: true,
|
|
47
54
|
autoAllowBashIfSandboxed: true,
|
|
48
55
|
allowUnsandboxedCommands: false,
|
|
56
|
+
network: { allowedDomains: [], deniedDomains: ["*"] },
|
|
49
57
|
filesystem: { allowWrite: [join(dirname(opts.cwd), "scratch")], denyWrite: [opts.cwd, ...(opts.configRoot ? [opts.configRoot] : [])] },
|
|
50
58
|
} } : {}),
|
|
51
59
|
};
|
|
52
60
|
const exactSkills = opts.preloadedSkillContent?.length
|
|
53
61
|
? ["# Preloaded Skills", "Use the following skills for this run.", ...opts.preloadedSkillContent.map((skill) => skill.content)].join("\n\n")
|
|
54
62
|
: undefined;
|
|
55
|
-
const systemAppend = [opts.systemPromptAppend, exactSkills].filter((part) => Boolean(part)).join("\n\n");
|
|
63
|
+
const systemAppend = [opts.systemPromptAppend, opts.sessionRole === "builder" ? `Builder runtime contract: shell network ${opts.networkAccess === true ? "explicitly approved for this build" : "disabled"}; sandbox fallback disabled. If acquisition requires unavailable access, request input first or use a provenance-verified local bundle. A ticket answer does not grant runtime permissions.` : undefined, exactSkills].filter((part) => Boolean(part)).join("\n\n");
|
|
56
64
|
if (systemAppend) {
|
|
57
65
|
base.systemPrompt = { type: "preset", preset: "claude_code", append: systemAppend };
|
|
58
66
|
}
|
|
@@ -165,6 +173,7 @@ export function claudeApiRetryEvent(message) {
|
|
|
165
173
|
export class ClaudeAdapter {
|
|
166
174
|
opts;
|
|
167
175
|
agent = "claude";
|
|
176
|
+
graphRuntimeSettings() { return { model: this.opts.model, effort: this.opts.effort, fast: this.opts.fast, runtimeExecutable: this.opts.runtimeExecutable }; }
|
|
168
177
|
inbox = new AsyncQueue();
|
|
169
178
|
eventQueue = new BuilderEventQueue();
|
|
170
179
|
query;
|
|
@@ -193,9 +202,17 @@ export class ClaudeAdapter {
|
|
|
193
202
|
activeProviderTurnId;
|
|
194
203
|
activeProviderTurnSpanId;
|
|
195
204
|
toolSpans = new Map();
|
|
205
|
+
toolCalls = new Map();
|
|
206
|
+
completedToolCalls = new Set();
|
|
207
|
+
unknownToolResults = new Map();
|
|
196
208
|
pending;
|
|
197
209
|
questionTrace;
|
|
198
210
|
responseOnlyTurn = false;
|
|
211
|
+
phaseBarrier = new ProviderPhaseBarrier();
|
|
212
|
+
contractCapabilities() { return { sameSessionAcceptance: true, nativeCompactionBarrier: true }; }
|
|
213
|
+
enableContractEnforcement() { this.phaseBarrier.enableEnforcement(); }
|
|
214
|
+
contractCompactionSequence() { return this.phaseBarrier.compactionSequence; }
|
|
215
|
+
acceptContractDelivery(sequence) { this.phaseBarrier.accept(sequence); }
|
|
199
216
|
terminalResult;
|
|
200
217
|
streamEnded = false;
|
|
201
218
|
closed = false;
|
|
@@ -239,6 +256,7 @@ export class ClaudeAdapter {
|
|
|
239
256
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
240
257
|
constructor(opts, query) {
|
|
241
258
|
this.opts = opts;
|
|
259
|
+
this.phaseBarrier.begin("initialization");
|
|
242
260
|
this._sessionId = opts.resumeSessionRef?.sessionId ?? opts.resumeSessionId;
|
|
243
261
|
this._sessionRef = opts.resumeSessionRef;
|
|
244
262
|
this.sessionIdentityReady = new Promise((resolve, reject) => {
|
|
@@ -255,7 +273,15 @@ export class ClaudeAdapter {
|
|
|
255
273
|
options: {
|
|
256
274
|
...buildClaudeQueryOptions(opts),
|
|
257
275
|
abortController: this.abort,
|
|
258
|
-
hooks: {
|
|
276
|
+
hooks: {
|
|
277
|
+
PreToolUse: [{ hooks: [async (input) => {
|
|
278
|
+
if (input.hook_event_name !== "PreToolUse")
|
|
279
|
+
return {};
|
|
280
|
+
const reason = this.phaseBarrier.denial(input.tool_name);
|
|
281
|
+
return reason ? { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: reason } } : {};
|
|
282
|
+
}] }],
|
|
283
|
+
PreCompact: [{ hooks: [async () => { this.phaseBarrier.compact(); return {}; }] }],
|
|
284
|
+
SessionStart: [{ hooks: [async (input) => {
|
|
259
285
|
if (input.hook_event_name !== "SessionStart" || input.agent_id)
|
|
260
286
|
return {};
|
|
261
287
|
try {
|
|
@@ -266,7 +292,8 @@ export class ClaudeAdapter {
|
|
|
266
292
|
throw error;
|
|
267
293
|
}
|
|
268
294
|
return {};
|
|
269
|
-
}] }]
|
|
295
|
+
}] }]
|
|
296
|
+
},
|
|
270
297
|
stderr: (data) => this.captureStderr(data),
|
|
271
298
|
canUseTool: async (toolName, input, requestOptions = {}) => {
|
|
272
299
|
if (this.responseOnlyTurn)
|
|
@@ -372,6 +399,9 @@ export class ClaudeAdapter {
|
|
|
372
399
|
}
|
|
373
400
|
else if (block.type === "tool_use") {
|
|
374
401
|
const callId = block.id;
|
|
402
|
+
this.toolCalls.set(callId, { name: block.name, input: block.input });
|
|
403
|
+
if (this.toolCalls.size > 1000)
|
|
404
|
+
this.toolCalls.delete(this.toolCalls.keys().next().value);
|
|
375
405
|
this.eventQueue.push({
|
|
376
406
|
kind: "tool",
|
|
377
407
|
name: block.name,
|
|
@@ -512,6 +542,7 @@ export class ClaudeAdapter {
|
|
|
512
542
|
if (this.pending)
|
|
513
543
|
throw new Error("a turn is already in progress");
|
|
514
544
|
this.responseOnlyTurn = Boolean(policy?.responseOnly);
|
|
545
|
+
this.phaseBarrier.begin(policy?.purpose ?? (policy?.responseOnly ? "response-repair" : "implementation"));
|
|
515
546
|
const turnId = randomUUID();
|
|
516
547
|
this.activeProviderTurnId = turnId;
|
|
517
548
|
const observer = this.opts.observer;
|
|
@@ -597,14 +628,35 @@ export class ClaudeAdapter {
|
|
|
597
628
|
const callId = typeof block.tool_use_id === "string" ? block.tool_use_id : undefined;
|
|
598
629
|
if (!callId)
|
|
599
630
|
continue;
|
|
631
|
+
if (this.completedToolCalls.has(callId))
|
|
632
|
+
continue;
|
|
633
|
+
const call = this.toolCalls.get(callId);
|
|
600
634
|
const spanId = this.toolSpans.get(callId);
|
|
601
635
|
const text = typeof block.content === "string" ? block.content : JSON.stringify(block.content ?? "");
|
|
602
|
-
const sanitized = text.replace(/\b(
|
|
636
|
+
const sanitized = text.replace(/\b(token|password|secret|api[_-]?key)\s*[:=]\s*\S+/gi, "$1=[REDACTED]");
|
|
603
637
|
const failed = block.is_error === true;
|
|
638
|
+
// TIME_ELAPSED alone does not establish success or failure. Preserve it as unknown.
|
|
639
|
+
const elapsedOnly = /^\s*TIME_ELAPSED\s*$/.test(text);
|
|
640
|
+
const status = failed && /timed?\s*out|timeout/i.test(text) ? "timed_out" : failed && /cancel(?:led|ed)/i.test(text) ? "cancelled" : elapsedOnly ? "unknown" : failed ? "failed" : "completed";
|
|
641
|
+
if (status !== "unknown") {
|
|
642
|
+
this.completedToolCalls.add(callId);
|
|
643
|
+
if (this.completedToolCalls.size > 1000)
|
|
644
|
+
this.completedToolCalls.delete(this.completedToolCalls.values().next().value);
|
|
645
|
+
this.toolCalls.delete(callId);
|
|
646
|
+
}
|
|
604
647
|
const outputDigest = createHash("sha256").update(text).digest("hex");
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
648
|
+
if (status === "unknown") {
|
|
649
|
+
if (this.unknownToolResults.get(callId) === outputDigest)
|
|
650
|
+
continue;
|
|
651
|
+
this.unknownToolResults.set(callId, outputDigest);
|
|
652
|
+
if (this.unknownToolResults.size > 1000)
|
|
653
|
+
this.unknownToolResults.delete(this.unknownToolResults.keys().next().value);
|
|
654
|
+
}
|
|
655
|
+
else
|
|
656
|
+
this.unknownToolResults.delete(callId);
|
|
657
|
+
this.eventQueue.push({ kind: "tool", name: call?.name ?? "unidentified tool", input: call?.input ?? {}, inputCompleteness: call ? "complete" : "unavailable", lifecycle: "completed", callId, status, outputSummary: sanitized.slice(0, 1000), outputDigest, outputCompleteness: block.is_truncated === true ? "truncated" : "complete", rawOutputBytes: Buffer.byteLength(text), completionKnown: status !== "unknown", providerTurnId: this.activeProviderTurnId, eventId: randomUUID(), observedAt: new Date().toISOString() });
|
|
658
|
+
if (spanId && this.opts.observer && status !== "unknown") {
|
|
659
|
+
this.opts.observer.store.finishSpan(spanId, { outcome: status, completionKnown: true, attributes: { callId, outputSummary: sanitized.slice(0, 1000), outputDigest } });
|
|
608
660
|
this.toolSpans.delete(callId);
|
|
609
661
|
}
|
|
610
662
|
this.opts.observer?.signal(true);
|
package/dist/adapters/codex.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ProviderTurnPurpose } from "../providerPhase.js";
|
|
1
2
|
import type { BuilderAdapter, BuilderAdapterOptions, BuilderEvent, CompactResult, ContextUsage, NativeAutoCompactionPolicy, NativeCompaction, ProviderSessionUsage, ProviderSettingSwitch, TurnResult } from "./types.js";
|
|
2
3
|
import type { ProviderSessionRefV1, SessionAvailabilityV1 } from "rafi-spec";
|
|
3
4
|
export interface CodexLineResult {
|
|
@@ -11,6 +12,12 @@ export declare function parseCodexLine(raw: Record<string, unknown>): CodexLineR
|
|
|
11
12
|
export declare class CodexAdapter implements BuilderAdapter {
|
|
12
13
|
private readonly opts;
|
|
13
14
|
readonly agent: "codex";
|
|
15
|
+
graphRuntimeSettings(): {
|
|
16
|
+
model: string | undefined;
|
|
17
|
+
effort: import("./types.js").EffortLevel | undefined;
|
|
18
|
+
fast: boolean | undefined;
|
|
19
|
+
runtimeExecutable: string | undefined;
|
|
20
|
+
};
|
|
14
21
|
private readonly eventQueue;
|
|
15
22
|
private process?;
|
|
16
23
|
private nextId;
|
|
@@ -40,9 +47,17 @@ export declare class CodexAdapter implements BuilderAdapter {
|
|
|
40
47
|
private compactionUncertain;
|
|
41
48
|
private observedToolCalls;
|
|
42
49
|
private activeProviderTurnId?;
|
|
50
|
+
private activeNativeTurnId?;
|
|
51
|
+
private connectionGeneration;
|
|
52
|
+
private readonly approvalRequests;
|
|
53
|
+
private readonly answeredApprovalRequests;
|
|
43
54
|
private activeProviderTurnSpanId?;
|
|
44
55
|
private readonly toolSpans;
|
|
45
56
|
constructor(opts: BuilderAdapterOptions);
|
|
57
|
+
contractCapabilities(): {
|
|
58
|
+
sameSessionAcceptance: boolean;
|
|
59
|
+
nativeCompactionBarrier: boolean;
|
|
60
|
+
};
|
|
46
61
|
buildInstruction(instruction: string): string;
|
|
47
62
|
buildAppServerArgs(): string[];
|
|
48
63
|
/** Deprecated fixture helper. Runtime execution uses only app-server. */
|
|
@@ -51,6 +66,7 @@ export declare class CodexAdapter implements BuilderAdapter {
|
|
|
51
66
|
handback?: boolean;
|
|
52
67
|
responseOnly?: boolean;
|
|
53
68
|
logicalActionId?: string;
|
|
69
|
+
purpose?: ProviderTurnPurpose;
|
|
54
70
|
}): Promise<TurnResult>;
|
|
55
71
|
private sendTurnInternal;
|
|
56
72
|
compact(): Promise<CompactResult>;
|
|
@@ -85,6 +101,10 @@ export declare class CodexAdapter implements BuilderAdapter {
|
|
|
85
101
|
private traceLifecycle;
|
|
86
102
|
private touchWaiters;
|
|
87
103
|
private providerIdleTimeoutMs;
|
|
104
|
+
private effectiveNetworkAccess;
|
|
105
|
+
private effectiveApprovalPolicy;
|
|
106
|
+
/** Fail closed; approval is for this operation only, never a session cache or policy amendment. */
|
|
107
|
+
private handleServerRequest;
|
|
88
108
|
private disconnect;
|
|
89
109
|
private restartForAutoCompaction;
|
|
90
110
|
private buildSkillsAppendix;
|
package/dist/adapters/codex.js
CHANGED
|
@@ -31,6 +31,7 @@ const DEFAULT_PROVIDER_IDLE_TIMEOUT_MS = 30 * 60_000;
|
|
|
31
31
|
export class CodexAdapter {
|
|
32
32
|
opts;
|
|
33
33
|
agent = "codex";
|
|
34
|
+
graphRuntimeSettings() { return { model: this.opts.model, effort: this.opts.effort, fast: this.opts.fast, runtimeExecutable: this.opts.runtimeExecutable }; }
|
|
34
35
|
eventQueue = new BuilderEventQueue();
|
|
35
36
|
process;
|
|
36
37
|
nextId = 1;
|
|
@@ -60,6 +61,10 @@ export class CodexAdapter {
|
|
|
60
61
|
compactionUncertain = false;
|
|
61
62
|
observedToolCalls = 0;
|
|
62
63
|
activeProviderTurnId;
|
|
64
|
+
activeNativeTurnId;
|
|
65
|
+
connectionGeneration = 0;
|
|
66
|
+
approvalRequests = new Map();
|
|
67
|
+
answeredApprovalRequests = new Set();
|
|
63
68
|
activeProviderTurnSpanId;
|
|
64
69
|
toolSpans = new Map();
|
|
65
70
|
constructor(opts) {
|
|
@@ -67,8 +72,11 @@ export class CodexAdapter {
|
|
|
67
72
|
this._sessionId = opts.resumeSessionRef?.sessionId ?? opts.resumeSessionId;
|
|
68
73
|
this._sessionRef = opts.resumeSessionRef;
|
|
69
74
|
}
|
|
75
|
+
contractCapabilities() { return { sameSessionAcceptance: true, nativeCompactionBarrier: false }; }
|
|
70
76
|
buildInstruction(instruction) {
|
|
71
|
-
return [this.opts.systemPromptAppend,
|
|
77
|
+
return [this.opts.systemPromptAppend,
|
|
78
|
+
this.opts.sessionRole === "builder" ? `Builder runtime contract: shell network ${this.effectiveNetworkAccess() ? "explicitly approved for this build" : "disabled"}; sandbox escalation ${this.effectiveApprovalPolicy() === "on-request" ? "requires separate approval for each exact operation" : "disabled"}. Web tools do not establish shell network access. If acquisition needs unavailable capabilities, request input before running acquisition, or use a local source/dependency bundle with verified provenance. A ticket answer does not change runtime permissions.` : undefined,
|
|
79
|
+
this.buildSkillsAppendix(), instruction]
|
|
72
80
|
.filter((part) => Boolean(part)).join("\n\n");
|
|
73
81
|
}
|
|
74
82
|
buildAppServerArgs() {
|
|
@@ -102,6 +110,7 @@ export class CodexAdapter {
|
|
|
102
110
|
}
|
|
103
111
|
finally {
|
|
104
112
|
this.activeProviderTurnId = undefined;
|
|
113
|
+
this.activeNativeTurnId = undefined;
|
|
105
114
|
}
|
|
106
115
|
}
|
|
107
116
|
const context = this.observationContext();
|
|
@@ -123,6 +132,7 @@ export class CodexAdapter {
|
|
|
123
132
|
this.toolSpans.delete(callId);
|
|
124
133
|
}
|
|
125
134
|
this.activeProviderTurnId = undefined;
|
|
135
|
+
this.activeNativeTurnId = undefined;
|
|
126
136
|
this.activeProviderTurnSpanId = undefined;
|
|
127
137
|
}
|
|
128
138
|
}
|
|
@@ -140,13 +150,18 @@ export class CodexAdapter {
|
|
|
140
150
|
this.turnUsageBaseline = this.providerSessionUsage ? { ...this.providerSessionUsage } : undefined;
|
|
141
151
|
const completion = this.waitFor("turn/completed", (params) => params.threadId === this._sessionId, this.providerIdleTimeoutMs(), true);
|
|
142
152
|
turnStartDispatched = true;
|
|
143
|
-
await this.request("turn/start", {
|
|
153
|
+
const started = await this.request("turn/start", {
|
|
144
154
|
threadId: this._sessionId,
|
|
145
155
|
input: [{ type: "text", text: providerInstruction, text_elements: [] }],
|
|
146
156
|
cwd: this.opts.cwd,
|
|
147
157
|
model: this.opts.model ?? null,
|
|
148
158
|
effort: this.opts.effort ?? (this.opts.fast ? "low" : null),
|
|
159
|
+
approvalPolicy: policy?.responseOnly || policy?.purpose && policy.purpose !== "implementation" ? "never" : this.effectiveApprovalPolicy(),
|
|
160
|
+
sandboxPolicy: policy?.responseOnly || policy?.purpose && policy.purpose !== "implementation" || this.opts.sandboxMode === "read-only" || this.opts.sessionRole === "qa"
|
|
161
|
+
? { type: "readOnly", networkAccess: false }
|
|
162
|
+
: { type: "workspaceWrite", writableRoots: [this.opts.cwd], networkAccess: this.effectiveNetworkAccess() },
|
|
149
163
|
});
|
|
164
|
+
this.activeNativeTurnId = String(started.turn?.id ?? "") || undefined;
|
|
150
165
|
const params = await new OperationDeadline("Codex active turn", this.opts.turnDeadlineMs ?? 3_600_000).run(() => completion);
|
|
151
166
|
const turn = params.turn;
|
|
152
167
|
const responseOnlyViolation = Boolean(policy?.responseOnly && this.observedToolCalls !== toolsBefore);
|
|
@@ -319,7 +334,7 @@ export class CodexAdapter {
|
|
|
319
334
|
if (this.opts.allowAutoCompactionSetupTurn === false)
|
|
320
335
|
return;
|
|
321
336
|
const toolsBefore = this.observedToolCalls;
|
|
322
|
-
const setup = await this.sendTurnInternal("Rafi internal initialization only. Do not call tools or modify files. Reply briefly that the context is ready.");
|
|
337
|
+
const setup = await this.sendTurnInternal("Rafi internal initialization only. Do not call tools or modify files. Reply briefly that the context is ready.", { purpose: "initialization", responseOnly: true });
|
|
323
338
|
if (setup.isError)
|
|
324
339
|
throw new Error(`Codex automatic-compaction setup failed: ${setup.text.slice(0, 240)}`);
|
|
325
340
|
if (this.observedToolCalls !== toolsBefore)
|
|
@@ -457,8 +472,9 @@ export class CodexAdapter {
|
|
|
457
472
|
try {
|
|
458
473
|
result = await this.request(method, {
|
|
459
474
|
...(this._sessionId ? { threadId: this._sessionId } : {}), cwd: this.opts.cwd,
|
|
460
|
-
model: this.opts.model ?? null, approvalPolicy:
|
|
461
|
-
|
|
475
|
+
model: this.opts.model ?? null, approvalPolicy: this.effectiveApprovalPolicy(),
|
|
476
|
+
config: { sandbox_workspace_write: { network_access: this.effectiveNetworkAccess() } },
|
|
477
|
+
sandbox: this.opts.sandboxMode === "read-only" || this.opts.sessionRole === "qa" ? "read-only" : "workspace-write",
|
|
462
478
|
developerInstructions: this.opts.systemPromptAppend ?? null,
|
|
463
479
|
});
|
|
464
480
|
}
|
|
@@ -474,6 +490,17 @@ export class CodexAdapter {
|
|
|
474
490
|
}
|
|
475
491
|
throw error;
|
|
476
492
|
}
|
|
493
|
+
const effective = result.sandbox;
|
|
494
|
+
if (this.effectiveNetworkAccess() && !effective)
|
|
495
|
+
throw new Error("Codex did not report an effective sandbox policy for the requested network grant; no turn dispatched");
|
|
496
|
+
const requestedSandbox = this.opts.sandboxMode === "read-only" || this.opts.sessionRole === "qa" ? "readOnly" : "workspaceWrite";
|
|
497
|
+
if (effective && (effective.type !== requestedSandbox || effective.networkAccess === true && !this.effectiveNetworkAccess()))
|
|
498
|
+
throw new Error("Codex effective sandbox exceeds the authorized role policy; no turn dispatched");
|
|
499
|
+
if (effective && this.effectiveNetworkAccess() && effective.networkAccess !== true)
|
|
500
|
+
throw new Error("Codex did not enable the explicitly approved builder network; use an offline bundle or correct provider policy before implementation");
|
|
501
|
+
if (result.approvalPolicy !== undefined && result.approvalPolicy !== this.effectiveApprovalPolicy())
|
|
502
|
+
throw new Error("Codex effective approval policy differs from the requested role policy; no turn dispatched");
|
|
503
|
+
this.eventQueue.push({ kind: "activity", state: "Codex role capabilities", provider: "codex", detail: JSON.stringify({ requested: { sandbox: requestedSandbox, networkAccess: this.effectiveNetworkAccess(), approvalPolicy: this.effectiveApprovalPolicy() }, effective: effective ?? "unknown", approvalPolicy: result.approvalPolicy ?? "unknown" }) });
|
|
477
504
|
const thread = result.thread;
|
|
478
505
|
const returnedSessionId = String(thread?.id ?? this._sessionId ?? "") || undefined;
|
|
479
506
|
if (this._sessionRef && returnedSessionId !== this._sessionRef.sessionId) {
|
|
@@ -567,6 +594,10 @@ export class CodexAdapter {
|
|
|
567
594
|
}
|
|
568
595
|
handle(message) {
|
|
569
596
|
this.touchWaiters();
|
|
597
|
+
if (message.id !== undefined && message.method) {
|
|
598
|
+
void this.handleServerRequest(message);
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
570
601
|
if (message.id !== undefined) {
|
|
571
602
|
const pending = this.pending.get(message.id);
|
|
572
603
|
if (pending) {
|
|
@@ -737,6 +768,11 @@ export class CodexAdapter {
|
|
|
737
768
|
if (timeoutMs !== undefined) {
|
|
738
769
|
const arm = () => {
|
|
739
770
|
timeout = setTimeout(() => {
|
|
771
|
+
// Human approval has its own bounded deadline; it is not provider idleness.
|
|
772
|
+
if (resetOnProviderActivity && this.approvalRequests.size > 0) {
|
|
773
|
+
arm();
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
740
776
|
const active = this.notificationWaiters.get(method) ?? [];
|
|
741
777
|
this.notificationWaiters.set(method, active.filter((candidate) => candidate !== waiter));
|
|
742
778
|
clear();
|
|
@@ -765,7 +801,65 @@ export class CodexAdapter {
|
|
|
765
801
|
const configured = this.opts.providerIdleTimeoutMs;
|
|
766
802
|
return Number.isFinite(configured) && configured > 0 ? configured : DEFAULT_PROVIDER_IDLE_TIMEOUT_MS;
|
|
767
803
|
}
|
|
804
|
+
effectiveNetworkAccess() {
|
|
805
|
+
return this.opts.sandboxMode !== "read-only" && this.opts.sessionRole !== "qa" && this.opts.networkAccess === true;
|
|
806
|
+
}
|
|
807
|
+
effectiveApprovalPolicy() {
|
|
808
|
+
return this.opts.sandboxMode === "read-only" || this.opts.sessionRole === "qa" ? "never" : this.opts.approvalPolicy ?? "never";
|
|
809
|
+
}
|
|
810
|
+
/** Fail closed; approval is for this operation only, never a session cache or policy amendment. */
|
|
811
|
+
async handleServerRequest(message) {
|
|
812
|
+
await Promise.resolve();
|
|
813
|
+
const id = message.id;
|
|
814
|
+
const identity = `${typeof id}:${id}`;
|
|
815
|
+
if (this.approvalRequests.has(identity) || this.answeredApprovalRequests.has(identity))
|
|
816
|
+
return;
|
|
817
|
+
const params = message.params ?? {};
|
|
818
|
+
const command = message.method === "item/commandExecution/requestApproval";
|
|
819
|
+
const fileChange = message.method === "item/fileChange/requestApproval";
|
|
820
|
+
if (!command && !fileChange) {
|
|
821
|
+
this.write({ id, error: { code: -32601, message: `Unsupported server request: ${message.method}` } });
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
const connection = this.process;
|
|
825
|
+
const generation = this.connectionGeneration;
|
|
826
|
+
const hostTurnId = this.activeProviderTurnId;
|
|
827
|
+
const controller = new AbortController();
|
|
828
|
+
this.approvalRequests.set(identity, controller);
|
|
829
|
+
let decision = "decline";
|
|
830
|
+
try {
|
|
831
|
+
if (this.effectiveApprovalPolicy() === "on-request" && params.threadId === this._sessionId && this.activeProviderTurnId && typeof params.turnId === "string" && typeof params.itemId === "string" && params.turnId === this.activeNativeTurnId && (params.cwd === undefined || canonicalSessionPath(String(params.cwd)) === canonicalSessionPath(this.opts.cwd))) {
|
|
832
|
+
const verdict = await new OperationDeadline("Codex approval", this.opts.approvalTimeoutMs ?? 300_000).run(() => this.opts.permission({
|
|
833
|
+
toolName: command ? "Bash" : "Write",
|
|
834
|
+
input: { ...params, providerSandboxEscalation: true, requestedCommand: params.command },
|
|
835
|
+
signal: controller.signal, toolUseID: String(params.itemId),
|
|
836
|
+
title: "Approve this exact sandbox escalation?", description: String(params.reason ?? "Provider requests access beyond the offline workspace sandbox"),
|
|
837
|
+
}), () => controller.abort());
|
|
838
|
+
if (!controller.signal.aborted && verdict.behavior === "allow" && params.threadId === this._sessionId && this.activeProviderTurnId === hostTurnId && params.turnId === this.activeNativeTurnId)
|
|
839
|
+
decision = "accept";
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
catch {
|
|
843
|
+
controller.abort();
|
|
844
|
+
}
|
|
845
|
+
finally {
|
|
846
|
+
this.approvalRequests.delete(identity);
|
|
847
|
+
this.touchWaiters();
|
|
848
|
+
this.answeredApprovalRequests.add(identity);
|
|
849
|
+
if (generation === this.connectionGeneration && connection === this.process && !this.closed) {
|
|
850
|
+
try {
|
|
851
|
+
this.write({ id, result: { decision } });
|
|
852
|
+
}
|
|
853
|
+
catch { /* disconnected: no stale reply on another connection */ }
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
}
|
|
768
857
|
disconnect(error) {
|
|
858
|
+
this.connectionGeneration++;
|
|
859
|
+
for (const controller of this.approvalRequests.values())
|
|
860
|
+
controller.abort();
|
|
861
|
+
this.approvalRequests.clear();
|
|
862
|
+
this.answeredApprovalRequests.clear();
|
|
769
863
|
const child = this.process;
|
|
770
864
|
this.process = undefined;
|
|
771
865
|
this.initialized = false;
|
|
@@ -23,8 +23,17 @@ export declare class RecoveringAdapter implements BuilderAdapter {
|
|
|
23
23
|
private readonly eventQueue;
|
|
24
24
|
private eventPump;
|
|
25
25
|
private closed;
|
|
26
|
+
private contractEnforcing;
|
|
26
27
|
constructor(opts: RecoveringAdapterOptions);
|
|
28
|
+
graphRuntimeSettings(): Pick<import("./types.js").BuilderAdapterOptions, "model" | "effort" | "fast" | "runtimeExecutable"> | undefined;
|
|
27
29
|
get agent(): "claude" | "codex";
|
|
30
|
+
contractCapabilities(): {
|
|
31
|
+
sameSessionAcceptance: boolean;
|
|
32
|
+
nativeCompactionBarrier: boolean;
|
|
33
|
+
};
|
|
34
|
+
enableContractEnforcement(): void;
|
|
35
|
+
contractCompactionSequence(): number;
|
|
36
|
+
acceptContractDelivery(sequence: number): void;
|
|
28
37
|
sendTurn(text: string, policy?: Parameters<BuilderAdapter["sendTurn"]>[1]): Promise<TurnResult>;
|
|
29
38
|
sessionId(): string | undefined;
|
|
30
39
|
sessionRef(): ProviderSessionRefV1 | undefined;
|