@kylecheng3146/agent-ops 0.5.3 → 0.6.0
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 +48 -0
- package/dist/packages/cli/src/args.js +165 -2
- package/dist/packages/cli/src/bin.js +64 -17
- package/dist/packages/cli/src/cli.js +33 -6
- package/dist/packages/cli/src/commands/advance.js +23 -3
- package/dist/packages/cli/src/commands/review-show.js +1 -1
- package/dist/packages/cli/src/commands/review.js +100 -9
- package/dist/packages/cli/src/commands/run.js +111 -0
- package/dist/packages/cli/src/commands/task.js +61 -17
- package/dist/packages/cli/src/commands/verify.js +1 -0
- package/dist/packages/cli/src/context.js +63 -1
- package/dist/packages/cli/src/hook-process.js +31 -1
- package/dist/packages/cli/src/owned-run-step.js +65 -0
- package/dist/packages/cli/src/parallel-deps.js +18 -5
- package/dist/packages/cli/src/run-deps.js +314 -0
- package/dist/packages/cli/src/run-entry.js +943 -0
- package/dist/packages/cli/src/run-setup-entry.js +32 -0
- package/dist/packages/cli/src/run-step-entry.js +58 -0
- package/dist/runtime/src/config/explain.js +6 -0
- package/dist/runtime/src/config/hash.js +1 -1
- package/dist/runtime/src/config/merge.js +9 -1
- package/dist/runtime/src/contracts.js +2 -2
- package/dist/runtime/src/hooks/completion-gate.js +8 -2
- package/dist/runtime/src/install/doctor.js +2 -0
- package/dist/runtime/src/parallel/finish.js +85 -22
- package/dist/runtime/src/parallel/guard.js +5 -1
- package/dist/runtime/src/parallel/integrate.js +288 -10
- package/dist/runtime/src/parallel/manage.js +4 -1
- package/dist/runtime/src/parallel/receipt.js +71 -5
- package/dist/runtime/src/parallel/service.js +41 -1
- package/dist/runtime/src/review/attestation.js +36 -11
- package/dist/runtime/src/review/execute.js +24 -1
- package/dist/runtime/src/review/packet.js +35 -1
- package/dist/runtime/src/review/runner.js +12 -1
- package/dist/runtime/src/review/scope.js +18 -1
- package/dist/runtime/src/review/slots.js +35 -10
- package/dist/runtime/src/run/authorization.js +60 -0
- package/dist/runtime/src/run/budget.js +30 -0
- package/dist/runtime/src/run/controls.js +139 -0
- package/dist/runtime/src/run/convergence.js +177 -0
- package/dist/runtime/src/run/hosts/claude.js +180 -0
- package/dist/runtime/src/run/hosts/codex.js +279 -0
- package/dist/runtime/src/run/hosts/index.js +4 -0
- package/dist/runtime/src/run/hosts/protocol.js +254 -0
- package/dist/runtime/src/run/hosts/types.js +1 -0
- package/dist/runtime/src/run/hosts/util.js +136 -0
- package/dist/runtime/src/run/integration.js +204 -0
- package/dist/runtime/src/run/macOS.js +189 -0
- package/dist/runtime/src/run/ownership.js +22 -0
- package/dist/runtime/src/run/planning.js +25 -0
- package/dist/runtime/src/run/policy-transition.js +1076 -0
- package/dist/runtime/src/run/policy.js +176 -0
- package/dist/runtime/src/run/ratchet.js +71 -0
- package/dist/runtime/src/run/scheduler.js +260 -0
- package/dist/runtime/src/run/service.js +425 -0
- package/dist/runtime/src/run/supervisor.js +635 -0
- package/dist/runtime/src/run/transport.js +159 -0
- package/dist/runtime/src/run/types.js +1 -0
- package/dist/runtime/src/run/usage.js +81 -0
- package/dist/runtime/src/run/verification.js +64 -0
- package/dist/runtime/src/run/worker-gate.js +48 -0
- package/dist/runtime/src/schema/acceptance.js +91 -0
- package/dist/runtime/src/schema/validate.js +67 -7
- package/dist/runtime/src/task/completion.js +14 -2
- package/dist/runtime/src/task/contract.js +17 -0
- package/dist/runtime/src/task/pin-finding.js +81 -0
- package/dist/runtime/src/task/service.js +316 -13
- package/dist/runtime/src/task/store.js +57 -7
- package/dist/runtime/src/verify/acceptance-coverage.js +85 -0
- package/dist/runtime/src/verify/acceptance-protocol.js +252 -0
- package/dist/runtime/src/verify/acceptance-replay.js +239 -0
- package/dist/runtime/src/verify/adapters/index.js +5 -0
- package/dist/runtime/src/verify/adapters/jest.js +142 -0
- package/dist/runtime/src/verify/adapters/node-reporter.js +20 -0
- package/dist/runtime/src/verify/adapters/node.js +254 -0
- package/dist/runtime/src/verify/adapters/pytest.js +188 -0
- package/dist/runtime/src/verify/adapters/rust.js +84 -0
- package/dist/runtime/src/verify/adapters/shared.js +82 -0
- package/dist/runtime/src/verify/adapters/vitest.js +218 -0
- package/dist/runtime/src/verify/evidence.js +5 -2
- package/dist/runtime/src/verify/service.js +24 -8
- package/dist/runtime/src/verify/source-fingerprint.js +1 -1
- package/dist/runtime/src/verify/spawn.js +13 -2
- package/docs/en/spec/acceptance-and-evidence.md +48 -0
- package/docs/en/spec/loop-engineering.md +49 -0
- package/docs/zh-TW/spec/acceptance-and-evidence.md +21 -1
- package/docs/zh-TW/spec/loop-engineering.md +22 -0
- package/package.json +1 -1
- package/schemas/config.schema.json +221 -19
- package/schemas/evidence.schema.json +129 -6
- package/schemas/task.schema.json +265 -6
- package/templates/acceptance/agent_ops_acceptance.rs +36 -0
- package/templates/acceptance/agent_ops_pytest.py +46 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { AgentOpsError } from "../../../runtime/src/fs/paths.js";
|
|
4
|
+
import { nativeProcessIdentity, NativeRunTransport } from "../../../runtime/src/run/transport.js";
|
|
5
|
+
import { ClaudeGoalHost } from "../../../runtime/src/run/hosts/claude.js";
|
|
6
|
+
import { CodexGoalHost } from "../../../runtime/src/run/hosts/codex.js";
|
|
7
|
+
import { activeWallTimeMs } from "../../../runtime/src/run/scheduler.js";
|
|
8
|
+
import { runPolicyContext } from "./context.js";
|
|
9
|
+
/** Native-issued proof commands use the same dormant group as final evidence. */
|
|
10
|
+
export async function runOwnedLocalProof(argv) {
|
|
11
|
+
if (!["verify", "review", "batch"].includes(argv[0] ?? "") || argv.includes("--help") || process.env.AGENT_OPS_RUN_PROOF_PID !== undefined)
|
|
12
|
+
return null;
|
|
13
|
+
const context = await runPolicyContext(process.cwd());
|
|
14
|
+
if (context === null)
|
|
15
|
+
return null;
|
|
16
|
+
const { repository, state, worker } = context;
|
|
17
|
+
if (worker === undefined || !["running", "idle"].includes(worker.status) || worker.stopIntent !== null)
|
|
18
|
+
throw new AgentOpsError("RUN_WORKER_STALE", "Local proof requires an active registered writer checkout.");
|
|
19
|
+
if (worker.proofProcess != null) {
|
|
20
|
+
const transport = new NativeRunTransport(state.host === "codex" ? new CodexGoalHost() : new ClaudeGoalHost(), repository);
|
|
21
|
+
const existing = await transport.inspect({ nativeSessionId: "worker-proof", nativeJobId: null, ...worker.proofProcess, instance: state.nativeInstance });
|
|
22
|
+
if (existing.processAlive)
|
|
23
|
+
throw new AgentOpsError("RUN_WORKER_PROOF_ACTIVE", "A registered proof command is already running for this worker.");
|
|
24
|
+
}
|
|
25
|
+
const remaining = state.budget.limitMs - activeWallTimeMs(state.budget.activeIntervals, Date.now());
|
|
26
|
+
if (remaining <= 0)
|
|
27
|
+
throw new AgentOpsError("RUN_BUDGET_EXHAUSTED", "No active run time remains for local proof.");
|
|
28
|
+
const cli = fileURLToPath(new URL("./bin.js", import.meta.url));
|
|
29
|
+
const child = spawn(process.execPath, [fileURLToPath(new URL("./run-step-entry.js", import.meta.url)), cli, ...argv], { cwd: worker.worktree, env: { ...process.env, AGENT_OPS_RUN_ID: state.runId, AGENT_OPS_WORKER_ID: worker.workerId,
|
|
30
|
+
AGENT_OPS_WORKER_GENERATION: String(worker.generation) }, detached: process.platform !== "win32", stdio: ["pipe", "inherit", "inherit"] });
|
|
31
|
+
const completion = new Promise((resolve, reject) => { child.once("error", reject); child.once("close", code => resolve(code ?? 1)); });
|
|
32
|
+
const kill = () => { if (child.pid !== undefined)
|
|
33
|
+
try {
|
|
34
|
+
process.kill(process.platform === "win32" ? child.pid : -child.pid, "SIGKILL");
|
|
35
|
+
}
|
|
36
|
+
catch { } };
|
|
37
|
+
const deadline = setTimeout(kill, remaining);
|
|
38
|
+
try {
|
|
39
|
+
const identity = child.pid === undefined ? null : await nativeProcessIdentity(child.pid);
|
|
40
|
+
if (identity === null)
|
|
41
|
+
throw new AgentOpsError("RUN_PROCESS_IDENTITY_REQUIRED", "Cannot register an unidentified local proof process.");
|
|
42
|
+
await repository.mutate(state.runId, current => {
|
|
43
|
+
const saved = current.workers.find(w => w.workerId === worker.workerId);
|
|
44
|
+
if (current.status !== "active" || current.disableRestart || current.awaitingResume || saved?.generation !== worker.generation ||
|
|
45
|
+
!["running", "idle"].includes(saved.status) || saved.stopIntent !== null ||
|
|
46
|
+
current.policyBinding?.artifactDigest !== state.policyBinding.artifactDigest ||
|
|
47
|
+
(saved.proofProcess != null && saved.proofProcess.processId !== worker.proofProcess?.processId))
|
|
48
|
+
throw new AgentOpsError("RUN_WORKER_STALE", "Run stop or another proof command raced registration.");
|
|
49
|
+
return { ...current, workers: current.workers.map(w => w.workerId === worker.workerId ? { ...w, proofProcess: { processId: child.pid, processIdentity: identity } } : w) };
|
|
50
|
+
});
|
|
51
|
+
child.stdin.on("error", kill);
|
|
52
|
+
child.stdin.end("start\n");
|
|
53
|
+
return await completion;
|
|
54
|
+
}
|
|
55
|
+
catch (cause) {
|
|
56
|
+
kill();
|
|
57
|
+
await completion.catch(() => { });
|
|
58
|
+
throw cause;
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
clearTimeout(deadline);
|
|
62
|
+
await repository.mutate(state.runId, current => ({ ...current, workers: current.workers.map(w => w.workerId === worker.workerId &&
|
|
63
|
+
w.generation === worker.generation && w.proofProcess?.processId === child.pid ? { ...w, proofProcess: null } : w) }));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { execFile, execFileSync } from "node:child_process";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { AgentOpsError } from "../../../runtime/src/fs/paths.js";
|
|
4
5
|
import { CompletionGateService } from "../../../runtime/src/hooks/completion-gate.js";
|
|
6
|
+
import { readWorktreeRecord } from "../../../runtime/src/parallel/service.js";
|
|
5
7
|
import { localStatePaths } from "../../../runtime/src/security/permissions.js";
|
|
6
8
|
import { FileTrustStore } from "../../../runtime/src/security/trust.js";
|
|
7
9
|
import { TaskService } from "../../../runtime/src/task/service.js";
|
|
8
10
|
import { FileTaskStore } from "../../../runtime/src/task/store.js";
|
|
9
11
|
import { FileEvidenceStore } from "../../../runtime/src/verify/evidence.js";
|
|
10
|
-
import {
|
|
11
|
-
import { loadEffectiveConfig, repositoryTrust, repositoryTrustBinding } from "./context.js";
|
|
12
|
+
import { registeredRunProofRunner } from "../../../runtime/src/verify/spawn.js";
|
|
13
|
+
import { loadEffectiveConfig, repositoryTrust, runPolicyContext, repositoryTrustBinding } from "./context.js";
|
|
12
14
|
import { CLI_VERSION } from "./version.js";
|
|
13
15
|
// Shared by the CLI and the hook entry point: Claude Code runs hooks through
|
|
14
16
|
// hook-entry.js, never bin.js, and its worktree guard needs these too.
|
|
@@ -58,19 +60,30 @@ export function worktreeDependencies() {
|
|
|
58
60
|
loadConfig: async (root) => (await loadEffectiveConfig(root, "project")).config,
|
|
59
61
|
trust: {
|
|
60
62
|
status: async (root, config) => await repositoryTrust(root, config, CLI_VERSION),
|
|
61
|
-
grant: async (root, config) =>
|
|
63
|
+
grant: async (root, config) => {
|
|
64
|
+
if (await runPolicyContext(root) !== null) {
|
|
65
|
+
if (await repositoryTrust(root, config, CLI_VERSION) !== "TRUSTED")
|
|
66
|
+
throw new AgentOpsError("RUN_POLICY_UNTRUSTED", "Run policy needs a current scoped authorization.");
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
await trustStore().grant(await repositoryTrustBinding(root, config, CLI_VERSION));
|
|
70
|
+
},
|
|
62
71
|
revoke: async (root, config) => {
|
|
72
|
+
const record = await readWorktreeRecord(root);
|
|
73
|
+
if (record?.runId !== undefined || process.env.AGENT_OPS_RUN_ID !== undefined)
|
|
74
|
+
return;
|
|
63
75
|
await trustStore().revoke(await repositoryTrustBinding(root, config, CLI_VERSION));
|
|
64
76
|
}
|
|
65
77
|
},
|
|
66
78
|
gate: gateFor,
|
|
67
|
-
tasks: (root, base) => new TaskService(new FileTaskStore(join(root, ".agent-ops", "tasks", "state.json"), root), base === undefined ? {} : { completion: {
|
|
79
|
+
tasks: (root, base, noChangePaths) => new TaskService(new FileTaskStore(join(root, ".agent-ops", "tasks", "state.json"), root), base === undefined ? {} : { completion: {
|
|
68
80
|
root,
|
|
69
81
|
gitRunner: gitRunner(root),
|
|
70
82
|
base,
|
|
83
|
+
...(noChangePaths === undefined ? {} : { noChangePaths }),
|
|
71
84
|
loadConfig: async () => (await loadEffectiveConfig(root, "project")).config
|
|
72
85
|
} }),
|
|
73
|
-
processRunner:
|
|
86
|
+
processRunner: registeredRunProofRunner(),
|
|
74
87
|
runSetup: async (cwd, step) => await new Promise((resolve) => {
|
|
75
88
|
execFile(step.command, [...step.args], {
|
|
76
89
|
cwd,
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { execFile, spawn } from "node:child_process";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
import { mkdir, stat } from "node:fs/promises";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
8
|
+
import { AgentOpsError } from "../../../runtime/src/fs/paths.js";
|
|
9
|
+
import { sha256 } from "../../../runtime/src/fs/hash.js";
|
|
10
|
+
import { resolveCheckouts, sessionWorktreeName } from "../../../runtime/src/parallel/service.js";
|
|
11
|
+
import { createLaunchdDescriptor, LaunchdController, readBootIdentity, readGuiLoginIdentity } from "../../../runtime/src/run/macOS.js";
|
|
12
|
+
import { FileRunRepository, RunService } from "../../../runtime/src/run/service.js";
|
|
13
|
+
import { readPrivateFile, writePrivateFile } from "../../../runtime/src/security/permissions.js";
|
|
14
|
+
import { collectChangeSurface } from "../../../runtime/src/verify/change-surface.js";
|
|
15
|
+
import { worktreeDependencies, gitRunner, trustStore } from "./parallel-deps.js";
|
|
16
|
+
import { NativeRunTransport } from "../../../runtime/src/run/transport.js";
|
|
17
|
+
import { ClaudeGoalHost } from "../../../runtime/src/run/hosts/claude.js";
|
|
18
|
+
import { CodexGoalHost } from "../../../runtime/src/run/hosts/codex.js";
|
|
19
|
+
import { RunSupervisor } from "../../../runtime/src/run/supervisor.js";
|
|
20
|
+
import { activeWallTimeMs } from "../../../runtime/src/run/scheduler.js";
|
|
21
|
+
import { bindRunPolicy, readRunPolicy, runRuntimeHash, runPolicyAssessment } from "../../../runtime/src/run/policy.js";
|
|
22
|
+
import { repositoryTrustBinding } from "./context.js";
|
|
23
|
+
import { CLI_VERSION } from "./version.js";
|
|
24
|
+
import { calculateConfigHash, canonicalJson } from "../../../runtime/src/config/hash.js";
|
|
25
|
+
import { discoverPendingPolicyTransition, renewBoundPolicyTransition, currentPolicyArtifact } from "../../../runtime/src/run/policy-transition.js";
|
|
26
|
+
import { nativeProcessIdentity } from "../../../runtime/src/run/transport.js";
|
|
27
|
+
import { redactSecrets } from "../../../runtime/src/security/redact.js";
|
|
28
|
+
import { RunControlService } from "../../../runtime/src/run/controls.js";
|
|
29
|
+
/** Worktree construction happens before a native lease exists; it must never write global trust. */
|
|
30
|
+
export async function runWorktreeDependencies(deps, state) {
|
|
31
|
+
const policy = await readRunPolicy(state, await runRuntimeHash());
|
|
32
|
+
if (policy === null || (await trustStore().status(policy.baseTrustBinding)).status !== "TRUSTED")
|
|
33
|
+
throw new AgentOpsError("RUN_REPO_UNTRUSTED", "Run worktree setup requires the original trusted repository and current scoped policy.");
|
|
34
|
+
const status = async (_root, config) => calculateConfigHash(config) === state.policyBinding.configHash ? "TRUSTED" : "STALE";
|
|
35
|
+
return { ...deps, loadConfig: async () => policy.config,
|
|
36
|
+
runSetup: async (root, step) => {
|
|
37
|
+
const index = (policy.config.worktree?.setup ?? []).findIndex(s => canonicalJson({ ...s, timeoutMs: s.timeoutMs ?? 600000 }) === canonicalJson(step));
|
|
38
|
+
if (index < 0)
|
|
39
|
+
throw new AgentOpsError("RUN_SETUP_UNAUTHORIZED", "Setup must select an exact fixed policy capability.");
|
|
40
|
+
const repository = new FileRunRepository(join(state.commonDir, "agent-ops/runs"), state.commonDir);
|
|
41
|
+
const current = (await repository.read(state.runId));
|
|
42
|
+
const coordinator = current.workers.find(w => w.workerId === current.coordinatorId);
|
|
43
|
+
const command = canonicalJson(step), resource = canonicalJson({ checkout: root, capabilityId: "setup:" + index });
|
|
44
|
+
const approved = await runPolicyAssessment(current, policy, "setup:" + index);
|
|
45
|
+
await new RunControlService(repository).recordAuthorization(state.runId, { authorizationId: "setup:" + index + ":" + sha256(resource) + ":" + current.policyBinding.artifactDigest,
|
|
46
|
+
workerId: coordinator.workerId, generation: coordinator.generation, nativeSessionId: coordinator.nativeSessionId, operation: "worktree-setup",
|
|
47
|
+
command, resource, commandDigest: sha256(command), resourceDigest: sha256(resource), reason: approved?.reason ?? "The original user trust authorizes this fixed repository setup capability in an isolated checkout.",
|
|
48
|
+
result: "auto-approved", source: approved === null ? "repo-trust" : "coordinator-policy-review", policyArtifactDigest: current.policyBinding.artifactDigest,
|
|
49
|
+
policyConfigHash: current.policyBinding.configHash, runtimeHash: current.policyBinding.runtimeHash, expiresAt: current.policyBinding.expiresAt });
|
|
50
|
+
const remaining = current.budget.limitMs - activeWallTimeMs(current.budget.activeIntervals, Date.now());
|
|
51
|
+
if (remaining <= 0)
|
|
52
|
+
throw new AgentOpsError("RUN_BUDGET_EXHAUSTED", "No active time remains for setup.");
|
|
53
|
+
const child = spawn(process.execPath, [fileURLToPath(new URL("./run-step-entry.js", import.meta.url)),
|
|
54
|
+
fileURLToPath(new URL("./run-setup-entry.js", import.meta.url)), state.commonDir, state.runId, root, String(index), state.policyBinding.artifactDigest], { cwd: root, env: process.env, detached: process.platform !== "win32", stdio: ["pipe", "pipe", "pipe"] });
|
|
55
|
+
let output = "";
|
|
56
|
+
const completion = new Promise((resolve, reject) => { child.once("error", reject); child.once("close", resolve); });
|
|
57
|
+
const kill = () => { if (child.pid !== undefined)
|
|
58
|
+
try {
|
|
59
|
+
process.kill(process.platform === "win32" ? child.pid : -child.pid, "SIGKILL");
|
|
60
|
+
}
|
|
61
|
+
catch { } };
|
|
62
|
+
child.stdout.on("data", chunk => { output += chunk.toString(); if (Buffer.byteLength(output) > 2 * 1024 * 1024)
|
|
63
|
+
kill(); });
|
|
64
|
+
child.stderr.resume();
|
|
65
|
+
const deadline = setTimeout(kill, Math.min(remaining, step.timeoutMs ?? 600000) + 3000);
|
|
66
|
+
try {
|
|
67
|
+
const identity = child.pid === undefined ? null : await nativeProcessIdentity(child.pid);
|
|
68
|
+
if (identity === null)
|
|
69
|
+
throw new AgentOpsError("RUN_PROCESS_IDENTITY_REQUIRED", "Cannot register setup process identity.");
|
|
70
|
+
await repository.mutate(state.runId, saved => {
|
|
71
|
+
if (saved.status !== "active" || saved.disableRestart || saved.proofProcess != null || saved.policyBinding?.artifactDigest !== state.policyBinding.artifactDigest)
|
|
72
|
+
throw new AgentOpsError("RUN_SETUP_STALE", "Stop or another proof process raced setup registration.");
|
|
73
|
+
return { ...saved, proofProcess: { processId: child.pid, processIdentity: identity } };
|
|
74
|
+
});
|
|
75
|
+
child.stdin.on("error", kill);
|
|
76
|
+
child.stdin.end("start\n");
|
|
77
|
+
const exitCode = await completion;
|
|
78
|
+
const result = exitCode === 0 ? JSON.parse(output) :
|
|
79
|
+
{ exitCode: exitCode ?? 1, output: "Registered setup interrupted, timed out or failed. " + redactSecrets(output) };
|
|
80
|
+
const artifact = canonicalJson({ runId: state.runId, command, resource, policyArtifactDigest: current.policyBinding.artifactDigest, result });
|
|
81
|
+
const digest = sha256(artifact);
|
|
82
|
+
await writePrivateFile(join(state.commonDir, "agent-ops/runs", state.runId, "setup", digest + ".json"), artifact, state.commonDir);
|
|
83
|
+
await repository.appendEvent(state.runId, { type: "diagnostic", code: result.exitCode === 0 ? "RUN_SETUP_PASSED" : "RUN_SETUP_FAILED", workerId: coordinator.workerId,
|
|
84
|
+
taskId: coordinator.taskId, detail: canonicalJson({ digest, capabilityId: "setup:" + index }) });
|
|
85
|
+
return { exitCode: result.exitCode, output: redactSecrets(result.output) };
|
|
86
|
+
}
|
|
87
|
+
catch (cause) {
|
|
88
|
+
kill();
|
|
89
|
+
await completion.catch(() => { });
|
|
90
|
+
throw cause;
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
clearTimeout(deadline);
|
|
94
|
+
await repository.mutate(state.runId, saved => ({ ...saved, ...(saved.proofProcess?.processId === child.pid ? { proofProcess: null } : {}) }));
|
|
95
|
+
}
|
|
96
|
+
}, trust: { status,
|
|
97
|
+
grant: async (root, config) => { if (await status(root, config) !== "TRUSTED")
|
|
98
|
+
throw new AgentOpsError("RUN_POLICY_UNTRUSTED", "Cannot grant an unassessed worktree policy."); },
|
|
99
|
+
revoke: async () => { } } };
|
|
100
|
+
}
|
|
101
|
+
/** Explicit recovery can renew an expired bound journal, including the bind-before-journal crash gap. */
|
|
102
|
+
export async function renewPendingRunPolicy(repository, runId, journal) {
|
|
103
|
+
if (journal.stages.at(-1).stage !== "bound")
|
|
104
|
+
return journal;
|
|
105
|
+
const now = Date.now(), runtimeHash = await runRuntimeHash();
|
|
106
|
+
let current = (await repository.read(runId));
|
|
107
|
+
const expected = journal.newPolicyArtifact;
|
|
108
|
+
const policy = (await readRunPolicy(current, runtimeHash, now, true));
|
|
109
|
+
const bound = (await readRunPolicy({ ...current, policyBinding: expected }, runtimeHash, now, true));
|
|
110
|
+
let digest = current.policyBinding.artifactDigest, ancestor = policy;
|
|
111
|
+
for (let depth = 0; digest !== expected.artifactDigest && depth < 128; depth++) {
|
|
112
|
+
if (calculateConfigHash(ancestor.config) !== expected.configHash || ancestor.runtimeHash !== expected.runtimeHash ||
|
|
113
|
+
canonicalJson(ancestor.baseTrustBinding) !== canonicalJson(bound.baseTrustBinding) || ancestor.previousDigest === null)
|
|
114
|
+
throw new AgentOpsError("RUN_POLICY_TRANSITION_STALE", "Pending renewal must match the exact immutable bound policy lineage.");
|
|
115
|
+
digest = ancestor.previousDigest;
|
|
116
|
+
const source = await readPrivateFile(join(current.commonDir, "agent-ops/runs", runId, "policies", digest + ".json"), current.commonDir);
|
|
117
|
+
if (source === null || sha256(source) !== digest)
|
|
118
|
+
throw new AgentOpsError("RUN_POLICY_CHANGED", "Historical renewal artifact is missing or changed.");
|
|
119
|
+
const { expiresAt } = JSON.parse(source);
|
|
120
|
+
ancestor = (await readRunPolicy({ ...current, policyBinding: { ...expected, artifactDigest: digest, expiresAt } }, runtimeHash, now, true));
|
|
121
|
+
}
|
|
122
|
+
if (digest !== expected.artifactDigest)
|
|
123
|
+
throw new AgentOpsError("RUN_POLICY_TRANSITION_STALE", "Pending renewal lineage exceeds its recovery bound.");
|
|
124
|
+
if (Date.parse(current.policyBinding.expiresAt) <= now) {
|
|
125
|
+
const lastResume = Math.max(0, ...current.controls.filter(c => c.action === "resume" && c.actor === "user").map(c => Date.parse(c.requestedAt)));
|
|
126
|
+
if (lastResume < Date.parse(journal.stages.at(-1).at))
|
|
127
|
+
throw new AgentOpsError("RUN_POLICY_RECOVERY_REQUIRED", "Expired transition execution requires explicit resume.");
|
|
128
|
+
const previousDigest = current.policyBinding.artifactDigest;
|
|
129
|
+
current = await bindRunPolicy(repository, runId, { config: policy.config, runtimeHash: policy.runtimeHash,
|
|
130
|
+
baseTrustBinding: policy.baseTrustBinding, expectedDigest: previousDigest, now });
|
|
131
|
+
const fresh = (await readRunPolicy(current, runtimeHash, now));
|
|
132
|
+
if (fresh.previousDigest !== previousDigest)
|
|
133
|
+
throw new AgentOpsError("RUN_POLICY_TRANSITION_STALE", "Renewal changed the immutable policy lineage.");
|
|
134
|
+
}
|
|
135
|
+
else if (current.policyBinding.artifactDigest === expected.artifactDigest)
|
|
136
|
+
return journal;
|
|
137
|
+
return await renewBoundPolicyTransition(repository, runId, journal.transitionId, {
|
|
138
|
+
renewedPolicyArtifact: currentPolicyArtifact(current), expectedJournalDigest: journal.artifactDigest, now: new Date(now).toISOString()
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
export const runEntry = fileURLToPath(new URL("./run-entry.js", import.meta.url));
|
|
142
|
+
export function runDescriptor(state) {
|
|
143
|
+
return createLaunchdDescriptor({ runId: state.runId, workerId: "supervisor", privateDirectory: join(state.commonDir, "agent-ops", "runs", state.runId),
|
|
144
|
+
command: process.execPath, args: [runEntry, state.root, state.runId], cwd: state.root, pathEnvironment: process.env.PATH });
|
|
145
|
+
}
|
|
146
|
+
export async function productionRunContext(cwd, options = {}) {
|
|
147
|
+
const deps = worktreeDependencies();
|
|
148
|
+
const checkouts = await resolveCheckouts(deps, cwd);
|
|
149
|
+
const target = await deps.git(checkouts.mainRoot, ["symbolic-ref", "--short", "HEAD"]);
|
|
150
|
+
if (target.exitCode !== 0)
|
|
151
|
+
throw new AgentOpsError("RUN_TARGET_REQUIRED", "Run requires a checked-out target branch.");
|
|
152
|
+
const directory = join(checkouts.commonDir, "agent-ops", "runs");
|
|
153
|
+
const repository = new FileRunRepository(directory, checkouts.commonDir);
|
|
154
|
+
const service = new RunService(repository);
|
|
155
|
+
const launchd = options.launchd ?? new LaunchdController();
|
|
156
|
+
const start = async (input) => {
|
|
157
|
+
if (!launchd.supported)
|
|
158
|
+
throw new AgentOpsError("RUN_BACKGROUND_UNSUPPORTED", "Background native runs currently require macOS.");
|
|
159
|
+
if (checkouts.currentRoot !== checkouts.mainRoot)
|
|
160
|
+
throw new AgentOpsError("WORKTREE_NESTED", "Start a run from the main checkout.");
|
|
161
|
+
if ((await collectChangeSurface(gitRunner(checkouts.mainRoot))).paths.length > 0)
|
|
162
|
+
throw new AgentOpsError("RUN_TARGET_DIRTY", "Commit or preserve the target's current edits before starting an isolated run.");
|
|
163
|
+
const config = await deps.loadConfig(checkouts.mainRoot);
|
|
164
|
+
if (await deps.trust.status(checkouts.mainRoot, config) !== "TRUSTED")
|
|
165
|
+
throw new AgentOpsError("RUN_REPO_UNTRUSTED", "The run requires current repository verifier trust.");
|
|
166
|
+
const ownerSessionId = randomUUID();
|
|
167
|
+
const result = await service.start({ ...input, root: checkouts.mainRoot, commonDir: checkouts.commonDir,
|
|
168
|
+
targetBranch: target.stdout.trim(), ownerSessionId, bootIdentity: await readBootIdentity(), loginDomain: await readGuiLoginIdentity() });
|
|
169
|
+
try {
|
|
170
|
+
await bindRunPolicy(repository, result.state.runId, { config,
|
|
171
|
+
baseTrustBinding: await repositoryTrustBinding(checkouts.mainRoot, config, CLI_VERSION), runtimeHash: await runRuntimeHash() });
|
|
172
|
+
const workerId = `coordinator-${result.state.runId}`;
|
|
173
|
+
const workerRoot = join(checkouts.mainRoot, ".worktrees", sessionWorktreeName(ownerSessionId));
|
|
174
|
+
const now = new Date().toISOString();
|
|
175
|
+
const state = await repository.mutate(result.state.runId, current => ({ ...current,
|
|
176
|
+
currentContractHash: current.goalHash,
|
|
177
|
+
tasks: [{ taskId: "planning-" + current.runId, dependencies: [], status: "ready", workerId,
|
|
178
|
+
deliveryDigest: null, sourceCommit: null, blockedReason: null }],
|
|
179
|
+
workers: [{ workerId, taskId: "planning-" + current.runId, host: current.host, ownerSessionId,
|
|
180
|
+
nativeSessionId: null, nativeJobId: null, worktree: workerRoot, processId: null, processIdentity: null,
|
|
181
|
+
generation: 1, status: "assigned", leaseExpiresAt: null, heartbeatAt: null, stopIntent: null,
|
|
182
|
+
nativeGoalState: "inactive", lastFailure: null }],
|
|
183
|
+
budget: { ...current.budget, activeIntervals: [{ startMs: Date.parse(current.createdAt), endMs: null }], lastObservedAt: now } }));
|
|
184
|
+
await mkdir(runDescriptor(state).privateDirectory, { recursive: true });
|
|
185
|
+
await launchd.writeDescriptor(runDescriptor(state));
|
|
186
|
+
await launchd.bootstrap(runDescriptor(state));
|
|
187
|
+
return { state, message: `Run ${state.runId} started; background setup and native goal use ${workerRoot}.` };
|
|
188
|
+
}
|
|
189
|
+
catch (cause) {
|
|
190
|
+
await repository.mutate(result.state.runId, current => ({ ...current, status: "failed", disableRestart: true }));
|
|
191
|
+
throw cause;
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
const resume = async (runId) => {
|
|
195
|
+
const before = await service.status(runId);
|
|
196
|
+
const runtimeHash = await runRuntimeHash();
|
|
197
|
+
const policy = await readRunPolicy(before, before.policyBinding?.runtimeHash ?? runtimeHash, Date.now(), true);
|
|
198
|
+
if (policy !== null && await deps.trust.status(checkouts.mainRoot, policy.baseConfig) !== "TRUSTED")
|
|
199
|
+
throw new AgentOpsError("RUN_REPO_UNTRUSTED", "Resume requires the original repository trust binding to remain valid.");
|
|
200
|
+
const pending = await discoverPendingPolicyTransition(repository, runId);
|
|
201
|
+
if (pending !== null && runtimeHash !== before.policyBinding?.runtimeHash)
|
|
202
|
+
throw new AgentOpsError("RUN_POLICY_RECOVERY_REQUIRED", "Recover the pending policy transition with its original executable runtime before upgrading it.");
|
|
203
|
+
const version = before.integration !== null ? before.nativeInstance : (await promisify(execFile)(before.host, ["--version"], { timeout: 5000, maxBuffer: 8192 })).stdout.trim();
|
|
204
|
+
const transport = new NativeRunTransport(before.host === "codex" ? new CodexGoalHost() : new ClaudeGoalHost(), repository);
|
|
205
|
+
const supervisor = new RunSupervisor({ repository, host: transport });
|
|
206
|
+
for (const worker of before.workers)
|
|
207
|
+
await supervisor.stopWorker(runId, worker.workerId, worker.generation, "stop");
|
|
208
|
+
if (before.proofProcess != null) {
|
|
209
|
+
await transport.stop({ nativeSessionId: "run-proof", nativeJobId: null, ...before.proofProcess, instance: before.nativeInstance, generation: 1, reason: "explicit resume reconciliation" });
|
|
210
|
+
await repository.mutate(runId, s => ({ ...s, proofProcess: null }));
|
|
211
|
+
}
|
|
212
|
+
if (before.integration === null)
|
|
213
|
+
for (const worker of before.workers.filter(w => w.worktree !== null)) {
|
|
214
|
+
const exists = await stat(worker.worktree).catch((cause) => { if (cause.code === "ENOENT")
|
|
215
|
+
return null; throw cause; });
|
|
216
|
+
if (exists === null && before.rootTaskId === null && worker.workerId === before.coordinatorId)
|
|
217
|
+
continue;
|
|
218
|
+
if (exists === null || (await collectChangeSurface(gitRunner(worker.worktree))).paths.length > 0)
|
|
219
|
+
throw new AgentOpsError("RUN_RECOVERY_DIRTY", "Commit or preserve every writer checkout before explicit resume.");
|
|
220
|
+
}
|
|
221
|
+
const result = await service.resume(runId);
|
|
222
|
+
try {
|
|
223
|
+
if (pending !== null)
|
|
224
|
+
await renewPendingRunPolicy(repository, runId, pending);
|
|
225
|
+
if (policy !== null && pending === null)
|
|
226
|
+
await bindRunPolicy(repository, runId, { config: policy.config, runtimeHash, resumeRuntime: true,
|
|
227
|
+
baseTrustBinding: policy.baseTrustBinding, expectedDigest: before.policyBinding.artifactDigest });
|
|
228
|
+
const login = await readGuiLoginIdentity();
|
|
229
|
+
const state = await repository.mutate(runId, current => ({ ...current,
|
|
230
|
+
bootIdentity: awaitBoot, loginDomain: login, nativeInstance: version }));
|
|
231
|
+
await launchd.enableRestart(runDescriptor(state));
|
|
232
|
+
await launchd.wake(runDescriptor(state));
|
|
233
|
+
return { ...result, state };
|
|
234
|
+
}
|
|
235
|
+
catch (cause) {
|
|
236
|
+
await repository.mutate(runId, current => ({ ...current, status: "paused", awaitingResume: true, disableRestart: true }));
|
|
237
|
+
throw new AgentOpsError("RUN_SUPERVISOR_RESUME_FAILED", "Supervisor could not be resumed; inspect run status and launchd diagnostics.", { cause });
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
const awaitBoot = await readBootIdentity();
|
|
241
|
+
const stop = async (runId, reason) => {
|
|
242
|
+
const result = await service.stop(runId, reason);
|
|
243
|
+
// Disable native/background continuation before asking the active supervisor to stop writers.
|
|
244
|
+
let disableError;
|
|
245
|
+
try {
|
|
246
|
+
await launchd.disableRestart(runDescriptor(result.state), reason ?? "user requested stop");
|
|
247
|
+
}
|
|
248
|
+
catch (cause) {
|
|
249
|
+
disableError = cause;
|
|
250
|
+
}
|
|
251
|
+
// The control command also reconciles saved process identities: a crashed
|
|
252
|
+
// supervisor must not leave an owned native group alive after user Stop.
|
|
253
|
+
const transport = new NativeRunTransport(result.state.host === "codex" ? new CodexGoalHost() : new ClaudeGoalHost(), repository);
|
|
254
|
+
const supervisor = new RunSupervisor({ repository, host: transport });
|
|
255
|
+
for (const worker of result.state.workers) {
|
|
256
|
+
try {
|
|
257
|
+
await supervisor.stopWorker(runId, worker.workerId, worker.generation, "stop");
|
|
258
|
+
}
|
|
259
|
+
catch (cause) {
|
|
260
|
+
const current = (await repository.read(runId));
|
|
261
|
+
const saved = current.workers.find(w => w.workerId === worker.workerId);
|
|
262
|
+
if (saved?.generation !== worker.generation || saved.stopIntent?.confirmedDeadAt == null)
|
|
263
|
+
throw cause;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const proof = (await repository.read(runId)).proofProcess;
|
|
267
|
+
if (proof != null) {
|
|
268
|
+
await transport.stop({ nativeSessionId: "run-proof", nativeJobId: null, ...proof,
|
|
269
|
+
instance: result.state.nativeInstance, generation: 1, reason: "user requested stop" });
|
|
270
|
+
await repository.mutate(runId, current => ({ ...current,
|
|
271
|
+
...(current.proofProcess?.processId === proof.processId ? { proofProcess: null } : {}) }));
|
|
272
|
+
}
|
|
273
|
+
try {
|
|
274
|
+
await launchd.bootout(runDescriptor(result.state));
|
|
275
|
+
}
|
|
276
|
+
catch (cause) {
|
|
277
|
+
disableError ??= cause;
|
|
278
|
+
}
|
|
279
|
+
const state = await repository.mutate(runId, current => ({ ...current, status: "paused", budget: { ...current.budget,
|
|
280
|
+
accumulatedMs: activeWallTimeMs(current.budget.activeIntervals, Date.now()), lastObservedAt: new Date().toISOString(),
|
|
281
|
+
activeIntervals: current.budget.activeIntervals.map(interval => ({ ...interval, endMs: interval.endMs ?? Date.now() })) } }));
|
|
282
|
+
if (disableError !== undefined) {
|
|
283
|
+
await repository.appendEvent(runId, { type: "diagnostic", code: "RUN_SUPERVISOR_DISABLE_FAILED", workerId: null, taskId: null,
|
|
284
|
+
detail: "Registered process death was confirmed; launchd disable failed. Ledger restart intent remains disabled." });
|
|
285
|
+
throw new AgentOpsError("RUN_SUPERVISOR_DISABLE_FAILED", "Owned executors stopped, but launchd disable failed; inspect background diagnostics.", { cause: disableError });
|
|
286
|
+
}
|
|
287
|
+
return { state, message: `Run ${runId} stopped; registered native process death was confirmed.` };
|
|
288
|
+
};
|
|
289
|
+
const respond = async (runId, questionId, answer) => {
|
|
290
|
+
const state = await service.status(runId);
|
|
291
|
+
if (!state.questions.some(q => q.questionId === questionId && q.answeredAt === null))
|
|
292
|
+
throw new AgentOpsError("RUN_QUESTION_NOT_FOUND", "An unanswered question is required.");
|
|
293
|
+
await writePrivateFile(join(state.commonDir, "agent-ops", "runs", state.runId, "answers", sha256(questionId) + ".json"), JSON.stringify({ questionId, answer: answer.trim() }), state.commonDir);
|
|
294
|
+
const result = await service.respond(runId, questionId, answer);
|
|
295
|
+
try {
|
|
296
|
+
const resumed = await resume(runId);
|
|
297
|
+
return { ...result, state: resumed.state };
|
|
298
|
+
}
|
|
299
|
+
catch (cause) {
|
|
300
|
+
await repository.mutate(runId, saved => ({ ...saved, status: "paused", awaitingResume: true, disableRestart: true }));
|
|
301
|
+
throw cause;
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
return { ...checkouts, targetBranch: target.stdout.trim(), ownerSessionId: randomUUID(), repository,
|
|
305
|
+
service: { start, resume, stop, respond, status: service.status.bind(service), logs: service.logs.bind(service) },
|
|
306
|
+
waitForCompletion: async (runId) => {
|
|
307
|
+
for (;;) {
|
|
308
|
+
const state = await service.status(runId);
|
|
309
|
+
if (!["active", "stopping"].includes(state.status))
|
|
310
|
+
return state;
|
|
311
|
+
await delay(1000);
|
|
312
|
+
}
|
|
313
|
+
} };
|
|
314
|
+
}
|