@fitlab-ai/agent-infra 0.9.10 → 0.9.11
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 +13 -3
- package/README.zh-CN.md +13 -3
- package/bin/cli.ts +17 -4
- package/dist/bin/cli.js +19 -4
- package/dist/bin/internal-cli.js +50 -4
- package/dist/lib/agent-client.js +0 -1
- package/dist/lib/agent-clients/adapters/opencode-sandbox.js +0 -4
- package/dist/lib/agent-clients/adapters/traecli.js +7 -8
- package/dist/lib/agent-clients/config.js +14 -99
- package/dist/lib/agent-clients/reconcile.js +1 -11
- package/dist/lib/agent-clients/schema.js +5 -4
- package/dist/lib/agent-clients/tokens.js +5 -15
- package/dist/lib/decide.js +14 -30
- package/dist/lib/defaults.json +4 -0
- package/dist/lib/init.js +2 -2
- package/dist/lib/internal/release-workflow.js +0 -1
- package/dist/lib/internal/task-finalization.js +19 -9
- package/dist/lib/internal/task-ledger.js +8 -2
- package/dist/lib/internal/task-lifecycle.js +17 -93
- package/dist/lib/internal/task-orchestration.js +30 -216
- package/dist/lib/internal/task-validate.js +2 -1
- package/dist/lib/merge.js +205 -103
- package/dist/lib/process-data/normalize.js +2 -9
- package/dist/lib/run/host.js +46 -16
- package/dist/lib/sandbox/agent-client-reconciler.js +4 -12
- package/dist/lib/sandbox/commands/create.js +3 -3
- package/dist/lib/sandbox/commands/enter.js +18 -4
- package/dist/lib/sandbox/config.js +3 -12
- package/dist/lib/sandbox/control/executor.js +131 -39
- package/dist/lib/sandbox/control/lifecycle.js +29 -20
- package/dist/lib/sandbox/control/server.js +18 -16
- package/dist/lib/sandbox/mounts.js +3 -2
- package/dist/lib/sandbox/recovery.js +123 -22
- package/dist/lib/sandbox/workspace-identity.js +14 -0
- package/dist/lib/sandbox/workspace-view.js +8 -9
- package/dist/lib/self-update.js +278 -0
- package/dist/lib/server/protocol.js +5 -5
- package/dist/lib/task/activity-log.js +4 -1
- package/dist/lib/task/artifact-lifecycle.js +4 -2
- package/dist/lib/task/codex-orchestration.js +1 -1
- package/dist/lib/task/command-options.js +1 -3
- package/dist/lib/task/commands/cat.js +0 -5
- package/dist/lib/task/commands/decisions.js +76 -92
- package/dist/lib/task/commands/files.js +6 -6
- package/dist/lib/task/commands/grep.js +25 -61
- package/dist/lib/task/commands/issue-body.js +6 -6
- package/dist/lib/task/commands/log.js +25 -10
- package/dist/lib/task/commands/show.js +5 -6
- package/dist/lib/task/commands/status.js +8 -8
- package/dist/lib/task/commit-operation.js +21 -25
- package/dist/lib/task/commit-policy.js +1 -1
- package/dist/lib/task/control-authority.js +431 -0
- package/dist/lib/task/create.js +6 -1
- package/dist/lib/task/current-contract.js +35 -0
- package/dist/lib/task/decision-details.js +263 -0
- package/dist/lib/task/decision-intents.js +5 -2
- package/dist/lib/task/events.js +11 -5
- package/dist/lib/task/index.js +19 -20
- package/dist/lib/task/ledger-intents.js +8 -9
- package/dist/lib/task/ledger.js +13 -6
- package/dist/lib/task/lifecycle.js +16 -0
- package/dist/lib/task/orchestration.js +50 -13
- package/dist/lib/task/review-artifacts.js +31 -12
- package/dist/lib/task/review-finalization.js +19 -6
- package/dist/lib/task/verification-engine.js +38 -15
- package/dist/lib/task/verification-review.js +1 -1
- package/dist/lib/task/workspace-snapshot.js +5 -11
- package/dist/lib/task/write.js +11 -1
- package/dist/lib/update.js +6 -37
- package/dist/lib/version.js +5 -1
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-client.ts +0 -1
- package/lib/agent-clients/adapters/opencode-sandbox.ts +0 -5
- package/lib/agent-clients/adapters/traecli.ts +7 -8
- package/lib/agent-clients/config.ts +14 -144
- package/lib/agent-clients/reconcile.ts +1 -12
- package/lib/agent-clients/schema.ts +5 -4
- package/lib/agent-clients/tokens.ts +5 -18
- package/lib/decide.ts +12 -22
- package/lib/defaults.json +4 -0
- package/lib/init.ts +2 -2
- package/lib/internal/release-workflow.ts +0 -1
- package/lib/internal/task-finalization.ts +27 -13
- package/lib/internal/task-ledger.ts +8 -2
- package/lib/internal/task-lifecycle.ts +33 -76
- package/lib/internal/task-orchestration.ts +33 -190
- package/lib/internal/task-validate.ts +3 -2
- package/lib/merge.ts +236 -120
- package/lib/process-data/normalize.ts +2 -8
- package/lib/run/host.ts +63 -14
- package/lib/sandbox/agent-client-reconciler.ts +5 -15
- package/lib/sandbox/commands/create.ts +4 -4
- package/lib/sandbox/commands/enter.ts +24 -4
- package/lib/sandbox/config.ts +2 -14
- package/lib/sandbox/control/container-identity.ts +2 -2
- package/lib/sandbox/control/executor.ts +148 -51
- package/lib/sandbox/control/lifecycle.ts +34 -28
- package/lib/sandbox/control/protocol.ts +1 -3
- package/lib/sandbox/control/server.ts +15 -12
- package/lib/sandbox/control/state.ts +7 -7
- package/lib/sandbox/mounts.ts +4 -3
- package/lib/sandbox/recovery.ts +162 -34
- package/lib/sandbox/tools.ts +1 -1
- package/lib/sandbox/workspace-identity.ts +28 -0
- package/lib/sandbox/workspace-view.ts +14 -10
- package/lib/self-update.ts +341 -0
- package/lib/server/protocol.ts +5 -5
- package/lib/task/activity-log.ts +6 -1
- package/lib/task/artifact-lifecycle.ts +5 -3
- package/lib/task/codex-orchestration.ts +2 -1
- package/lib/task/command-options.ts +1 -2
- package/lib/task/commands/cat.ts +0 -4
- package/lib/task/commands/decisions.ts +80 -75
- package/lib/task/commands/files.ts +6 -6
- package/lib/task/commands/grep.ts +23 -51
- package/lib/task/commands/issue-body.ts +6 -6
- package/lib/task/commands/log.ts +22 -10
- package/lib/task/commands/show.ts +5 -6
- package/lib/task/commands/status.ts +8 -8
- package/lib/task/commit-operation.ts +25 -30
- package/lib/task/commit-policy.ts +1 -2
- package/lib/task/control-authority.ts +563 -0
- package/lib/task/create.ts +6 -1
- package/lib/task/current-contract.ts +49 -0
- package/lib/task/decision-details.ts +340 -0
- package/lib/task/decision-intents.ts +7 -2
- package/lib/task/events.ts +10 -5
- package/lib/task/index.ts +19 -20
- package/lib/task/ledger-intents.ts +6 -8
- package/lib/task/ledger.ts +17 -7
- package/lib/task/lifecycle.ts +20 -0
- package/lib/task/orchestration.ts +40 -15
- package/lib/task/review-artifacts.ts +53 -15
- package/lib/task/review-finalization.ts +33 -7
- package/lib/task/verification-engine.ts +33 -18
- package/lib/task/verification-review.ts +1 -1
- package/lib/task/workspace-snapshot.ts +4 -13
- package/lib/task/write.ts +20 -1
- package/lib/update.ts +6 -40
- package/lib/version.ts +6 -1
- package/package.json +1 -1
- package/templates/.agents/README.en.md +5 -3
- package/templates/.agents/README.zh-CN.md +5 -3
- package/templates/.agents/rules/compatibility-policy.en.md +5 -4
- package/templates/.agents/rules/compatibility-policy.zh-CN.md +5 -4
- package/templates/.agents/rules/create-issue.github.en.md +1 -1
- package/templates/.agents/rules/create-issue.github.zh-CN.md +1 -1
- package/templates/.agents/rules/human-decision-context.en.md +9 -7
- package/templates/.agents/rules/human-decision-context.zh-CN.md +9 -7
- package/templates/.agents/rules/issue-fields.github.en.md +1 -1
- package/templates/.agents/rules/issue-fields.github.zh-CN.md +1 -1
- package/templates/.agents/rules/issue-pr-commands.en.md +1 -1
- package/templates/.agents/rules/issue-pr-commands.github.en.md +1 -1
- package/templates/.agents/rules/issue-pr-commands.github.zh-CN.md +1 -1
- package/templates/.agents/rules/issue-pr-commands.zh-CN.md +1 -1
- package/templates/.agents/rules/issue-sync.github.en.md +1 -1
- package/templates/.agents/rules/issue-sync.github.zh-CN.md +2 -2
- package/templates/.agents/rules/milestone-inference.github.en.md +1 -1
- package/templates/.agents/rules/milestone-inference.github.zh-CN.md +1 -1
- package/templates/.agents/rules/next-step-output.en.md +3 -3
- package/templates/.agents/rules/next-step-output.zh-CN.md +3 -3
- package/templates/.agents/rules/no-mid-flow-questions.en.md +1 -1
- package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +1 -1
- package/templates/.agents/rules/pr-sync.en.md +1 -1
- package/templates/.agents/rules/pr-sync.github.en.md +1 -1
- package/templates/.agents/rules/pr-sync.github.zh-CN.md +1 -1
- package/templates/.agents/rules/pr-sync.zh-CN.md +1 -1
- package/templates/.agents/rules/release-commands.github.en.md +6 -0
- package/templates/.agents/rules/release-commands.github.zh-CN.md +6 -0
- package/templates/.agents/rules/review-handshake.en.md +1 -1
- package/templates/.agents/rules/review-handshake.zh-CN.md +1 -1
- package/templates/.agents/rules/task-management.en.md +1 -2
- package/templates/.agents/rules/task-management.zh-CN.md +1 -2
- package/templates/.agents/skills/analyze-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/block-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/cancel-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/check-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/check-task/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/close-codescan/SKILL.en.md +1 -1
- package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/close-dependabot/SKILL.en.md +1 -1
- package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/code-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/commit/SKILL.en.md +27 -7
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +3 -4
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +5 -5
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +5 -5
- package/templates/.agents/skills/complete-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/create-pr/SKILL.en.md +5 -7
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +5 -7
- package/templates/.agents/skills/create-pr/reference/change-report.en.md +9 -4
- package/templates/.agents/skills/create-pr/reference/change-report.zh-CN.md +9 -4
- package/templates/.agents/skills/create-pr/scripts/change-report.mjs +203 -0
- package/templates/.agents/skills/create-release-note/SKILL.en.md +5 -6
- package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +5 -6
- package/templates/.agents/skills/create-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/import-issue/SKILL.en.md +1 -1
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/plan-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/restore-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/review-analysis/SKILL.en.md +5 -3
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +5 -3
- package/templates/.agents/skills/review-analysis/config/verify.en.json +1 -0
- package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -0
- package/templates/.agents/skills/review-code/SKILL.en.md +5 -3
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +5 -3
- package/templates/.agents/skills/review-code/config/verify.en.json +1 -0
- package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -0
- package/templates/.agents/skills/review-code/reference/migration-risks.en.md +2 -1
- package/templates/.agents/skills/review-code/reference/migration-risks.zh-CN.md +2 -1
- package/templates/.agents/skills/review-plan/SKILL.en.md +5 -3
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +5 -3
- package/templates/.agents/skills/review-plan/config/verify.en.json +1 -0
- package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -0
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +4 -0
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +4 -0
- package/templates/.agents/skills/run-task/SKILL.en.md +4 -0
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +4 -0
- package/templates/.agents/skills/update-agent-infra/SKILL.en.md +2 -2
- package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +140 -109
- package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -2
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +2 -2
- package/templates/.agents/templates/task.en.md +1 -1
- package/templates/.agents/templates/task.zh-CN.md +1 -1
- package/templates/.claude/commands/analyze-task.en.md +1 -1
- package/templates/.claude/commands/analyze-task.zh-CN.md +1 -1
- package/templates/.claude/commands/block-task.en.md +1 -1
- package/templates/.claude/commands/block-task.zh-CN.md +1 -1
- package/templates/.claude/commands/cancel-task.en.md +1 -1
- package/templates/.claude/commands/cancel-task.zh-CN.md +1 -1
- package/templates/.claude/commands/check-task.en.md +1 -1
- package/templates/.claude/commands/check-task.zh-CN.md +1 -1
- package/templates/.claude/commands/code-task.en.md +1 -1
- package/templates/.claude/commands/code-task.zh-CN.md +1 -1
- package/templates/.claude/commands/commit.en.md +1 -1
- package/templates/.claude/commands/commit.zh-CN.md +1 -1
- package/templates/.claude/commands/complete-manual-validation.en.md +1 -1
- package/templates/.claude/commands/complete-manual-validation.zh-CN.md +1 -1
- package/templates/.claude/commands/complete-task.en.md +1 -1
- package/templates/.claude/commands/complete-task.zh-CN.md +1 -1
- package/templates/.claude/commands/create-pr.en.md +1 -1
- package/templates/.claude/commands/create-pr.zh-CN.md +1 -1
- package/templates/.claude/commands/plan-task.en.md +1 -1
- package/templates/.claude/commands/plan-task.zh-CN.md +1 -1
- package/templates/.claude/commands/review-analysis.en.md +1 -1
- package/templates/.claude/commands/review-analysis.zh-CN.md +1 -1
- package/templates/.claude/commands/review-code.en.md +1 -1
- package/templates/.claude/commands/review-code.zh-CN.md +1 -1
- package/templates/.claude/commands/review-plan.en.md +1 -1
- package/templates/.claude/commands/review-plan.zh-CN.md +1 -1
- package/templates/.claude/commands/run-manual-validation.en.md +1 -1
- package/templates/.claude/commands/run-manual-validation.zh-CN.md +1 -1
- package/templates/.claude/commands/run-task.en.md +1 -1
- package/templates/.claude/commands/run-task.zh-CN.md +1 -1
- package/templates/.claude/commands/watch-pr.en.md +1 -1
- package/templates/.claude/commands/watch-pr.zh-CN.md +1 -1
- package/templates/{.trae/skills → .traecli/commands}/update-agent-infra.en.md +0 -1
- package/templates/{.trae/skills → .traecli/commands}/update-agent-infra.zh-CN.md +0 -1
- package/dist/lib/builtin-tuis.js +0 -40
- package/lib/builtin-tuis.ts +0 -56
|
@@ -1,15 +1,32 @@
|
|
|
1
|
-
import { spawn
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { randomUUID } from 'node:crypto';
|
|
5
5
|
import { getProcessStartTime } from '../../server/process-state.js';
|
|
6
6
|
import { createTask } from '../../task/create-service.js';
|
|
7
7
|
import { applyTaskFinalization } from '../../task/finalization.js';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
8
|
+
import { bindSandboxControlTask, validateSandboxControlRequest } from './protocol.js';
|
|
9
|
+
import { atomicWriteJson, executionPath, readActiveLease, terminateSandboxControlExecution } from './state.js';
|
|
10
|
+
import { createSandboxExecutorExecutionContext, dispatchTaskControlOperation, parseTaskControlOperation } from '../../task/control-authority.js';
|
|
11
|
+
import { assertSandboxControlBrokerOwner, readSandboxControlManifest } from './lifecycle.js';
|
|
11
12
|
import { computeLifecycleBuildIdentity } from '../../agent-clients/adapters/codex-lifecycle/build-identity.js';
|
|
12
13
|
import { closeCodexControllerRegistration, CodexControllerRegistrationError, openCodexControllerRegistration, resolveCodexControllerBinding } from './controller-registration.js';
|
|
14
|
+
function sameBrokerOwner(left, right) {
|
|
15
|
+
return left.version === right.version
|
|
16
|
+
&& left.pid === right.pid
|
|
17
|
+
&& left.startTime === right.startTime
|
|
18
|
+
&& left.brokerId === right.brokerId
|
|
19
|
+
&& left.token === right.token
|
|
20
|
+
&& left.generation === right.generation;
|
|
21
|
+
}
|
|
22
|
+
export function assertSandboxControlExecutorAuthority(manifest, gateOwner) {
|
|
23
|
+
const owner = assertSandboxControlBrokerOwner(manifest);
|
|
24
|
+
if (gateOwner && !sameBrokerOwner(owner, gateOwner)) {
|
|
25
|
+
throw new Error('SANDBOX_CONTROL_GATE_OWNER_MISMATCH');
|
|
26
|
+
}
|
|
27
|
+
if (readActiveLease(manifest))
|
|
28
|
+
throw new Error('SANDBOX_CONTROL_HANDOFF_ACTIVE');
|
|
29
|
+
}
|
|
13
30
|
function safeEnv(env) {
|
|
14
31
|
return Object.fromEntries(Object.entries(env).filter(([key]) => !key.toUpperCase().startsWith('AGENT_INFRA_CONTROL_')));
|
|
15
32
|
}
|
|
@@ -74,12 +91,15 @@ export async function prepareSandboxControlExecution(params) {
|
|
|
74
91
|
start(canWrite = () => true) {
|
|
75
92
|
if (!child.connected)
|
|
76
93
|
throw new Error('SANDBOX_CONTROL_EXECUTOR_GATE_CLOSED');
|
|
94
|
+
if (!canWrite())
|
|
95
|
+
throw new Error('SANDBOX_CONTROL_OWNER_LOST');
|
|
96
|
+
const gateOwner = assertSandboxControlBrokerOwner(params.manifest);
|
|
77
97
|
if (!canWrite())
|
|
78
98
|
throw new Error('SANDBOX_CONTROL_OWNER_LOST');
|
|
79
99
|
atomicWriteJson(executionPath(params.manifest, params.request.id), { ...execution, phase: 'running', updatedAt: Date.now() });
|
|
80
100
|
if (!canWrite())
|
|
81
101
|
throw new Error('SANDBOX_CONTROL_OWNER_LOST');
|
|
82
|
-
child.send({ version: 1, nonce });
|
|
102
|
+
child.send({ version: 1, nonce, owner: gateOwner });
|
|
83
103
|
},
|
|
84
104
|
completion,
|
|
85
105
|
terminate(updateState = true) {
|
|
@@ -104,11 +124,16 @@ function waitForGate(nonce, timeoutMs = 2_000) {
|
|
|
104
124
|
clearTimeout(timer);
|
|
105
125
|
process.off('disconnect', onDisconnect);
|
|
106
126
|
const value = message;
|
|
107
|
-
|
|
127
|
+
const owner = value?.owner;
|
|
128
|
+
if (!value || value.version !== 1 || value.nonce !== nonce
|
|
129
|
+
|| !owner || owner.version !== 3 || !Number.isSafeInteger(owner.pid) || (owner.pid ?? 0) <= 0
|
|
130
|
+
|| typeof owner.startTime !== 'number' || !Number.isSafeInteger(owner.startTime)
|
|
131
|
+
|| typeof owner.brokerId !== 'string' || owner.brokerId.length === 0
|
|
132
|
+
|| typeof owner.token !== 'string' || typeof owner.generation !== 'string') {
|
|
108
133
|
reject(new Error('SANDBOX_CONTROL_EXECUTOR_GATE_INVALID'));
|
|
109
134
|
return;
|
|
110
135
|
}
|
|
111
|
-
resolve();
|
|
136
|
+
resolve(owner);
|
|
112
137
|
});
|
|
113
138
|
});
|
|
114
139
|
}
|
|
@@ -139,6 +164,16 @@ function orchestrationFailure(code, message) {
|
|
|
139
164
|
stderr: ''
|
|
140
165
|
};
|
|
141
166
|
}
|
|
167
|
+
function lifecycleFailure(code, message) {
|
|
168
|
+
return {
|
|
169
|
+
exitCode: 1,
|
|
170
|
+
stdout: `${JSON.stringify({
|
|
171
|
+
status: 'failed', changed: false,
|
|
172
|
+
error: { code, message }
|
|
173
|
+
})}\n`,
|
|
174
|
+
stderr: ''
|
|
175
|
+
};
|
|
176
|
+
}
|
|
142
177
|
function finalizationResult(result) {
|
|
143
178
|
const payload = {
|
|
144
179
|
version: 1,
|
|
@@ -154,19 +189,7 @@ function finalizationResult(result) {
|
|
|
154
189
|
stderr: ''
|
|
155
190
|
};
|
|
156
191
|
}
|
|
157
|
-
function
|
|
158
|
-
if (args[1] !== 'prepare')
|
|
159
|
-
return false;
|
|
160
|
-
const values = [];
|
|
161
|
-
for (let index = 2; index < args.length; index += 1) {
|
|
162
|
-
if (args[index] === '--client')
|
|
163
|
-
values.push(args[index + 1] ?? '');
|
|
164
|
-
else if (args[index]?.startsWith('--client='))
|
|
165
|
-
values.push(args[index].slice('--client='.length));
|
|
166
|
-
}
|
|
167
|
-
return values.length === 1 && values[0] === 'codex';
|
|
168
|
-
}
|
|
169
|
-
export function executeRequest(manifest, manifestPath, request, options = {}) {
|
|
192
|
+
export async function executeRequest(manifest, manifestPath, request, options = {}) {
|
|
170
193
|
if (request.family === 'task-create') {
|
|
171
194
|
const result = createTask(request.candidate, { repoRoot: manifest.repoRoot });
|
|
172
195
|
return {
|
|
@@ -220,12 +243,41 @@ export function executeRequest(manifest, manifestPath, request, options = {}) {
|
|
|
220
243
|
}
|
|
221
244
|
}
|
|
222
245
|
if (request.family === 'task-finalization') {
|
|
223
|
-
|
|
246
|
+
const operation = parseTaskControlOperation('task-finalization', [manifest.taskId, 'complete', '--agent', request.agent]);
|
|
247
|
+
if (operation.family !== 'task-finalization')
|
|
248
|
+
throw new Error('SANDBOX_CONTROL_FINALIZATION_OPERATION_INVALID');
|
|
249
|
+
const result = dispatchTaskControlOperation(createSandboxExecutorExecutionContext({
|
|
250
|
+
repoRoot: manifest.repoRoot,
|
|
251
|
+
worktreeRoot: manifest.worktreeRoot,
|
|
252
|
+
runtimeDir: manifest.runtimeDir,
|
|
253
|
+
taskId: manifest.taskId,
|
|
254
|
+
generation: manifest.generation,
|
|
255
|
+
manifestPath,
|
|
256
|
+
requestId: request.id
|
|
257
|
+
}), operation);
|
|
258
|
+
return finalizationResult(result);
|
|
224
259
|
}
|
|
225
260
|
const boundArgs = bindSandboxControlTask(request, manifest.taskId);
|
|
261
|
+
if (request.family === 'task-orchestration') {
|
|
262
|
+
boundArgs.push('--git-worktree-root', manifest.worktreeRoot);
|
|
263
|
+
}
|
|
264
|
+
let operation;
|
|
265
|
+
try {
|
|
266
|
+
operation = parseTaskControlOperation(request.family, boundArgs);
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
270
|
+
const code = /^([A-Z][A-Z0-9_]+)/u.exec(message)?.[1] ?? 'TASK_CONTROL_OPERATION_INVALID';
|
|
271
|
+
return request.family === 'task-lifecycle'
|
|
272
|
+
? lifecycleFailure('LIFECYCLE_PAYLOAD_INVALID', message)
|
|
273
|
+
: orchestrationFailure(code, message);
|
|
274
|
+
}
|
|
226
275
|
let controllerBinding = null;
|
|
227
276
|
if (request.family === 'task-orchestration') {
|
|
228
|
-
|
|
277
|
+
const codexPrepare = operation.family === 'task-orchestration'
|
|
278
|
+
&& operation.intent === 'prepare'
|
|
279
|
+
&& operation.input.client === 'codex';
|
|
280
|
+
if (codexPrepare) {
|
|
229
281
|
if (!request.controllerProof) {
|
|
230
282
|
return orchestrationFailure('CODEX_SANDBOX_CONTROLLER_PROOF_REQUIRED', 'Codex prepare requires a current controller lease proof');
|
|
231
283
|
}
|
|
@@ -247,33 +299,73 @@ export function executeRequest(manifest, manifestPath, request, options = {}) {
|
|
|
247
299
|
else if (request.controllerProof !== null) {
|
|
248
300
|
return orchestrationFailure('CODEX_SANDBOX_CONTROLLER_PROOF_INVALID', 'Controller proof is only accepted for canonical Codex prepare');
|
|
249
301
|
}
|
|
250
|
-
boundArgs.push('--git-worktree-root', manifest.worktreeRoot);
|
|
251
302
|
}
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
}
|
|
303
|
+
const context = createSandboxExecutorExecutionContext({
|
|
304
|
+
repoRoot: manifest.repoRoot,
|
|
305
|
+
worktreeRoot: manifest.worktreeRoot,
|
|
306
|
+
runtimeDir: manifest.runtimeDir,
|
|
307
|
+
taskId: manifest.taskId,
|
|
308
|
+
generation: manifest.generation,
|
|
309
|
+
manifestPath,
|
|
310
|
+
requestId: request.id,
|
|
311
|
+
...(controllerBinding ? { controllerBinding } : {})
|
|
262
312
|
});
|
|
263
|
-
|
|
313
|
+
const format = (value) => {
|
|
314
|
+
const result = value;
|
|
315
|
+
return {
|
|
316
|
+
exitCode: result.status === 'failed' ? 1 : 0,
|
|
317
|
+
stdout: `${JSON.stringify(value)}\n`,
|
|
318
|
+
stderr: ''
|
|
319
|
+
};
|
|
320
|
+
};
|
|
321
|
+
if (request.family === 'task-orchestration') {
|
|
322
|
+
if (operation.family !== 'task-orchestration')
|
|
323
|
+
throw new Error('SANDBOX_CONTROL_ORCHESTRATION_OPERATION_INVALID');
|
|
324
|
+
return format(await dispatchTaskControlOperation(context, operation));
|
|
325
|
+
}
|
|
326
|
+
if (operation.family !== 'task-lifecycle')
|
|
327
|
+
throw new Error('SANDBOX_CONTROL_LIFECYCLE_OPERATION_INVALID');
|
|
328
|
+
return format(dispatchTaskControlOperation(context, operation));
|
|
264
329
|
}
|
|
265
330
|
export async function runSandboxControlExecutor(requestPath, nonce) {
|
|
266
|
-
await waitForGate(nonce);
|
|
331
|
+
const gateOwner = await waitForGate(nonce);
|
|
267
332
|
process.disconnect?.();
|
|
268
|
-
const root = fs.realpathSync.native(process.cwd());
|
|
269
|
-
const request = JSON.parse(fs.readFileSync(requestPath, 'utf8'));
|
|
270
333
|
const manifestPath = process.env.AGENT_INFRA_EXECUTOR_MANIFEST;
|
|
271
334
|
if (!manifestPath)
|
|
272
335
|
throw new Error('SANDBOX_CONTROL_EXECUTOR_MANIFEST_MISSING');
|
|
273
|
-
const manifest =
|
|
336
|
+
const manifest = readSandboxControlManifest(manifestPath);
|
|
337
|
+
const root = fs.realpathSync.native(process.cwd());
|
|
274
338
|
if (fs.realpathSync.native(manifest.repoRoot) !== root)
|
|
275
339
|
throw new Error('SANDBOX_CONTROL_EXECUTOR_ROOT_INVALID');
|
|
276
|
-
const
|
|
340
|
+
const raw = JSON.parse(fs.readFileSync(requestPath, 'utf8'));
|
|
341
|
+
const request = validateSandboxControlRequest(raw, manifest);
|
|
342
|
+
const requestDirectory = path.resolve(path.dirname(requestPath));
|
|
343
|
+
const processingDirectory = path.resolve(path.join(manifest.processingDir, request.id));
|
|
344
|
+
const channelRequestDirectory = path.resolve(path.join(manifest.channelDir, 'requests'));
|
|
345
|
+
if (requestDirectory !== processingDirectory && requestDirectory !== channelRequestDirectory) {
|
|
346
|
+
throw new Error('SANDBOX_CONTROL_EXECUTOR_REQUEST_INVALID');
|
|
347
|
+
}
|
|
348
|
+
let result;
|
|
349
|
+
try {
|
|
350
|
+
assertSandboxControlExecutorAuthority(manifest, gateOwner);
|
|
351
|
+
result = await executeRequest(manifest, manifestPath, request);
|
|
352
|
+
}
|
|
353
|
+
catch (error) {
|
|
354
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
355
|
+
const code = /^([A-Z][A-Z0-9_]+)/u.exec(detail)?.[1] ?? 'SANDBOX_CONTROL_EXECUTOR_FAILED';
|
|
356
|
+
result = request.family === 'task-finalization'
|
|
357
|
+
? {
|
|
358
|
+
exitCode: 1,
|
|
359
|
+
stdout: `${JSON.stringify({
|
|
360
|
+
version: 1, status: 'failed', changed: false, accepted: true, result: null,
|
|
361
|
+
error: { code, message: detail, retryable: false }
|
|
362
|
+
})}\n`,
|
|
363
|
+
stderr: ''
|
|
364
|
+
}
|
|
365
|
+
: request.family === 'task-lifecycle'
|
|
366
|
+
? lifecycleFailure(code, detail)
|
|
367
|
+
: orchestrationFailure(code, detail);
|
|
368
|
+
}
|
|
277
369
|
process.stdout.write(result.stdout);
|
|
278
370
|
process.stderr.write(result.stderr);
|
|
279
371
|
process.exitCode = result.exitCode;
|
|
@@ -414,8 +414,6 @@ function readProcessingSnapshot(root) {
|
|
|
414
414
|
export function captureSandboxControlCutoverSnapshot(root) {
|
|
415
415
|
const resolvedRoot = path.resolve(root);
|
|
416
416
|
const manifestPath = path.join(resolvedRoot, 'manifest.json');
|
|
417
|
-
if (fs.existsSync(manifestPath))
|
|
418
|
-
readSandboxControlManifestForTransition(manifestPath);
|
|
419
417
|
return {
|
|
420
418
|
root: resolvedRoot,
|
|
421
419
|
manifestRaw: readOptionalRaw(manifestPath),
|
|
@@ -430,7 +428,7 @@ export function assertSandboxControlCutoverSnapshot(snapshot) {
|
|
|
430
428
|
throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
|
|
431
429
|
}
|
|
432
430
|
}
|
|
433
|
-
function readSandboxControlManifestValue(manifestPath
|
|
431
|
+
function readSandboxControlManifestValue(manifestPath) {
|
|
434
432
|
if (!regularFile(manifestPath))
|
|
435
433
|
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
436
434
|
let manifest;
|
|
@@ -445,8 +443,14 @@ function readSandboxControlManifestValue(manifestPath, allowLegacy) {
|
|
|
445
443
|
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
446
444
|
}
|
|
447
445
|
const candidate = manifest;
|
|
448
|
-
|
|
449
|
-
|
|
446
|
+
const expectedKeys = [
|
|
447
|
+
'branch', 'channelDir', 'container', 'containerIdentity', 'engine', 'generation',
|
|
448
|
+
'mode', 'processingDir', 'project', 'publicStatusDir', 'repoRoot', 'runtimeDir',
|
|
449
|
+
'taskId', 'token', 'worktreeRoot'
|
|
450
|
+
];
|
|
451
|
+
if ('version' in candidate || typeof candidate.runtimeDir !== 'string'
|
|
452
|
+
|| Object.keys(manifest).sort().join(',') !== expectedKeys.sort().join(',')) {
|
|
453
|
+
throw new Error('SANDBOX_CONTROL_MANIFEST_REBUILD_REQUIRED: container-only recreation/rebuild is required');
|
|
450
454
|
}
|
|
451
455
|
if (typeof candidate.containerIdentity !== 'object' || candidate.containerIdentity === null
|
|
452
456
|
|| Array.isArray(candidate.containerIdentity))
|
|
@@ -463,7 +467,9 @@ function readSandboxControlManifestValue(manifestPath, allowLegacy) {
|
|
|
463
467
|
|| (candidate.taskId !== null && typeof candidate.taskId !== 'string')
|
|
464
468
|
|| typeof candidate.channelDir !== 'string' || typeof candidate.publicStatusDir !== 'string'
|
|
465
469
|
|| typeof candidate.processingDir !== 'string' || typeof candidate.token !== 'string'
|
|
466
|
-
|| typeof candidate.generation !== 'string'
|
|
470
|
+
|| typeof candidate.generation !== 'string'
|
|
471
|
+
|| (candidate.mode === 'task-bound' && (typeof candidate.taskId !== 'string' || candidate.taskId.length === 0))
|
|
472
|
+
|| (candidate.mode === 'branch-only' && candidate.taskId !== null))
|
|
467
473
|
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
468
474
|
const root = path.dirname(path.resolve(manifestPath));
|
|
469
475
|
if (path.resolve(candidate.channelDir) !== path.join(root, 'channel')
|
|
@@ -471,22 +477,13 @@ function readSandboxControlManifestValue(manifestPath, allowLegacy) {
|
|
|
471
477
|
|| path.resolve(candidate.processingDir) !== path.join(root, 'processing')) {
|
|
472
478
|
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
473
479
|
}
|
|
474
|
-
if (candidate.
|
|
475
|
-
if (!allowLegacy) {
|
|
476
|
-
throw new Error('SANDBOX_CONTROL_MANIFEST_VERSION_INVALID: expected version 5; container-only recreation is required');
|
|
477
|
-
}
|
|
478
|
-
return candidate;
|
|
479
|
-
}
|
|
480
|
-
if (typeof candidate.runtimeDir !== 'string' || path.resolve(candidate.runtimeDir) !== path.join(root, 'runtime')) {
|
|
480
|
+
if (path.resolve(candidate.runtimeDir) !== path.join(root, 'runtime')) {
|
|
481
481
|
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
482
482
|
}
|
|
483
483
|
return candidate;
|
|
484
484
|
}
|
|
485
485
|
export function readSandboxControlManifest(manifestPath) {
|
|
486
|
-
return readSandboxControlManifestValue(manifestPath
|
|
487
|
-
}
|
|
488
|
-
export function readSandboxControlManifestForTransition(manifestPath) {
|
|
489
|
-
return readSandboxControlManifestValue(manifestPath, true);
|
|
486
|
+
return readSandboxControlManifestValue(manifestPath);
|
|
490
487
|
}
|
|
491
488
|
function readBrokerOwner(filePath) {
|
|
492
489
|
if (!fs.existsSync(filePath))
|
|
@@ -507,6 +504,18 @@ function readBrokerOwner(filePath) {
|
|
|
507
504
|
throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
508
505
|
return owner;
|
|
509
506
|
}
|
|
507
|
+
export function assertSandboxControlBrokerOwner(manifest, options = {}) {
|
|
508
|
+
const root = path.dirname(path.resolve(manifest.publicStatusDir));
|
|
509
|
+
const owner = readBrokerOwner(path.join(root, 'broker.json'));
|
|
510
|
+
if (!owner)
|
|
511
|
+
throw new Error('SANDBOX_CONTROL_OWNER_EVIDENCE_MISSING');
|
|
512
|
+
if (owner.token !== manifest.token || owner.generation !== manifest.generation) {
|
|
513
|
+
throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
514
|
+
}
|
|
515
|
+
if (!ownerLive(owner, options.identityProbe))
|
|
516
|
+
throw new Error('SANDBOX_CONTROL_OWNER_LOST');
|
|
517
|
+
return owner;
|
|
518
|
+
}
|
|
510
519
|
function readStatusOwner(filePath) {
|
|
511
520
|
if (!fs.existsSync(filePath))
|
|
512
521
|
return null;
|
|
@@ -641,7 +650,7 @@ export async function quiesceSandboxControlRoot(root, options = {}) {
|
|
|
641
650
|
markSandboxControlRootQuiescing(resolvedRoot);
|
|
642
651
|
await waitForStartupTransition(resolvedRoot, Math.max(0, forceAt - Date.now()), identityProbe);
|
|
643
652
|
const manifestPath = path.join(resolvedRoot, 'manifest.json');
|
|
644
|
-
const manifest = fs.existsSync(manifestPath) ?
|
|
653
|
+
const manifest = fs.existsSync(manifestPath) ? readSandboxControlManifest(manifestPath) : null;
|
|
645
654
|
const broker = readBrokerOwner(path.join(resolvedRoot, 'broker.json'));
|
|
646
655
|
const status = readStatusOwner(path.join(resolvedRoot, 'public', 'status.json'));
|
|
647
656
|
const statusOwner = status ? status.broker : null;
|
|
@@ -730,7 +739,7 @@ export async function removeSandboxControlRoot(root, options) {
|
|
|
730
739
|
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
731
740
|
throw new Error('SANDBOX_CONTROL_CHANNEL_INVALID');
|
|
732
741
|
const manifestPath = path.join(resolvedRoot, 'manifest.json');
|
|
733
|
-
const manifest =
|
|
742
|
+
const manifest = readSandboxControlManifest(manifestPath);
|
|
734
743
|
const inspectContainer = options.inspectContainer
|
|
735
744
|
?? ((timeoutMs) => inspectSandboxControlContainer(manifest, { timeoutMs }));
|
|
736
745
|
const timeoutMs = options.timeoutMs ?? options.timing?.quiesceDeadlineMs ?? DEFAULT_QUIESCE_TIMEOUT_MS;
|
|
@@ -792,7 +801,7 @@ export async function removeSandboxControlRoot(root, options) {
|
|
|
792
801
|
: 'SANDBOX_CONTROL_CONTAINER_STILL_EXISTS');
|
|
793
802
|
}
|
|
794
803
|
}
|
|
795
|
-
const currentManifest =
|
|
804
|
+
const currentManifest = readSandboxControlManifest(manifestPath);
|
|
796
805
|
if (currentManifest.token !== manifest.token || currentManifest.generation !== manifest.generation
|
|
797
806
|
|| !isSandboxControlRootQuiescing(resolvedRoot)) {
|
|
798
807
|
throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
|
|
@@ -197,6 +197,7 @@ export async function serveSandboxControl(manifestPath, signal = new AbortContro
|
|
|
197
197
|
if (!recoverProcessing(manifest, brokerOwns))
|
|
198
198
|
return;
|
|
199
199
|
while (!signal.aborted) {
|
|
200
|
+
let settledExecution = null;
|
|
200
201
|
if (!brokerOwns())
|
|
201
202
|
break;
|
|
202
203
|
let current;
|
|
@@ -255,22 +256,7 @@ export async function serveSandboxControl(manifestPath, signal = new AbortContro
|
|
|
255
256
|
if (active?.settled) {
|
|
256
257
|
if (!brokerOwns())
|
|
257
258
|
break;
|
|
258
|
-
|
|
259
|
-
if (!brokerOwns())
|
|
260
|
-
break;
|
|
261
|
-
writeResponse(manifest, {
|
|
262
|
-
version: 2, id: active.request.id, phase: 'completed', exitCode: active.result.exitCode,
|
|
263
|
-
stdout: active.result.stdout, stderr: active.result.stderr, error: null
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
else {
|
|
267
|
-
if (!brokerOwns())
|
|
268
|
-
break;
|
|
269
|
-
writeResponse(manifest, unknown(active.request.id));
|
|
270
|
-
}
|
|
271
|
-
if (!brokerOwns())
|
|
272
|
-
break;
|
|
273
|
-
fs.rmSync(path.join(manifest.processingDir, active.request.id), { recursive: true, force: true });
|
|
259
|
+
settledExecution = active;
|
|
274
260
|
active = null;
|
|
275
261
|
}
|
|
276
262
|
let reasonCode = null;
|
|
@@ -313,6 +299,22 @@ export async function serveSandboxControl(manifestPath, signal = new AbortContro
|
|
|
313
299
|
appendSandboxControlAudit(manifest, 'broker-state', { state, reasonCode, requestId: active?.request.id ?? null });
|
|
314
300
|
lastState = stateKey;
|
|
315
301
|
}
|
|
302
|
+
if (settledExecution) {
|
|
303
|
+
if (!brokerOwns())
|
|
304
|
+
break;
|
|
305
|
+
if (settledExecution.result) {
|
|
306
|
+
writeResponse(manifest, {
|
|
307
|
+
version: 2, id: settledExecution.request.id, phase: 'completed', exitCode: settledExecution.result.exitCode,
|
|
308
|
+
stdout: settledExecution.result.stdout, stderr: settledExecution.result.stderr, error: null
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
writeResponse(manifest, unknown(settledExecution.request.id));
|
|
313
|
+
}
|
|
314
|
+
if (!brokerOwns())
|
|
315
|
+
break;
|
|
316
|
+
fs.rmSync(path.join(manifest.processingDir, settledExecution.request.id), { recursive: true, force: true });
|
|
317
|
+
}
|
|
316
318
|
let retiring = false;
|
|
317
319
|
for (const name of fs.readdirSync(requestsDir).sort()) {
|
|
318
320
|
if (!/^[a-f0-9-]{16,64}\.json$/.test(name))
|
|
@@ -2,7 +2,8 @@ import path from 'node:path';
|
|
|
2
2
|
import { shareBranchDir, shareCommonDir, shellConfigDirCandidates, worktreeDirCandidates } from './constants.js';
|
|
3
3
|
import { sandboxWorkspaceViewStatePaths } from './workspace-view.js';
|
|
4
4
|
export function sandboxCoreBindMounts(config, branch, overrides) {
|
|
5
|
-
const
|
|
5
|
+
const taskSources = overrides.taskSources ?? [];
|
|
6
|
+
const taskBound = Boolean(taskSources.length > 0 && overrides.taskId);
|
|
6
7
|
const workspaceMounts = sandboxWorkspaceViewStatePaths(overrides.workspaceViewRoot).map(({ state, hostPath }) => taskBound && state === 'active'
|
|
7
8
|
? {
|
|
8
9
|
hostPaths: [path.join(hostPath, '.short-ids.json')],
|
|
@@ -26,7 +27,7 @@ export function sandboxCoreBindMounts(config, branch, overrides) {
|
|
|
26
27
|
];
|
|
27
28
|
if (taskBound) {
|
|
28
29
|
mounts.push({
|
|
29
|
-
hostPaths:
|
|
30
|
+
hostPaths: taskSources,
|
|
30
31
|
containerPath: path.posix.join('/workspace/.agents/workspace/active', overrides.taskId),
|
|
31
32
|
readOnly: false
|
|
32
33
|
});
|
|
@@ -9,6 +9,7 @@ import { createSandboxCapabilityPlan } from './agent-client-reconciler.js';
|
|
|
9
9
|
import { toEnginePath } from './engines/wsl2-paths.js';
|
|
10
10
|
import { sandboxCoreBindMounts } from './mounts.js';
|
|
11
11
|
import { assertSandboxTaskSource, sandboxControlPaths, sandboxWorkspaceViewPaths } from './workspace-view.js';
|
|
12
|
+
import { resolveTaskWorkspace } from './task-resolver.js';
|
|
12
13
|
import { parseSandboxWorkspaceIdentity, sameSandboxWorkspaceIdentity } from './workspace-identity.js';
|
|
13
14
|
import { inspectWorktree } from './worktree-safety.js';
|
|
14
15
|
import { commandForEngine, runEngine, runOkEngine, runProbe, runVerboseEngine } from './shell.js';
|
|
@@ -407,6 +408,66 @@ function sourceCandidates(engine, hostPath) {
|
|
|
407
408
|
}
|
|
408
409
|
return [...new Set(candidates.map((candidate) => normalizeMountSource(engine, candidate)))];
|
|
409
410
|
}
|
|
411
|
+
function recoveryTaskSources(repoRoot, taskId) {
|
|
412
|
+
const activePath = path.join(repoRoot, '.agents', 'workspace', 'active', taskId);
|
|
413
|
+
const completedPath = path.join(repoRoot, '.agents', 'workspace', 'completed', taskId);
|
|
414
|
+
if (fs.existsSync(activePath) && !fs.existsSync(completedPath)) {
|
|
415
|
+
const source = assertSandboxTaskSource(repoRoot, taskId);
|
|
416
|
+
return { mountPaths: [source], accessiblePaths: [source] };
|
|
417
|
+
}
|
|
418
|
+
const task = resolveTaskWorkspace(taskId, repoRoot);
|
|
419
|
+
if (task.state === 'active') {
|
|
420
|
+
const source = assertSandboxTaskSource(repoRoot, taskId);
|
|
421
|
+
return { mountPaths: [source], accessiblePaths: [source] };
|
|
422
|
+
}
|
|
423
|
+
if (task.state !== 'completed') {
|
|
424
|
+
throw new Error(`SANDBOX_TASK_SOURCE_STATE_UNSUPPORTED: task ${taskId} is ${task.state}`);
|
|
425
|
+
}
|
|
426
|
+
const completedRoot = fs.realpathSync.native(path.join(repoRoot, '.agents', 'workspace', 'completed'));
|
|
427
|
+
const completedSource = fs.realpathSync.native(path.dirname(task.taskMd));
|
|
428
|
+
if (path.dirname(completedSource) !== completedRoot) {
|
|
429
|
+
throw new Error(`SANDBOX_TASK_SOURCE_INVALID: ${completedSource} is not under completed workspace`);
|
|
430
|
+
}
|
|
431
|
+
const historicalActivePath = path.join(repoRoot, '.agents', 'workspace', 'active', taskId);
|
|
432
|
+
const mountPaths = [completedSource, historicalActivePath];
|
|
433
|
+
if (fs.existsSync(historicalActivePath)) {
|
|
434
|
+
try {
|
|
435
|
+
mountPaths[1] = fs.realpathSync.native(historicalActivePath);
|
|
436
|
+
}
|
|
437
|
+
catch {
|
|
438
|
+
// Keep the exact historical path as a candidate; the current completed
|
|
439
|
+
// source remains the only source that must still be accessible.
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return { mountPaths, accessiblePaths: [completedSource] };
|
|
443
|
+
}
|
|
444
|
+
function startCompletedSandboxContainer(params) {
|
|
445
|
+
const taskSources = recoveryTaskSources(params.config.repoRoot, params.taskId);
|
|
446
|
+
const completedSource = taskSources.accessiblePaths[0];
|
|
447
|
+
const activeRoot = path.join(params.config.repoRoot, '.agents', 'workspace', 'active');
|
|
448
|
+
const historicalSource = path.join(activeRoot, params.taskId);
|
|
449
|
+
fs.mkdirSync(activeRoot, { recursive: true });
|
|
450
|
+
try {
|
|
451
|
+
fs.lstatSync(historicalSource);
|
|
452
|
+
throw new Error(`SANDBOX_COMPLETED_SOURCE_CONFLICT: historical task source already exists at ${historicalSource}`);
|
|
453
|
+
}
|
|
454
|
+
catch (error) {
|
|
455
|
+
if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT'))
|
|
456
|
+
throw error;
|
|
457
|
+
}
|
|
458
|
+
fs.symlinkSync(completedSource, historicalSource, process.platform === 'win32' ? 'junction' : 'dir');
|
|
459
|
+
try {
|
|
460
|
+
params.start(params.engine, params.container);
|
|
461
|
+
}
|
|
462
|
+
finally {
|
|
463
|
+
const temporarySource = fs.lstatSync(historicalSource);
|
|
464
|
+
const resolvedSource = fs.realpathSync.native(historicalSource);
|
|
465
|
+
if (!temporarySource.isSymbolicLink() || resolvedSource !== completedSource) {
|
|
466
|
+
throw new Error(`SANDBOX_COMPLETED_SOURCE_CHANGED: refusing to remove unexpected path at ${historicalSource}`);
|
|
467
|
+
}
|
|
468
|
+
fs.unlinkSync(historicalSource);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
410
471
|
function sameHostSource(left, right) {
|
|
411
472
|
try {
|
|
412
473
|
const leftStat = fs.statSync(left, { bigint: true });
|
|
@@ -440,21 +501,29 @@ function expectedMounts(params) {
|
|
|
440
501
|
container: params.container,
|
|
441
502
|
identity: params.workspace
|
|
442
503
|
});
|
|
504
|
+
const taskSources = params.workspace.mode === 'task-bound'
|
|
505
|
+
? recoveryTaskSources(config.repoRoot, params.workspace.taskId)
|
|
506
|
+
: null;
|
|
507
|
+
const taskId = params.workspace.mode === 'task-bound' ? params.workspace.taskId : null;
|
|
443
508
|
const core = sandboxCoreBindMounts(config, branch, {
|
|
444
509
|
workspaceViewRoot: view.root,
|
|
445
510
|
controlDir: control.channelDir,
|
|
446
511
|
controlStatusDir: control.statusDir,
|
|
447
|
-
...(
|
|
448
|
-
? {
|
|
512
|
+
...(taskSources === null
|
|
513
|
+
? {}
|
|
514
|
+
: {
|
|
449
515
|
runtimeDir: control.runtimeDir,
|
|
450
|
-
|
|
451
|
-
taskId:
|
|
452
|
-
}
|
|
453
|
-
: {})
|
|
516
|
+
taskSources: taskSources.mountPaths,
|
|
517
|
+
taskId: taskId
|
|
518
|
+
})
|
|
454
519
|
}).map((mount) => ({
|
|
455
520
|
path: mount.containerPath,
|
|
456
521
|
expectedType: 'bind',
|
|
457
522
|
hostPaths: mount.hostPaths,
|
|
523
|
+
...(taskSources !== null && taskId !== null
|
|
524
|
+
&& mount.containerPath === `/workspace/.agents/workspace/active/${taskId}`
|
|
525
|
+
? { sourceAccessiblePaths: taskSources.accessiblePaths }
|
|
526
|
+
: {}),
|
|
458
527
|
expectedRW: !mount.readOnly
|
|
459
528
|
}));
|
|
460
529
|
const live = tools.flatMap((tool) => (tool.hostLiveMounts ?? []).flatMap(({ hostPath, containerSubpath }) => {
|
|
@@ -469,9 +538,7 @@ function expectedMounts(params) {
|
|
|
469
538
|
}];
|
|
470
539
|
}));
|
|
471
540
|
const persistentTools = tools
|
|
472
|
-
.filter((tool) => !tool.tmpfs
|
|
473
|
-
&& (config.agentClientSource === 'canonical'
|
|
474
|
-
|| actualMounts.has(tool.containerMount)))
|
|
541
|
+
.filter((tool) => !tool.tmpfs)
|
|
475
542
|
.map((tool) => ({
|
|
476
543
|
path: tool.containerMount,
|
|
477
544
|
expectedType: 'bind',
|
|
@@ -577,13 +644,10 @@ export function collectSandboxRecoverySnapshot(params) {
|
|
|
577
644
|
taskId: sandboxTaskIdLabel(params.config)
|
|
578
645
|
});
|
|
579
646
|
const selectedToolIds = new Set(tools.map((tool) => tool.id));
|
|
580
|
-
const
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
.map((tool) => tool.containerMount)
|
|
585
|
-
.filter((mountPath) => mountsByDestination.has(mountPath))
|
|
586
|
-
: [];
|
|
647
|
+
const unexpectedCapabilityMounts = capabilityPlan.cleanupInventory
|
|
648
|
+
.filter((tool) => isAgentClientId(tool.id) && !selectedToolIds.has(tool.id))
|
|
649
|
+
.map((tool) => tool.containerMount)
|
|
650
|
+
.filter((mountPath) => mountsByDestination.has(mountPath));
|
|
587
651
|
const mountSnapshots = expectedMounts({
|
|
588
652
|
config: params.config,
|
|
589
653
|
branch: params.branch,
|
|
@@ -616,7 +680,7 @@ export function collectSandboxRecoverySnapshot(params) {
|
|
|
616
680
|
sourceAccessible: expected.expectedType === 'tmpfs'
|
|
617
681
|
? true
|
|
618
682
|
: matchedHostPath !== undefined
|
|
619
|
-
&& hostSourceAccessible(
|
|
683
|
+
&& (expected.sourceAccessiblePaths ?? [matchedHostPath]).some((hostPath) => hostSourceAccessible(hostPath, expected.expectedRW))
|
|
620
684
|
};
|
|
621
685
|
});
|
|
622
686
|
return {
|
|
@@ -636,9 +700,7 @@ export function collectSandboxRecoverySnapshot(params) {
|
|
|
636
700
|
identityOk: typeof inspection.Id === 'string' && inspection.Id.length > 0
|
|
637
701
|
&& branchLabel === params.branch
|
|
638
702
|
&& sameSandboxWorkspaceIdentity(containerWorkspace, workspace),
|
|
639
|
-
runtimeCapabilityOk:
|
|
640
|
-
? labels[sandboxRuntimeCapabilityLabel(params.config)] === capabilityPlan.runtimeSignature
|
|
641
|
-
: undefined,
|
|
703
|
+
runtimeCapabilityOk: labels[sandboxRuntimeCapabilityLabel(params.config)] === capabilityPlan.runtimeSignature,
|
|
642
704
|
unexpectedCapabilityMounts,
|
|
643
705
|
mounts: mountSnapshots,
|
|
644
706
|
tmpfs,
|
|
@@ -813,11 +875,22 @@ export async function ensureSandboxReady(params) {
|
|
|
813
875
|
workspace: params.workspace ?? { mode: 'branch-only' }
|
|
814
876
|
});
|
|
815
877
|
}
|
|
816
|
-
if (params.forceRecreate) {
|
|
878
|
+
if (params.forceRecreate || (params.reentry === 'completed' && params.allowRecreate)) {
|
|
817
879
|
throw new Error('Explicit container recreation requested.');
|
|
818
880
|
}
|
|
819
881
|
if (!params.row.running) {
|
|
820
|
-
|
|
882
|
+
if (params.reentry === 'completed' && params.workspace?.mode === 'task-bound') {
|
|
883
|
+
startCompletedSandboxContainer({
|
|
884
|
+
config: params.config,
|
|
885
|
+
engine: params.engine,
|
|
886
|
+
container: params.row.name,
|
|
887
|
+
taskId: params.workspace.taskId,
|
|
888
|
+
start: startFn
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
else {
|
|
892
|
+
startFn(params.engine, params.row.name);
|
|
893
|
+
}
|
|
821
894
|
const initial = assess({
|
|
822
895
|
config: params.config,
|
|
823
896
|
engine: params.engine,
|
|
@@ -930,6 +1003,34 @@ export async function ensureSandboxReady(params) {
|
|
|
930
1003
|
catch (error) {
|
|
931
1004
|
failure = error instanceof Error ? error : new Error(String(error));
|
|
932
1005
|
}
|
|
1006
|
+
if (params.reentry === 'completed') {
|
|
1007
|
+
if (params.workspace?.mode !== 'task-bound') {
|
|
1008
|
+
throw new Error('SANDBOX_COMPLETED_REENTRY_INVALID: completed re-entry requires a task-bound workspace');
|
|
1009
|
+
}
|
|
1010
|
+
const taskId = params.workspace.taskId;
|
|
1011
|
+
const commandChain = [
|
|
1012
|
+
`docker exec -it ${params.row.name} bash /usr/local/bin/sandbox-tmux-entry`,
|
|
1013
|
+
`ai sandbox rm ${taskId}`,
|
|
1014
|
+
`ai sandbox create ${params.branch}`
|
|
1015
|
+
];
|
|
1016
|
+
const code = params.allowRecreate || params.forceRecreate
|
|
1017
|
+
? 'SANDBOX_COMPLETED_RECREATE_UNSUPPORTED'
|
|
1018
|
+
: 'SANDBOX_COMPLETED_REENTRY_FAILED';
|
|
1019
|
+
const lead = code === 'SANDBOX_COMPLETED_RECREATE_UNSUPPORTED'
|
|
1020
|
+
? 'Completed sandbox replacement is unsupported; the existing container was not replaced.'
|
|
1021
|
+
: 'Completed sandbox recovery failed; the existing container was not replaced.';
|
|
1022
|
+
throw new Error([
|
|
1023
|
+
`${code}: ${lead}`,
|
|
1024
|
+
`Task: ${taskId}; container: ${params.row.name}; branch: ${params.branch}.`,
|
|
1025
|
+
'To preserve the current container state, enter it manually with:',
|
|
1026
|
+
` ${commandChain[0]}`,
|
|
1027
|
+
'If you intentionally want a replacement, the following is the full interactive sandbox cleanup, not a container-only delete.',
|
|
1028
|
+
'It may remove the worktree, local branch, tool/shell state, and branch share:',
|
|
1029
|
+
` ${commandChain[1]}`,
|
|
1030
|
+
` ${commandChain[2]}`,
|
|
1031
|
+
`Details: ${failure.message}`
|
|
1032
|
+
].join('\n'));
|
|
1033
|
+
}
|
|
933
1034
|
const dataBoundary = 'The existing container writable layer, ordinary /tmp data, processes, tmux sessions, and RAM state may be lost by replacement; worktree and host-managed sandbox data are preserved.';
|
|
934
1035
|
const recoveryTarget = params.workspace?.mode === 'task-bound'
|
|
935
1036
|
? params.workspace.taskId
|