@fitlab-ai/agent-infra 0.9.7 → 0.9.9
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 +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/bin/internal-cli.js +11 -1
- package/dist/lib/agent-clients/adapters/claude-code.js +3 -3
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +60 -22
- package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +8 -1
- package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +175 -11
- package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-context.js +147 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +86 -69
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +2 -1
- package/dist/lib/decide.js +68 -14
- package/dist/lib/git/workflow.js +10 -1
- package/dist/lib/init.js +1 -1
- package/dist/lib/internal/codex-lifecycle.js +19 -5
- package/dist/lib/internal/codex-sandbox-controller.js +6 -13
- package/dist/lib/internal/git-workflow.js +5 -7
- package/dist/lib/internal/platform-pr.js +20 -7
- package/dist/lib/internal/release-workflow.js +3 -4
- package/dist/lib/internal/sandbox-control.js +79 -2
- package/dist/lib/internal/task-activity.js +57 -7
- package/dist/lib/internal/task-event.js +57 -4
- package/dist/lib/internal/task-finalization.js +54 -0
- package/dist/lib/internal/task-ledger.js +59 -5
- package/dist/lib/internal/task-lifecycle.js +59 -8
- package/dist/lib/internal/task-orchestration.js +161 -288
- package/dist/lib/internal/task-override.js +131 -0
- package/dist/lib/internal/task-review.js +64 -6
- package/dist/lib/internal/task-verify.js +65 -3
- package/dist/lib/internal/task-warning.js +57 -4
- package/dist/lib/platform/pr-summary.js +53 -13
- package/dist/lib/platform/pull-requests.js +205 -41
- package/dist/lib/runtime/agent-runtime.js +56 -0
- package/dist/lib/sandbox/commands/create.js +389 -311
- package/dist/lib/sandbox/commands/rm.js +316 -55
- package/dist/lib/sandbox/control/client.js +190 -6
- package/dist/lib/sandbox/control/container-identity.js +1 -1
- package/dist/lib/sandbox/control/controller-registration.js +273 -0
- package/dist/lib/sandbox/control/executor.js +145 -5
- package/dist/lib/sandbox/control/lifecycle.js +395 -73
- package/dist/lib/sandbox/control/protocol.js +67 -6
- package/dist/lib/sandbox/control/state.js +27 -11
- package/dist/lib/sandbox/engine.js +14 -7
- package/dist/lib/sandbox/index.js +5 -3
- package/dist/lib/sandbox/managed-fs.js +5 -0
- package/dist/lib/sandbox/mounts.js +20 -5
- package/dist/lib/sandbox/recovery.js +78 -10
- package/dist/lib/sandbox/task-resolver.js +50 -6
- package/dist/lib/sandbox/workspace-identity.js +27 -0
- package/dist/lib/sandbox/workspace-view.js +62 -40
- package/dist/lib/sandbox/worktree-safety.js +215 -2
- package/dist/lib/server/process-state.js +51 -1
- package/dist/lib/task/activity-intent.js +4 -1
- package/dist/lib/task/codex-orchestration.js +158 -33
- package/dist/lib/task/commands/status.js +59 -1
- package/dist/lib/task/commit-operation.js +335 -0
- package/dist/lib/task/commit-policy.js +18 -0
- package/dist/lib/task/decision-intents.js +4 -2
- package/dist/lib/task/delegation-receipts.js +279 -17
- package/dist/lib/task/events.js +9 -5
- package/dist/lib/task/finalization.js +567 -0
- package/dist/lib/task/guard-override.js +31 -0
- package/dist/lib/task/human-override.js +1240 -0
- package/dist/lib/task/ledger-intents.js +6 -3
- package/dist/lib/task/lifecycle.js +69 -14
- package/dist/lib/task/operation-outcome.js +33 -0
- package/dist/lib/task/orchestration.js +199 -719
- package/dist/lib/task/review-finalization.js +3 -2
- package/dist/lib/task/short-id.js +59 -1
- package/dist/lib/task/task-execution-lock.js +6 -1
- package/dist/lib/task/verification-engine.js +138 -59
- package/dist/lib/task/verification.js +15 -2
- package/dist/lib/task/workflow-warning-intents.js +54 -3
- package/dist/lib/task/write.js +2 -1
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapters/claude-code.ts +3 -3
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +63 -20
- package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +12 -0
- package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +278 -13
- package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +180 -0
- package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -0
- package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +9 -1
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +112 -93
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +2 -1
- package/lib/decide.ts +59 -14
- package/lib/git/workflow.ts +11 -2
- package/lib/init.ts +1 -1
- package/lib/internal/codex-lifecycle.ts +18 -5
- package/lib/internal/codex-sandbox-controller.ts +6 -12
- package/lib/internal/git-workflow.ts +5 -6
- package/lib/internal/platform-pr.ts +14 -7
- package/lib/internal/release-workflow.ts +3 -4
- package/lib/internal/sandbox-control.ts +92 -2
- package/lib/internal/task-activity.ts +46 -6
- package/lib/internal/task-event.ts +53 -4
- package/lib/internal/task-finalization.ts +68 -0
- package/lib/internal/task-ledger.ts +48 -4
- package/lib/internal/task-lifecycle.ts +56 -12
- package/lib/internal/task-orchestration.ts +32 -107
- package/lib/internal/task-override.ts +138 -0
- package/lib/internal/task-review.ts +51 -6
- package/lib/internal/task-verify.ts +52 -3
- package/lib/internal/task-warning.ts +44 -3
- package/lib/platform/pr-summary.ts +65 -14
- package/lib/platform/pull-requests.ts +211 -41
- package/lib/runtime/agent-runtime.ts +76 -0
- package/lib/sandbox/commands/create.ts +88 -4
- package/lib/sandbox/commands/rm.ts +413 -61
- package/lib/sandbox/config.ts +2 -1
- package/lib/sandbox/control/client.ts +255 -6
- package/lib/sandbox/control/container-identity.ts +3 -3
- package/lib/sandbox/control/controller-registration.ts +347 -0
- package/lib/sandbox/control/executor.ts +172 -6
- package/lib/sandbox/control/lifecycle.ts +465 -77
- package/lib/sandbox/control/protocol.ts +85 -9
- package/lib/sandbox/control/state.ts +35 -18
- package/lib/sandbox/engine.ts +20 -9
- package/lib/sandbox/index.ts +5 -3
- package/lib/sandbox/managed-fs.ts +5 -0
- package/lib/sandbox/mounts.ts +23 -5
- package/lib/sandbox/recovery.ts +90 -9
- package/lib/sandbox/task-resolver.ts +58 -7
- package/lib/sandbox/workspace-identity.ts +50 -2
- package/lib/sandbox/workspace-view.ts +73 -43
- package/lib/sandbox/worktree-safety.ts +236 -2
- package/lib/server/process-state.ts +48 -1
- package/lib/task/activity-intent.ts +4 -2
- package/lib/task/codex-orchestration.ts +167 -33
- package/lib/task/commands/status.ts +78 -1
- package/lib/task/commit-operation.ts +430 -0
- package/lib/task/commit-policy.ts +30 -0
- package/lib/task/decision-intents.ts +4 -2
- package/lib/task/delegation-receipts.ts +304 -19
- package/lib/task/events.ts +10 -5
- package/lib/task/finalization.ts +719 -0
- package/lib/task/guard-override.ts +50 -0
- package/lib/task/human-override.ts +1503 -0
- package/lib/task/ledger-intents.ts +6 -3
- package/lib/task/lifecycle.ts +71 -14
- package/lib/task/operation-outcome.ts +48 -0
- package/lib/task/orchestration.ts +239 -922
- package/lib/task/review-finalization.ts +6 -2
- package/lib/task/short-id.ts +65 -1
- package/lib/task/task-execution-lock.ts +10 -1
- package/lib/task/verification-engine.ts +145 -64
- package/lib/task/verification.ts +18 -3
- package/lib/task/workflow-warning-intents.ts +61 -4
- package/lib/task/write.ts +4 -1
- package/package.json +2 -2
- package/templates/.agents/QUICKSTART.en.md +2 -2
- package/templates/.agents/QUICKSTART.zh-CN.md +2 -2
- package/templates/.agents/hooks/lifecycle-delegation.js +9 -24
- package/templates/.agents/rules/lifecycle-orchestration.en.md +4 -3
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +4 -3
- package/templates/.agents/rules/task-management.en.md +2 -2
- package/templates/.agents/rules/task-management.zh-CN.md +2 -2
- package/templates/.agents/skills/block-task/SKILL.en.md +2 -4
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -4
- package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/close-codescan/SKILL.en.md +6 -2
- package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +6 -2
- package/templates/.agents/skills/close-dependabot/SKILL.en.md +6 -2
- package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +6 -2
- package/templates/.agents/skills/commit/SKILL.en.md +41 -125
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +52 -123
- package/templates/.agents/skills/commit/reference/commit-message.en.md +1 -1
- package/templates/.agents/skills/commit/reference/commit-message.zh-CN.md +1 -1
- package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +14 -43
- package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +14 -49
- package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +1 -1
- package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +1 -1
- package/templates/.agents/skills/commit/reference/task-status-update.en.md +2 -35
- package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +2 -35
- package/templates/.agents/skills/complete-task/SKILL.en.md +31 -38
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +31 -38
- package/templates/.agents/skills/complete-task/config/verify.en.json +1 -0
- package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +1 -0
- package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +1 -1
- package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +1 -1
- package/templates/.agents/skills/create-pr/SKILL.en.md +6 -4
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +6 -4
- package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +2 -2
- package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +2 -2
- package/templates/.agents/skills/run-task/SKILL.en.md +5 -5
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +5 -5
- package/templates/.agents/skills/run-task/reference/host-validation.en.md +3 -1
- package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +3 -1
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +1 -1
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +1 -1
- package/templates/.codex/hooks.json +4 -4
- package/dist/lib/task/commit-finalization.js +0 -305
- package/dist/lib/task/commit-intent.js +0 -165
- package/lib/task/commit-finalization.ts +0 -419
- package/lib/task/commit-intent.ts +0 -230
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { validateTaskCreateCandidate } from '../../task/create.js';
|
|
2
|
+
import { normalizeAgentToken } from '../../agent-clients/tokens.js';
|
|
2
3
|
export const SANDBOX_CONTROL_MAX_BYTES = 64 * 1024;
|
|
3
4
|
export const SANDBOX_CONTROL_ADMISSION_WINDOW_MS = 2_000;
|
|
4
5
|
export const SANDBOX_CONTROL_STATUS_INTERVAL_MS = 250;
|
|
5
6
|
export const SANDBOX_CONTROL_STATUS_STALE_MS = 1_500;
|
|
6
7
|
export const SANDBOX_CONTROL_FUTURE_SKEW_MS = 1_000;
|
|
7
|
-
export const SANDBOX_CONTROL_FAMILIES = ['task-lifecycle', 'task-orchestration', 'task-create'];
|
|
8
|
+
export const SANDBOX_CONTROL_FAMILIES = ['task-lifecycle', 'task-orchestration', 'task-finalization', 'task-create', 'codex-controller'];
|
|
8
9
|
export const DEFAULT_SANDBOX_CONTROL_TIMING = Object.freeze({
|
|
9
10
|
controlTickMs: 250,
|
|
10
11
|
parkedBindingInitialMs: 1_000,
|
|
@@ -33,7 +34,7 @@ export function validateSandboxControlRequest(value, manifest, options = {}) {
|
|
|
33
34
|
fail('SANDBOX_CONTROL_REQUEST_INVALID', 'request must be an object');
|
|
34
35
|
}
|
|
35
36
|
const request = value;
|
|
36
|
-
if (request.version !==
|
|
37
|
+
if (request.version !== 3 || typeof request.id !== 'string'
|
|
37
38
|
|| !/^[a-f0-9-]{16,64}$/.test(request.id) || request.token !== manifest.token
|
|
38
39
|
|| typeof request.family !== 'string' || !isSandboxControlFamily(request.family)
|
|
39
40
|
|| !Number.isSafeInteger(request.issuedAt) || !Number.isSafeInteger(request.expiresAt))
|
|
@@ -53,13 +54,47 @@ export function validateSandboxControlRequest(value, manifest, options = {}) {
|
|
|
53
54
|
fail('SANDBOX_CONTROL_REQUEST_TOO_LARGE', 'request exceeds the control limit');
|
|
54
55
|
}
|
|
55
56
|
if (request.family === 'task-create') {
|
|
56
|
-
const expected = ['candidate', 'expiresAt', 'family', 'generation', 'id', 'issuedAt', 'token', 'version'];
|
|
57
|
+
const expected = ['candidate', 'controllerProcess', 'controllerProof', 'expiresAt', 'family', 'generation', 'id', 'issuedAt', 'token', 'version'];
|
|
57
58
|
if (Object.keys(request).sort().join(',') !== expected.sort().join(',')) {
|
|
58
59
|
fail('SANDBOX_CONTROL_REQUEST_INVALID', 'request schema or authorization is invalid');
|
|
59
60
|
}
|
|
61
|
+
if (request.controllerProcess !== null || request.controllerProof !== null) {
|
|
62
|
+
fail('SANDBOX_CONTROL_REQUEST_INVALID', 'task-create cannot carry controller authority');
|
|
63
|
+
}
|
|
60
64
|
return { ...request, candidate: validateTaskCreateCandidate(request.candidate) };
|
|
61
65
|
}
|
|
62
|
-
|
|
66
|
+
if (request.family === 'codex-controller') {
|
|
67
|
+
const expected = ['args', 'command', 'controllerProcess', 'controllerProof', 'expiresAt', 'family', 'generation', 'id', 'issuedAt', 'token', 'version'];
|
|
68
|
+
if (Object.keys(request).sort().join(',') !== expected.sort().join(',')
|
|
69
|
+
|| !Array.isArray(request.args) || request.args.length !== 0
|
|
70
|
+
|| !['open', 'close', 'verify'].includes(request.command)
|
|
71
|
+
|| !validControllerProcess(request.controllerProcess)
|
|
72
|
+
|| (request.command === 'open' && request.controllerProof !== null)
|
|
73
|
+
|| (request.command !== 'open' && !validControllerProof(request.controllerProof))
|
|
74
|
+
|| (request.command === 'verify'
|
|
75
|
+
&& JSON.stringify(request.controllerProcess) !== JSON.stringify(request.controllerProof.controllerProcess))) {
|
|
76
|
+
fail('SANDBOX_CONTROL_REQUEST_INVALID', 'controller request schema is invalid');
|
|
77
|
+
}
|
|
78
|
+
if (manifest.mode !== 'task-bound' || !manifest.taskId) {
|
|
79
|
+
fail('SANDBOX_CONTROL_BRANCH_ONLY', 'branch-only sandboxes cannot register a Codex controller');
|
|
80
|
+
}
|
|
81
|
+
return request;
|
|
82
|
+
}
|
|
83
|
+
if (request.family === 'task-finalization') {
|
|
84
|
+
const expected = ['agent', 'args', 'controllerProcess', 'controllerProof', 'expiresAt', 'family', 'generation', 'id', 'issuedAt', 'operation', 'token', 'version'];
|
|
85
|
+
if (Object.keys(request).sort().join(',') !== expected.sort().join(',')
|
|
86
|
+
|| request.operation !== 'complete'
|
|
87
|
+
|| !Array.isArray(request.args) || request.args.length !== 0
|
|
88
|
+
|| typeof request.agent !== 'string' || normalizeAgentToken(request.agent) !== request.agent
|
|
89
|
+
|| request.controllerProcess !== null || request.controllerProof !== null) {
|
|
90
|
+
fail('SANDBOX_CONTROL_REQUEST_INVALID', 'task-finalization request schema or authorization is invalid');
|
|
91
|
+
}
|
|
92
|
+
if (manifest.mode !== 'task-bound' || !manifest.taskId) {
|
|
93
|
+
fail('SANDBOX_CONTROL_BRANCH_ONLY', 'branch-only sandboxes cannot finalize tasks');
|
|
94
|
+
}
|
|
95
|
+
return request;
|
|
96
|
+
}
|
|
97
|
+
const expected = ['args', 'controllerProcess', 'controllerProof', 'expiresAt', 'family', 'generation', 'id', 'issuedAt', 'token', 'version'];
|
|
63
98
|
if (Object.keys(request).sort().join(',') !== expected.sort().join(',')
|
|
64
99
|
|| !Array.isArray(request.args) || !request.args.every((arg) => typeof arg === 'string')) {
|
|
65
100
|
fail('SANDBOX_CONTROL_REQUEST_INVALID', 'request schema or authorization is invalid');
|
|
@@ -71,15 +106,41 @@ export function validateSandboxControlRequest(value, manifest, options = {}) {
|
|
|
71
106
|
&& request.args.some((arg) => arg === '--git-worktree-root' || arg.startsWith('--git-worktree-root='))) {
|
|
72
107
|
fail('SANDBOX_CONTROL_REQUEST_INVALID', 'worktree binding is reserved for the control broker');
|
|
73
108
|
}
|
|
109
|
+
if (request.controllerProcess !== null
|
|
110
|
+
|| (request.controllerProof !== null && !validControllerProof(request.controllerProof))) {
|
|
111
|
+
fail('SANDBOX_CONTROL_REQUEST_INVALID', 'controller authority is invalid');
|
|
112
|
+
}
|
|
113
|
+
if (request.family !== 'task-orchestration' && request.controllerProof !== null) {
|
|
114
|
+
fail('SANDBOX_CONTROL_REQUEST_INVALID', 'controller proof is not allowed for this family');
|
|
115
|
+
}
|
|
74
116
|
return request;
|
|
75
117
|
}
|
|
76
118
|
export function bindSandboxControlTask(request, taskId) {
|
|
77
|
-
if (request.family === 'task-create')
|
|
78
|
-
fail('SANDBOX_CONTROL_REQUEST_INVALID',
|
|
119
|
+
if (request.family === 'task-create' || request.family === 'codex-controller' || request.family === 'task-finalization') {
|
|
120
|
+
fail('SANDBOX_CONTROL_REQUEST_INVALID', `${request.family} requests do not bind a current task`);
|
|
121
|
+
}
|
|
79
122
|
if (request.args.length === 0)
|
|
80
123
|
fail('SANDBOX_CONTROL_REQUEST_INVALID', 'command arguments are required');
|
|
81
124
|
return [taskId, ...request.args.slice(1)];
|
|
82
125
|
}
|
|
126
|
+
function validControllerProcess(value) {
|
|
127
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
128
|
+
return false;
|
|
129
|
+
const processValue = value;
|
|
130
|
+
return Object.keys(processValue).sort().join(',') === 'pid,startTime'
|
|
131
|
+
&& Number.isSafeInteger(processValue.pid) && processValue.pid > 0
|
|
132
|
+
&& Number.isSafeInteger(processValue.startTime) && processValue.startTime >= 0;
|
|
133
|
+
}
|
|
134
|
+
function validControllerProof(value) {
|
|
135
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
136
|
+
return false;
|
|
137
|
+
const proof = value;
|
|
138
|
+
return Object.keys(proof).sort().join(',') === 'controllerProcess,leaseId,leaseSecret,version'
|
|
139
|
+
&& proof.version === 1
|
|
140
|
+
&& typeof proof.leaseId === 'string' && /^[a-f0-9]{64}$/u.test(proof.leaseId)
|
|
141
|
+
&& typeof proof.leaseSecret === 'string' && /^[a-f0-9]{64}$/u.test(proof.leaseSecret)
|
|
142
|
+
&& validControllerProcess(proof.controllerProcess);
|
|
143
|
+
}
|
|
83
144
|
export function controlError(error) {
|
|
84
145
|
if (error instanceof SandboxControlProtocolError) {
|
|
85
146
|
return { code: error.code, message: error.message, retryable: error.retryable };
|
|
@@ -3,7 +3,7 @@ import fs from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { randomUUID } from 'node:crypto';
|
|
5
5
|
import { buildProcessTreeStopCommand } from '../../server/process-control.js';
|
|
6
|
-
import {
|
|
6
|
+
import { getProcessIdentityState } from '../../server/process-state.js';
|
|
7
7
|
export const SANDBOX_CONTROL_AUDIT_MAX_BYTES = 1024 * 1024;
|
|
8
8
|
export const SANDBOX_CONTROL_EXECUTION_STOP_MS = 2_000;
|
|
9
9
|
function assertRegularFile(filePath) {
|
|
@@ -72,7 +72,7 @@ export function readActiveLease(manifest, now = Date.now()) {
|
|
|
72
72
|
return null;
|
|
73
73
|
return lease;
|
|
74
74
|
}
|
|
75
|
-
export function cleanupStaleSandboxControlLease(manifest, now = Date.now()) {
|
|
75
|
+
export function cleanupStaleSandboxControlLease(manifest, now = Date.now(), options = {}) {
|
|
76
76
|
const filePath = path.join(path.dirname(manifest.publicStatusDir), 'lease.json');
|
|
77
77
|
if (!fs.existsSync(filePath))
|
|
78
78
|
return false;
|
|
@@ -87,10 +87,13 @@ export function cleanupStaleSandboxControlLease(manifest, now = Date.now()) {
|
|
|
87
87
|
|| typeof lease.branch !== 'string' || typeof lease.reason !== 'string') {
|
|
88
88
|
throw new Error('SANDBOX_CONTROL_LEASE_INVALID');
|
|
89
89
|
}
|
|
90
|
-
const
|
|
91
|
-
|| lease.branch !== manifest.branch || lease.expiresAt <= now || !processIdentityMatches({
|
|
90
|
+
const ownerState = (options.identityProbe ?? getProcessIdentityState)({
|
|
92
91
|
pid: lease.owner.pid, startTime: lease.owner.startTime
|
|
93
92
|
});
|
|
93
|
+
if (ownerState === 'unknown')
|
|
94
|
+
throw new Error('SANDBOX_CONTROL_LEASE_OWNER_UNAVAILABLE');
|
|
95
|
+
const stale = lease.generation !== manifest.generation || lease.taskId !== manifest.taskId
|
|
96
|
+
|| lease.branch !== manifest.branch || lease.expiresAt <= now || ownerState === 'dead';
|
|
94
97
|
if (!stale)
|
|
95
98
|
return false;
|
|
96
99
|
if (fs.readFileSync(filePath, 'utf8') !== raw)
|
|
@@ -125,29 +128,42 @@ export function readExecution(filePath) {
|
|
|
125
128
|
export function terminateSandboxControlExecution(execution, options = {}) {
|
|
126
129
|
const platform = options.platform ?? process.platform;
|
|
127
130
|
const processGroupId = execution.child.processGroupId;
|
|
128
|
-
const
|
|
131
|
+
const processGroupState = (groupId) => {
|
|
129
132
|
try {
|
|
130
133
|
const rows = execFileSync('ps', ['-axo', 'pgid=,stat='], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
131
134
|
return rows.split('\n').some((row) => {
|
|
132
135
|
const match = row.trim().match(/^(\d+)\s+(\S+)/);
|
|
133
136
|
return Number(match?.[1]) === groupId && !match?.[2]?.startsWith('Z');
|
|
134
|
-
});
|
|
137
|
+
}) ? 'alive' : 'dead';
|
|
135
138
|
}
|
|
136
139
|
catch {
|
|
137
140
|
try {
|
|
138
141
|
process.kill(-groupId, 0);
|
|
139
|
-
return
|
|
142
|
+
return 'alive';
|
|
140
143
|
}
|
|
141
144
|
catch (error) {
|
|
142
|
-
|
|
145
|
+
const code = error.code;
|
|
146
|
+
if (code === 'ESRCH')
|
|
147
|
+
return 'dead';
|
|
148
|
+
if (code === 'EPERM')
|
|
149
|
+
return 'alive';
|
|
150
|
+
return 'unknown';
|
|
143
151
|
}
|
|
144
152
|
}
|
|
145
153
|
};
|
|
146
|
-
const
|
|
154
|
+
const treeState = () => {
|
|
147
155
|
if (platform === 'win32' || !processGroupId) {
|
|
148
|
-
return
|
|
156
|
+
return (options.identityProbe ?? getProcessIdentityState)({
|
|
157
|
+
pid: execution.child.pid, startTime: execution.child.startTime
|
|
158
|
+
});
|
|
149
159
|
}
|
|
150
|
-
return
|
|
160
|
+
return processGroupState(processGroupId);
|
|
161
|
+
};
|
|
162
|
+
const treeAlive = () => {
|
|
163
|
+
const state = treeState();
|
|
164
|
+
if (state === 'unknown')
|
|
165
|
+
throw new Error('SANDBOX_CONTROL_EXECUTION_OWNER_UNAVAILABLE');
|
|
166
|
+
return state === 'alive';
|
|
151
167
|
};
|
|
152
168
|
if (!treeAlive())
|
|
153
169
|
return true;
|
|
@@ -14,6 +14,12 @@ const PLATFORM_DEFAULTS = Object.freeze({
|
|
|
14
14
|
darwin: ENGINES.COLIMA,
|
|
15
15
|
win32: ENGINES.WSL2
|
|
16
16
|
});
|
|
17
|
+
function isPerPlatformEngine(engine) {
|
|
18
|
+
return typeof engine === 'object' && engine !== null;
|
|
19
|
+
}
|
|
20
|
+
function resolveEngineForPlatform(engine, os) {
|
|
21
|
+
return isPerPlatformEngine(engine) ? engine[os] : engine;
|
|
22
|
+
}
|
|
17
23
|
function runFns({ runFn = run, runOkFn = runOk, runSafeFn = runSafe, runVerboseFn = runVerbose } = {}) {
|
|
18
24
|
return {
|
|
19
25
|
run: runFn,
|
|
@@ -56,23 +62,24 @@ function ensureBuildKit(engine, runOkEngineFn) {
|
|
|
56
62
|
}
|
|
57
63
|
}
|
|
58
64
|
export function validateSandboxEngine(engine, { platformFn = platform } = {}) {
|
|
59
|
-
|
|
65
|
+
const os = platformFn();
|
|
66
|
+
const resolved = resolveEngineForPlatform(engine, os);
|
|
67
|
+
if (resolved === null || resolved === undefined) {
|
|
60
68
|
return null;
|
|
61
69
|
}
|
|
62
|
-
|
|
63
|
-
if (!(engine in ADAPTERS)) {
|
|
70
|
+
if (!(resolved in ADAPTERS)) {
|
|
64
71
|
const known = Object.keys(ADAPTERS).join(', ');
|
|
65
|
-
throw new Error(`sandbox: invalid "sandbox.engine" value "${
|
|
72
|
+
throw new Error(`sandbox: invalid "sandbox.engine" value "${resolved}" (unknown sandbox engine). `
|
|
66
73
|
+ `Valid engines: ${known}.`);
|
|
67
74
|
}
|
|
68
|
-
const adapter = ADAPTERS[
|
|
75
|
+
const adapter = ADAPTERS[resolved];
|
|
69
76
|
if (!adapter.supportedPlatforms.includes(os)) {
|
|
70
77
|
const supported = enginesForPlatform(os);
|
|
71
78
|
const supportedList = supported.length > 0 ? supported.join(', ') : 'none';
|
|
72
|
-
throw new Error(`sandbox: "sandbox.engine" value "${
|
|
79
|
+
throw new Error(`sandbox: "sandbox.engine" value "${resolved}" is not supported on ${os}. `
|
|
73
80
|
+ `Supported engines on ${os}: ${supportedList}.`);
|
|
74
81
|
}
|
|
75
|
-
return
|
|
82
|
+
return resolved;
|
|
76
83
|
}
|
|
77
84
|
export function detectEngine(config = {}, { platformFn = platform } = {}) {
|
|
78
85
|
const configured = validateSandboxEngine(config.engine, { platformFn });
|
|
@@ -15,9 +15,11 @@ Commands:
|
|
|
15
15
|
rebuild [--quiet] [--refresh]
|
|
16
16
|
Rebuild the sandbox image (--refresh pulls base + tools)
|
|
17
17
|
refresh Sync host Claude Code credentials to all sandbox copies
|
|
18
|
-
rm <branch> | --unbound | --purge
|
|
19
|
-
Remove one sandbox
|
|
20
|
-
|
|
18
|
+
rm <branch | TASK-id> | --unbound | --purge
|
|
19
|
+
Remove one sandbox; use a full TASK-id for task-bound
|
|
20
|
+
cleanup and a branch for branch-only cleanup. --unbound
|
|
21
|
+
removes completed task-bound and branch-only sandboxes;
|
|
22
|
+
active, blocked, and archive tasks are protected.
|
|
21
23
|
start [--recreate] <branch | TASK-id | N>
|
|
22
24
|
Verify or recover an existing sandbox container;
|
|
23
25
|
optionally replace only the container on failure
|
|
@@ -21,6 +21,11 @@ export function removeWorktreeDir(repoRoot, worktreeBase, dir, permit, { runFn =
|
|
|
21
21
|
throw new Error(`Worktree permit target mismatch: ${dir}`);
|
|
22
22
|
}
|
|
23
23
|
verifyWorktreePermit(permit);
|
|
24
|
+
if (permit.snapshot.source === 'recovered') {
|
|
25
|
+
removeManagedDir(worktreeBase, dir);
|
|
26
|
+
runSafeFn('git', ['-C', repoRoot, 'worktree', 'prune']);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
24
29
|
try {
|
|
25
30
|
runFn('git', ['-C', repoRoot, 'worktree', 'remove', dir, '--force']);
|
|
26
31
|
}
|
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { shareBranchDir, shareCommonDir, shellConfigDirCandidates, worktreeDirCandidates } from './constants.js';
|
|
3
|
+
import { sandboxWorkspaceViewStatePaths } from './workspace-view.js';
|
|
3
4
|
export function sandboxCoreBindMounts(config, branch, overrides) {
|
|
4
5
|
const taskBound = Boolean(overrides.taskSource && overrides.taskId);
|
|
6
|
+
const workspaceMounts = sandboxWorkspaceViewStatePaths(overrides.workspaceViewRoot).map(({ state, hostPath }) => taskBound && state === 'active'
|
|
7
|
+
? {
|
|
8
|
+
hostPaths: [path.join(hostPath, '.short-ids.json')],
|
|
9
|
+
containerPath: '/workspace/.agents/workspace/active/.short-ids.json',
|
|
10
|
+
readOnly: true
|
|
11
|
+
}
|
|
12
|
+
: {
|
|
13
|
+
hostPaths: [hostPath],
|
|
14
|
+
containerPath: path.posix.join('/workspace/.agents/workspace', state),
|
|
15
|
+
readOnly: true
|
|
16
|
+
});
|
|
5
17
|
const mounts = [
|
|
6
18
|
{
|
|
7
19
|
hostPaths: overrides.worktree
|
|
@@ -10,11 +22,7 @@ export function sandboxCoreBindMounts(config, branch, overrides) {
|
|
|
10
22
|
containerPath: '/workspace',
|
|
11
23
|
readOnly: false
|
|
12
24
|
},
|
|
13
|
-
|
|
14
|
-
hostPaths: [overrides.workspaceViewRoot],
|
|
15
|
-
containerPath: '/workspace/.agents/workspace',
|
|
16
|
-
readOnly: true
|
|
17
|
-
},
|
|
25
|
+
...workspaceMounts,
|
|
18
26
|
];
|
|
19
27
|
if (taskBound) {
|
|
20
28
|
mounts.push({
|
|
@@ -47,6 +55,13 @@ export function sandboxCoreBindMounts(config, branch, overrides) {
|
|
|
47
55
|
containerPath: '/run/agent-infra/control',
|
|
48
56
|
readOnly: false
|
|
49
57
|
});
|
|
58
|
+
if (taskBound && overrides.runtimeDir) {
|
|
59
|
+
mounts.push({
|
|
60
|
+
hostPaths: [overrides.runtimeDir],
|
|
61
|
+
containerPath: '/run/agent-infra/runtime',
|
|
62
|
+
readOnly: false
|
|
63
|
+
});
|
|
64
|
+
}
|
|
50
65
|
return mounts;
|
|
51
66
|
}
|
|
52
67
|
//# sourceMappingURL=mounts.js.map
|
|
@@ -14,7 +14,7 @@ import { inspectWorktree } from './worktree-safety.js';
|
|
|
14
14
|
import { commandForEngine, runEngine, runOkEngine, runProbe, runVerboseEngine } from './shell.js';
|
|
15
15
|
import { declaredTmpfsSeedEntries, resolveTools, toolConfigDirCandidates } from './tools.js';
|
|
16
16
|
import { fetchSandboxRows, selectSandboxContainer, startSandboxContainer } from './commands/list-running.js';
|
|
17
|
-
import {
|
|
17
|
+
import { getProcessIdentityState, getProcessStartTime, removePidFileIfMatches } from '../server/process-state.js';
|
|
18
18
|
import { acquireSandboxControlBrokerStartup, garbageCollectSandboxControlRoot, isSandboxControlRootQuiescing, readSandboxControlManifest } from './control/lifecycle.js';
|
|
19
19
|
import { appendSandboxControlAudit, readSandboxControlStatus } from './control/state.js';
|
|
20
20
|
import { inspectSandboxControlContainer } from './control/container-identity.js';
|
|
@@ -89,7 +89,10 @@ export async function startSandboxControlBroker(repoRoot, manifestPath) {
|
|
|
89
89
|
if (broker === null || broker.version !== 3 || owner === null) {
|
|
90
90
|
throw new Error('SANDBOX_CONTROL_BROKER_VERSION_INVALID: rebuild the sandbox container before starting a new broker');
|
|
91
91
|
}
|
|
92
|
-
const
|
|
92
|
+
const ownerState = getProcessIdentityState({ pid: owner.pid, startTime: owner.startTime });
|
|
93
|
+
if (ownerState === 'unknown')
|
|
94
|
+
throw new Error('SANDBOX_CONTROL_BROKER_OWNER_UNAVAILABLE');
|
|
95
|
+
const live = ownerState === 'alive';
|
|
93
96
|
if (live
|
|
94
97
|
&& owner !== null
|
|
95
98
|
&& broker !== null
|
|
@@ -176,6 +179,15 @@ async function ensureSandboxControlBroker(params) {
|
|
|
176
179
|
const brokerPath = path.join(control.root, 'broker.json');
|
|
177
180
|
try {
|
|
178
181
|
const broker = JSON.parse(fs.readFileSync(brokerPath, 'utf8'));
|
|
182
|
+
const validIdentity = typeof broker.pid === 'number'
|
|
183
|
+
&& Number.isSafeInteger(broker.pid)
|
|
184
|
+
&& typeof broker.startTime === 'number'
|
|
185
|
+
&& Number.isSafeInteger(broker.startTime);
|
|
186
|
+
const ownerState = validIdentity
|
|
187
|
+
? getProcessIdentityState({ pid: broker.pid, startTime: broker.startTime })
|
|
188
|
+
: 'dead';
|
|
189
|
+
if (ownerState === 'unknown')
|
|
190
|
+
throw new Error('SANDBOX_CONTROL_BROKER_OWNER_UNAVAILABLE');
|
|
179
191
|
if (broker.version === 3
|
|
180
192
|
&& typeof broker.pid === 'number'
|
|
181
193
|
&& Number.isSafeInteger(broker.pid)
|
|
@@ -185,7 +197,7 @@ async function ensureSandboxControlBroker(params) {
|
|
|
185
197
|
&& broker.brokerId.length > 0
|
|
186
198
|
&& broker.token === validatedManifest.token
|
|
187
199
|
&& broker.generation === validatedManifest.generation
|
|
188
|
-
&&
|
|
200
|
+
&& ownerState === 'alive') {
|
|
189
201
|
try {
|
|
190
202
|
const status = readSandboxControlStatus(control.statusDir);
|
|
191
203
|
if (status.generation === validatedManifest.generation && status.broker.pid === broker.pid
|
|
@@ -207,7 +219,9 @@ async function ensureSandboxControlBroker(params) {
|
|
|
207
219
|
return;
|
|
208
220
|
}
|
|
209
221
|
}
|
|
210
|
-
catch {
|
|
222
|
+
catch (error) {
|
|
223
|
+
if (error instanceof Error && error.message === 'SANDBOX_CONTROL_BROKER_OWNER_UNAVAILABLE')
|
|
224
|
+
throw error;
|
|
211
225
|
// A missing, stale, or malformed broker record is replaced below.
|
|
212
226
|
}
|
|
213
227
|
await startSandboxControlBroker(params.config.repoRoot, control.manifestPath);
|
|
@@ -242,6 +256,30 @@ export function classifySandboxRecovery(snapshot) {
|
|
|
242
256
|
message: 'Container runtime capability signature does not match the current sandbox plan.'
|
|
243
257
|
});
|
|
244
258
|
}
|
|
259
|
+
if (snapshot.workspaceTopology === 'legacy-parent') {
|
|
260
|
+
findings.push({
|
|
261
|
+
repairKind: 'hard-failure',
|
|
262
|
+
code: 'SANDBOX_WORKSPACE_TOPOLOGY_MISMATCH',
|
|
263
|
+
message: 'Sandbox uses the legacy workspace parent-mount topology; recreate the container explicitly to migrate it.',
|
|
264
|
+
path: '/workspace/.agents/workspace'
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
if (snapshot.taskView && !snapshot.taskView.readable) {
|
|
268
|
+
findings.push({
|
|
269
|
+
repairKind: 'hard-failure',
|
|
270
|
+
code: 'SANDBOX_TASK_VIEW_UNREADABLE',
|
|
271
|
+
message: `Bound task view is not readable at ${snapshot.taskView.path}.`,
|
|
272
|
+
path: snapshot.taskView.path
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
if (snapshot.runtimeStoreOk === false) {
|
|
276
|
+
findings.push({
|
|
277
|
+
repairKind: 'hard-failure',
|
|
278
|
+
code: 'SANDBOX_RUNTIME_STORE_UNAVAILABLE',
|
|
279
|
+
message: 'Task-bound runtime store is not writable and readable by devuser.',
|
|
280
|
+
path: '/run/agent-infra/runtime'
|
|
281
|
+
});
|
|
282
|
+
}
|
|
245
283
|
for (const mountPath of snapshot.unexpectedCapabilityMounts ?? []) {
|
|
246
284
|
findings.push({
|
|
247
285
|
repairKind: 'hard-failure',
|
|
@@ -250,6 +288,9 @@ export function classifySandboxRecovery(snapshot) {
|
|
|
250
288
|
});
|
|
251
289
|
}
|
|
252
290
|
for (const mount of snapshot.mounts) {
|
|
291
|
+
if (snapshot.workspaceTopology === 'legacy-parent'
|
|
292
|
+
&& mount.path.startsWith('/workspace/.agents/workspace'))
|
|
293
|
+
continue;
|
|
253
294
|
if (mount.actualType !== mount.expectedType
|
|
254
295
|
|| !mount.sourceMatches
|
|
255
296
|
|| mount.actualRW !== mount.expectedRW
|
|
@@ -331,6 +372,20 @@ function inspectContainer(engine, container, runFn) {
|
|
|
331
372
|
}
|
|
332
373
|
return parsed[0];
|
|
333
374
|
}
|
|
375
|
+
function workspaceTopology(mountsByDestination) {
|
|
376
|
+
if (mountsByDestination.has('/workspace/.agents/workspace'))
|
|
377
|
+
return 'legacy-parent';
|
|
378
|
+
const perState = [
|
|
379
|
+
'/workspace/.agents/workspace/active',
|
|
380
|
+
'/workspace/.agents/workspace/active/.short-ids.json',
|
|
381
|
+
'/workspace/.agents/workspace/completed',
|
|
382
|
+
'/workspace/.agents/workspace/blocked',
|
|
383
|
+
'/workspace/.agents/workspace/archive'
|
|
384
|
+
];
|
|
385
|
+
return perState.some((destination) => mountsByDestination.has(destination))
|
|
386
|
+
? 'per-state'
|
|
387
|
+
: 'unknown';
|
|
388
|
+
}
|
|
334
389
|
function probe(engine, container, script, args, runOkFn, user = 'devuser') {
|
|
335
390
|
return runOkFn(engine, 'docker', [
|
|
336
391
|
'exec', '--user', user, container, 'sh', '-c', script, 'agent-infra-recovery', ...args
|
|
@@ -391,6 +446,7 @@ function expectedMounts(params) {
|
|
|
391
446
|
controlStatusDir: control.statusDir,
|
|
392
447
|
...(params.workspace.mode === 'task-bound'
|
|
393
448
|
? {
|
|
449
|
+
runtimeDir: control.runtimeDir,
|
|
394
450
|
taskSource: assertSandboxTaskSource(config.repoRoot, params.workspace.taskId),
|
|
395
451
|
taskId: params.workspace.taskId
|
|
396
452
|
}
|
|
@@ -489,6 +545,10 @@ export function collectSandboxRecoverySnapshot(params) {
|
|
|
489
545
|
const capabilityPlan = createSandboxCapabilityPlan(params.config);
|
|
490
546
|
const workspace = params.workspace ?? { mode: 'branch-only' };
|
|
491
547
|
const tools = [...capabilityPlan.tools];
|
|
548
|
+
const topology = workspaceTopology(mountsByDestination);
|
|
549
|
+
const taskViewPath = workspace.mode === 'task-bound'
|
|
550
|
+
? `/workspace/.agents/workspace/active/${workspace.taskId}/task.md`
|
|
551
|
+
: null;
|
|
492
552
|
const seeds = declaredTmpfsSeedEntries(tools).map((seed) => {
|
|
493
553
|
const mounted = mountsByDestination.get(seed.stagingPath)?.Type === 'bind';
|
|
494
554
|
return {
|
|
@@ -507,6 +567,9 @@ export function collectSandboxRecoverySnapshot(params) {
|
|
|
507
567
|
permissionsOk: probe(params.engine, params.container, 'test "$(stat -c %u:%g:%a -- "$1")" = "$(id -u devuser):$(id -g devuser):700"', [tool.containerMount], runOkFn),
|
|
508
568
|
writable: probe(params.engine, params.container, 'probe="$1/.agent-infra-ready-$$"; trap \'rm -f -- "$probe"\' EXIT; : > "$probe"', [tool.containerMount], runOkFn)
|
|
509
569
|
}));
|
|
570
|
+
const runtimeStoreOk = workspace.mode === 'task-bound'
|
|
571
|
+
? probe(params.engine, params.container, 'probe="$1/.agent-infra-runtime-$$"; trap \'rm -f -- "$probe"\' EXIT; printf runtime-probe > "$probe" && test "$(cat -- "$probe")" = runtime-probe', ['/run/agent-infra/runtime'], runOkFn)
|
|
572
|
+
: undefined;
|
|
510
573
|
const labels = inspection.Config?.Labels ?? {};
|
|
511
574
|
const branchLabel = labels[sandboxBranchLabel(params.config)];
|
|
512
575
|
const containerWorkspace = parseSandboxWorkspaceIdentity(labels, {
|
|
@@ -562,6 +625,14 @@ export function collectSandboxRecoverySnapshot(params) {
|
|
|
562
625
|
actualBranch: typeof branchLabel === 'string' ? branchLabel : null,
|
|
563
626
|
expectedWorkspace: workspace,
|
|
564
627
|
actualWorkspace: containerWorkspace,
|
|
628
|
+
workspaceTopology: topology,
|
|
629
|
+
...(taskViewPath === null ? {} : {
|
|
630
|
+
taskView: {
|
|
631
|
+
path: taskViewPath,
|
|
632
|
+
readable: probe(params.engine, params.container, 'test -r "$1"', [taskViewPath], runOkFn)
|
|
633
|
+
}
|
|
634
|
+
}),
|
|
635
|
+
...(runtimeStoreOk === undefined ? {} : { runtimeStoreOk }),
|
|
565
636
|
identityOk: typeof inspection.Id === 'string' && inspection.Id.length > 0
|
|
566
637
|
&& branchLabel === params.branch
|
|
567
638
|
&& sameSandboxWorkspaceIdentity(containerWorkspace, workspace),
|
|
@@ -677,10 +748,12 @@ function assess(params) {
|
|
|
677
748
|
}
|
|
678
749
|
function canRetryFreshReadiness(findings) {
|
|
679
750
|
return findings.length > 0 && findings.every((finding) => finding.repairKind === 'hard-failure'
|
|
751
|
+
&& finding.code !== 'SANDBOX_WORKSPACE_TOPOLOGY_MISMATCH'
|
|
680
752
|
&& finding.path !== undefined
|
|
681
753
|
&& (finding.path.startsWith('/workspace/.agents/workspace')
|
|
682
754
|
|| finding.path === '/home/devuser/.host-shell-config'
|
|
683
|
-
|| finding.path === '/home/devuser/.bash_aliases'
|
|
755
|
+
|| finding.path === '/home/devuser/.bash_aliases'
|
|
756
|
+
|| finding.path === '/run/agent-infra/runtime'));
|
|
684
757
|
}
|
|
685
758
|
export async function assertFreshSandboxReady(params) {
|
|
686
759
|
validateTmpfsSeedEntries({
|
|
@@ -891,14 +964,9 @@ export async function ensureSandboxReady(params) {
|
|
|
891
964
|
: `Sandbox recovery failed in place. Replacing only the container. ${dataBoundary}`;
|
|
892
965
|
warnings.push(warning);
|
|
893
966
|
(params.writeWarning ?? ((message) => process.stderr.write(`${message}\n`)))(warning);
|
|
894
|
-
const runVerboseFn = deps?.runVerbose ?? runVerboseEngine;
|
|
895
967
|
let replacementResult = null;
|
|
896
968
|
let replacementFailure = null;
|
|
897
969
|
try {
|
|
898
|
-
if (params.row.running) {
|
|
899
|
-
runVerboseFn(params.engine, 'docker', ['stop', params.row.name]);
|
|
900
|
-
}
|
|
901
|
-
runVerboseFn(params.engine, 'docker', ['rm', params.row.name]);
|
|
902
970
|
await params.recreate(params.branch);
|
|
903
971
|
const rows = (deps?.fetchRows ?? fetchSandboxRows)(params.engine, sandboxLabel(params.config), sandboxBranchLabel(params.config));
|
|
904
972
|
const replacement = selectSandboxContainer([...rows.running, ...rows.nonRunning], containerNameCandidates(params.config, params.branch));
|
|
@@ -4,7 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
import { isRemovedHashShortIdInput } from '../task/short-id.js';
|
|
5
5
|
const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
|
|
6
6
|
const SHORT_ID_RE = /^\d+$/;
|
|
7
|
-
const
|
|
7
|
+
export const TASK_WORKSPACE_STATES = ['active', 'completed', 'blocked', 'archive'];
|
|
8
8
|
function resolveShortIdStrict(arg, repoRoot) {
|
|
9
9
|
const scriptPath = path.join(repoRoot, '.agents', 'scripts', 'task-short-id.js');
|
|
10
10
|
if (!fs.existsSync(scriptPath)) {
|
|
@@ -20,14 +20,28 @@ function stripQuotes(value) {
|
|
|
20
20
|
return value.replace(/^(["'])(.*)\1$/, '$2');
|
|
21
21
|
}
|
|
22
22
|
function readTaskContent(repoRoot, taskId) {
|
|
23
|
-
for (const
|
|
24
|
-
const taskPath = path.join(repoRoot, '.agents', 'workspace',
|
|
23
|
+
for (const state of TASK_WORKSPACE_STATES) {
|
|
24
|
+
const taskPath = path.join(repoRoot, '.agents', 'workspace', state, taskId, 'task.md');
|
|
25
25
|
if (fs.existsSync(taskPath)) {
|
|
26
|
-
return fs.readFileSync(taskPath, 'utf8');
|
|
26
|
+
return { content: fs.readFileSync(taskPath, 'utf8'), state, taskMd: taskPath };
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
throw new Error(`Task not found: ${taskId}`);
|
|
30
30
|
}
|
|
31
|
+
function taskWorkspaceCandidates(repoRoot, taskId) {
|
|
32
|
+
return TASK_WORKSPACE_STATES.flatMap((state) => {
|
|
33
|
+
const taskMd = path.join(repoRoot, '.agents', 'workspace', state, taskId, 'task.md');
|
|
34
|
+
if (!fs.existsSync(taskMd))
|
|
35
|
+
return [];
|
|
36
|
+
const content = fs.readFileSync(taskMd, 'utf8');
|
|
37
|
+
return [{
|
|
38
|
+
taskId,
|
|
39
|
+
branch: resolveBranchFromTaskContent(content, taskId),
|
|
40
|
+
state,
|
|
41
|
+
taskMd
|
|
42
|
+
}];
|
|
43
|
+
});
|
|
44
|
+
}
|
|
31
45
|
function resolveBranchFromTaskContent(content, taskId) {
|
|
32
46
|
const frontmatterBranch = content.match(/^branch:\s*(.+)$/m);
|
|
33
47
|
if (frontmatterBranch?.[1]?.trim()) {
|
|
@@ -45,13 +59,43 @@ export function resolveTaskBranch(arg, repoRoot) {
|
|
|
45
59
|
}
|
|
46
60
|
if (SHORT_ID_RE.test(arg)) {
|
|
47
61
|
const taskId = resolveShortIdStrict(arg, repoRoot);
|
|
48
|
-
const content = readTaskContent(repoRoot, taskId);
|
|
62
|
+
const { content } = readTaskContent(repoRoot, taskId);
|
|
49
63
|
return resolveBranchFromTaskContent(content, taskId);
|
|
50
64
|
}
|
|
51
65
|
if (!TASK_ID_RE.test(arg)) {
|
|
52
66
|
return arg;
|
|
53
67
|
}
|
|
54
|
-
const content = readTaskContent(repoRoot, arg);
|
|
68
|
+
const { content } = readTaskContent(repoRoot, arg);
|
|
55
69
|
return resolveBranchFromTaskContent(content, arg);
|
|
56
70
|
}
|
|
71
|
+
export function resolveTaskWorkspace(taskId, repoRoot) {
|
|
72
|
+
if (!TASK_ID_RE.test(taskId))
|
|
73
|
+
throw new Error(`Invalid task id: ${taskId}`);
|
|
74
|
+
const matches = taskWorkspaceCandidates(repoRoot, taskId);
|
|
75
|
+
if (matches.length === 0)
|
|
76
|
+
throw new Error(`Task not found: ${taskId}`);
|
|
77
|
+
if (matches.length !== 1) {
|
|
78
|
+
throw new Error(`SANDBOX_TASK_STATE_AMBIGUOUS: task ${taskId} exists in ${matches.map((match) => match.state).join(', ')}`);
|
|
79
|
+
}
|
|
80
|
+
return matches[0];
|
|
81
|
+
}
|
|
82
|
+
export function listTaskWorkspaces(repoRoot, state) {
|
|
83
|
+
const stateRoot = path.join(repoRoot, '.agents', 'workspace', state);
|
|
84
|
+
if (!fs.existsSync(stateRoot))
|
|
85
|
+
return [];
|
|
86
|
+
return fs.readdirSync(stateRoot, { withFileTypes: true })
|
|
87
|
+
.filter((entry) => entry.isDirectory() && TASK_ID_RE.test(entry.name))
|
|
88
|
+
.flatMap((entry) => {
|
|
89
|
+
const taskMd = path.join(stateRoot, entry.name, 'task.md');
|
|
90
|
+
if (!fs.existsSync(taskMd))
|
|
91
|
+
return [];
|
|
92
|
+
const content = fs.readFileSync(taskMd, 'utf8');
|
|
93
|
+
return [{
|
|
94
|
+
taskId: entry.name,
|
|
95
|
+
branch: resolveBranchFromTaskContent(content, entry.name),
|
|
96
|
+
state,
|
|
97
|
+
taskMd
|
|
98
|
+
}];
|
|
99
|
+
});
|
|
100
|
+
}
|
|
57
101
|
//# sourceMappingURL=task-resolver.js.map
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { isRemovedHashShortIdInput } from '../task/short-id.js';
|
|
4
|
+
import { resolveTaskWorkspace } from './task-resolver.js';
|
|
4
5
|
const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
|
|
5
6
|
const SHORT_ID_RE = /^\d+$/;
|
|
6
7
|
function stripQuotes(value) {
|
|
@@ -78,6 +79,32 @@ export function resolveSandboxTarget(requestedRef, repoRoot) {
|
|
|
78
79
|
}
|
|
79
80
|
return { requestedRef, branch: requestedRef, workspace: { mode: 'branch-only' } };
|
|
80
81
|
}
|
|
82
|
+
export function resolveSandboxCleanupTarget(requestedRef, repoRoot, options = {}) {
|
|
83
|
+
if (isRemovedHashShortIdInput(requestedRef)) {
|
|
84
|
+
throw new Error(`Invalid task short id '${requestedRef}': task short ids must use bare digits`);
|
|
85
|
+
}
|
|
86
|
+
if (TASK_ID_RE.test(requestedRef)) {
|
|
87
|
+
const task = resolveTaskWorkspace(requestedRef, repoRoot);
|
|
88
|
+
if (!options.allowProtected && (task.state === 'blocked' || task.state === 'archive')) {
|
|
89
|
+
throw new Error(`SANDBOX_CLEANUP_STATE_UNSUPPORTED: task ${requestedRef} is ${task.state}`);
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
requestedRef,
|
|
93
|
+
branch: task.branch,
|
|
94
|
+
workspace: { mode: 'task-bound', taskId: task.taskId },
|
|
95
|
+
taskState: task.state
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
const target = resolveSandboxTarget(requestedRef, repoRoot);
|
|
99
|
+
return {
|
|
100
|
+
requestedRef,
|
|
101
|
+
branch: target.branch,
|
|
102
|
+
workspace: target.workspace.mode === 'task-bound'
|
|
103
|
+
? { mode: 'task-bound', taskId: target.workspace.taskId }
|
|
104
|
+
: { mode: 'branch-only' },
|
|
105
|
+
taskState: target.workspace.mode === 'task-bound' ? 'active' : 'branch-only'
|
|
106
|
+
};
|
|
107
|
+
}
|
|
81
108
|
export function parseSandboxWorkspaceIdentity(labels, keys) {
|
|
82
109
|
const mode = labels[keys.mode];
|
|
83
110
|
const taskId = labels[keys.taskId];
|