@fitlab-ai/agent-infra 0.9.5 → 0.9.7
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/dist/bin/internal-cli.js +8 -3
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +171 -20
- package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +162 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +244 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -2
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +370 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +68 -8
- package/dist/lib/agent-clients/adapters/codex.js +1 -1
- package/dist/lib/defaults.json +10 -2
- package/dist/lib/git/workflow.js +30 -4
- package/dist/lib/internal/codex-lifecycle.js +169 -3
- package/dist/lib/internal/codex-sandbox-controller.js +106 -0
- package/dist/lib/internal/release-workflow.js +108 -11
- package/dist/lib/internal/sandbox-control.js +43 -6
- package/dist/lib/internal/task-create.js +12 -4
- package/dist/lib/internal/task-orchestration.js +40 -7
- package/dist/lib/platform/context.js +6 -4
- package/dist/lib/platform/github-client.js +55 -2
- package/dist/lib/platform/issue-comments.js +2 -1
- package/dist/lib/process-data/checkpoint.js +319 -0
- package/dist/lib/process-data/index.js +128 -60
- package/dist/lib/process-data/materialize.js +84 -0
- package/dist/lib/process-data/normalize.js +34 -1
- package/dist/lib/process-data/reconcile.js +55 -1
- package/dist/lib/process-data/sources.js +422 -1
- package/dist/lib/process-data/store.js +341 -11
- package/dist/lib/release/workflow.js +29 -21
- package/dist/lib/sandbox/commands/create.js +111 -60
- package/dist/lib/sandbox/commands/prune.js +40 -16
- package/dist/lib/sandbox/commands/rm.js +372 -94
- package/dist/lib/sandbox/commands/start.js +4 -3
- package/dist/lib/sandbox/control/client.js +138 -22
- package/dist/lib/sandbox/control/container-identity.js +64 -0
- package/dist/lib/sandbox/control/executor.js +142 -0
- package/dist/lib/sandbox/control/lifecycle.js +557 -0
- package/dist/lib/sandbox/control/protocol.js +69 -26
- package/dist/lib/sandbox/control/server.js +395 -99
- package/dist/lib/sandbox/control/state.js +191 -0
- package/dist/lib/sandbox/control/timing.js +8 -0
- package/dist/lib/sandbox/index.js +2 -2
- package/dist/lib/sandbox/managed-fs.js +14 -2
- package/dist/lib/sandbox/mounts.js +22 -19
- package/dist/lib/sandbox/recovery.js +284 -50
- package/dist/lib/sandbox/workspace-view.js +90 -10
- package/dist/lib/sandbox/worktree-safety.js +196 -0
- package/dist/lib/server/daemon.js +1 -1
- package/dist/lib/server/process-control.js +17 -1
- package/dist/lib/server/process-state.js +57 -20
- package/dist/lib/task/artifact-lifecycle.js +87 -15
- package/dist/lib/task/artifact-receipts.js +128 -0
- package/dist/lib/task/codex-orchestration.js +236 -0
- package/dist/lib/task/commands/status.js +3 -0
- package/dist/lib/task/commands/validate.js +289 -0
- package/dist/lib/task/commit-finalization.js +36 -9
- package/dist/lib/task/create.js +10 -2
- package/dist/lib/task/delegation-receipts.js +109 -2
- package/dist/lib/task/events.js +149 -6
- package/dist/lib/task/finalization-artifacts.js +2 -1
- package/dist/lib/task/implementation-inputs.js +1 -7
- package/dist/lib/task/index.js +9 -0
- package/dist/lib/task/ledger.js +13 -1
- package/dist/lib/task/orchestration.js +365 -15
- package/dist/lib/task/sections.js +5 -1
- package/dist/lib/task/task-execution-lock.js +5 -4
- package/dist/lib/task/verification-engine.js +78 -14
- package/dist/lib/task/verification.js +1 -0
- package/dist/lifecycle-build-manifest.json +6 -0
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +202 -18
- package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +227 -0
- package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +321 -0
- package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -3
- package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +26 -0
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +453 -0
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +83 -6
- package/lib/agent-clients/adapters/codex.ts +1 -1
- package/lib/defaults.json +10 -2
- package/lib/git/workflow.ts +25 -3
- package/lib/internal/codex-lifecycle.ts +175 -4
- package/lib/internal/codex-sandbox-controller.ts +109 -0
- package/lib/internal/release-workflow.ts +107 -11
- package/lib/internal/sandbox-control.ts +38 -6
- package/lib/internal/task-create.ts +16 -4
- package/lib/internal/task-orchestration.ts +34 -7
- package/lib/platform/context.ts +8 -5
- package/lib/platform/github-client.ts +61 -3
- package/lib/platform/issue-comments.ts +2 -1
- package/lib/process-data/checkpoint.ts +335 -0
- package/lib/process-data/index.ts +112 -60
- package/lib/process-data/materialize.ts +83 -0
- package/lib/process-data/normalize.ts +29 -1
- package/lib/process-data/reconcile.ts +64 -2
- package/lib/process-data/sources.ts +449 -2
- package/lib/process-data/store.ts +304 -11
- package/lib/process-data/types.ts +124 -2
- package/lib/release/workflow.ts +69 -20
- package/lib/sandbox/commands/create.ts +70 -25
- package/lib/sandbox/commands/prune.ts +53 -15
- package/lib/sandbox/commands/rm.ts +423 -108
- package/lib/sandbox/commands/start.ts +4 -3
- package/lib/sandbox/control/client.ts +152 -28
- package/lib/sandbox/control/container-identity.ts +93 -0
- package/lib/sandbox/control/executor.ts +167 -0
- package/lib/sandbox/control/lifecycle.ts +573 -0
- package/lib/sandbox/control/protocol.ts +121 -68
- package/lib/sandbox/control/server.ts +398 -109
- package/lib/sandbox/control/state.ts +213 -0
- package/lib/sandbox/control/timing.ts +12 -0
- package/lib/sandbox/index.ts +2 -2
- package/lib/sandbox/managed-fs.ts +17 -2
- package/lib/sandbox/mounts.ts +16 -8
- package/lib/sandbox/recovery.ts +335 -65
- package/lib/sandbox/workspace-view.ts +102 -11
- package/lib/sandbox/worktree-safety.ts +234 -0
- package/lib/server/daemon.ts +1 -1
- package/lib/server/process-control.ts +24 -3
- package/lib/server/process-state.ts +82 -27
- package/lib/task/artifact-lifecycle.ts +78 -14
- package/lib/task/artifact-receipts.ts +161 -0
- package/lib/task/codex-orchestration.ts +319 -0
- package/lib/task/commands/status.ts +2 -0
- package/lib/task/commands/validate.ts +282 -0
- package/lib/task/commit-finalization.ts +52 -9
- package/lib/task/create.ts +11 -2
- package/lib/task/delegation-receipts.ts +208 -7
- package/lib/task/events.ts +131 -5
- package/lib/task/finalization-artifacts.ts +2 -1
- package/lib/task/implementation-inputs.ts +1 -5
- package/lib/task/index.ts +8 -0
- package/lib/task/ledger.ts +16 -1
- package/lib/task/orchestration.ts +552 -18
- package/lib/task/sections.ts +6 -2
- package/lib/task/task-execution-lock.ts +11 -9
- package/lib/task/verification-engine.ts +93 -24
- package/lib/task/verification.ts +2 -1
- package/package.json +1 -1
- package/templates/.agents/README.en.md +7 -0
- package/templates/.agents/README.zh-CN.md +5 -0
- package/templates/.agents/hooks/lifecycle-delegation.js +72 -12
- package/templates/.agents/rules/lifecycle-orchestration.en.md +8 -2
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +8 -2
- package/templates/.agents/rules/no-mid-flow-questions.en.md +1 -0
- package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +1 -0
- package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
- package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +1 -1
- package/templates/.agents/rules/review-handshake.en.md +3 -1
- package/templates/.agents/rules/review-handshake.zh-CN.md +3 -1
- package/templates/.agents/skills/code-task/reference/dual-mode.en.md +3 -1
- package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +3 -1
- package/templates/.agents/skills/complete-task/SKILL.en.md +4 -0
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +4 -0
- package/templates/.agents/skills/post-release/SKILL.en.md +32 -6
- package/templates/.agents/skills/post-release/SKILL.zh-CN.md +32 -6
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +42 -0
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +42 -0
- package/templates/.agents/skills/run-manual-validation/config/verify.en.json +25 -0
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +25 -0
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +67 -0
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +67 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +69 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +69 -0
- package/templates/.agents/skills/run-task/SKILL.en.md +9 -7
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +9 -7
- package/templates/.agents/skills/run-task/reference/host-validation.en.md +5 -3
- package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +5 -3
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +75 -3
- package/templates/.claude/commands/run-manual-validation.en.md +9 -0
- package/templates/.claude/commands/run-manual-validation.zh-CN.md +9 -0
- package/templates/.codex/hooks.json +8 -8
- package/templates/.opencode/commands/run-manual-validation.en.md +11 -0
- package/templates/.opencode/commands/run-manual-validation.zh-CN.md +11 -0
- package/templates/.agents/workspace/README.en.md +0 -26
- package/templates/.agents/workspace/README.zh-CN.md +0 -26
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { buildProcessTreeStopCommand } from '../../server/process-control.ts';
|
|
6
|
+
import { processIdentityMatches } from '../../server/process-state.ts';
|
|
7
|
+
import type { ProcessIdentity } from '../../server/process-state.ts';
|
|
8
|
+
import type {
|
|
9
|
+
SandboxControlExecution,
|
|
10
|
+
SandboxControlLease,
|
|
11
|
+
SandboxControlManifest,
|
|
12
|
+
SandboxControlStatus
|
|
13
|
+
} from './protocol.ts';
|
|
14
|
+
|
|
15
|
+
export const SANDBOX_CONTROL_AUDIT_MAX_BYTES = 1024 * 1024;
|
|
16
|
+
export const SANDBOX_CONTROL_EXECUTION_STOP_MS = 2_000;
|
|
17
|
+
|
|
18
|
+
function assertRegularFile(filePath: string): void {
|
|
19
|
+
const stat = fs.lstatSync(filePath);
|
|
20
|
+
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('SANDBOX_CONTROL_STATE_INVALID');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function atomicWriteJson(filePath: string, value: unknown, mode = 0o600, createParent = true): void {
|
|
24
|
+
if (createParent) fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
25
|
+
const temporary = path.join(path.dirname(filePath), `.${path.basename(filePath)}.${process.pid}.${randomUUID()}.tmp`);
|
|
26
|
+
try {
|
|
27
|
+
fs.writeFileSync(temporary, `${JSON.stringify(value)}\n`, { mode, flag: 'wx' });
|
|
28
|
+
fs.renameSync(temporary, filePath);
|
|
29
|
+
} finally {
|
|
30
|
+
fs.rmSync(temporary, { force: true });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function readJsonFile(filePath: string): unknown {
|
|
35
|
+
assertRegularFile(filePath);
|
|
36
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function statusPath(manifest: SandboxControlManifest): string {
|
|
40
|
+
return path.join(manifest.publicStatusDir, 'status.json');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function writeSandboxControlStatus(
|
|
44
|
+
manifest: SandboxControlManifest,
|
|
45
|
+
broker: ProcessIdentity & { brokerId: string },
|
|
46
|
+
state: SandboxControlStatus['state'],
|
|
47
|
+
reasonCode: string | null,
|
|
48
|
+
activeRequestId: string | null,
|
|
49
|
+
now = Date.now()
|
|
50
|
+
): SandboxControlStatus {
|
|
51
|
+
const status: SandboxControlStatus = {
|
|
52
|
+
version: 2, generation: manifest.generation, broker, state, reasonCode, activeRequestId, updatedAt: now
|
|
53
|
+
};
|
|
54
|
+
atomicWriteJson(statusPath(manifest), status, 0o600, false);
|
|
55
|
+
return status;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function parseSandboxControlStatus(value: unknown): SandboxControlStatus {
|
|
59
|
+
const status = value as Partial<SandboxControlStatus> | null;
|
|
60
|
+
if (!status || status.version !== 2 || typeof status.generation !== 'string'
|
|
61
|
+
|| !status.broker || !Number.isSafeInteger(status.broker.pid) || typeof status.broker.startTime !== 'number'
|
|
62
|
+
|| !Number.isSafeInteger(status.broker.startTime) || typeof status.broker.brokerId !== 'string'
|
|
63
|
+
|| status.broker.brokerId.length === 0
|
|
64
|
+
|| !['starting', 'healthy', 'busy', 'parked'].includes(status.state ?? '')
|
|
65
|
+
|| (status.reasonCode !== null && typeof status.reasonCode !== 'string')
|
|
66
|
+
|| (status.activeRequestId !== null && typeof status.activeRequestId !== 'string')
|
|
67
|
+
|| !Number.isSafeInteger(status.updatedAt)) throw new Error('SANDBOX_CONTROL_STATUS_INVALID');
|
|
68
|
+
return status as SandboxControlStatus;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function readSandboxControlStatus(directory: string): SandboxControlStatus {
|
|
72
|
+
return parseSandboxControlStatus(readJsonFile(path.join(directory, 'status.json')));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function readActiveLease(manifest: SandboxControlManifest, now = Date.now()): SandboxControlLease | null {
|
|
76
|
+
const filePath = path.join(path.dirname(manifest.publicStatusDir), 'lease.json');
|
|
77
|
+
if (!fs.existsSync(filePath)) return null;
|
|
78
|
+
const lease = readJsonFile(filePath) as Partial<SandboxControlLease> | null;
|
|
79
|
+
if (!lease || lease.version !== 2
|
|
80
|
+
|| typeof lease.nonce !== 'string' || !lease.owner || !Number.isSafeInteger(lease.owner.pid)
|
|
81
|
+
|| typeof lease.owner.startTime !== 'number' || !Number.isSafeInteger(lease.owner.startTime)
|
|
82
|
+
|| !Number.isSafeInteger(lease.issuedAt)
|
|
83
|
+
|| !Number.isSafeInteger(lease.expiresAt) || typeof lease.generation !== 'string'
|
|
84
|
+
|| (lease.taskId !== null && typeof lease.taskId !== 'string')
|
|
85
|
+
|| typeof lease.branch !== 'string' || typeof lease.reason !== 'string') {
|
|
86
|
+
throw new Error('SANDBOX_CONTROL_LEASE_INVALID');
|
|
87
|
+
}
|
|
88
|
+
if (lease.generation !== manifest.generation || lease.expiresAt! <= now
|
|
89
|
+
|| lease.taskId !== manifest.taskId || lease.branch !== manifest.branch) return null;
|
|
90
|
+
return lease as SandboxControlLease;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function cleanupStaleSandboxControlLease(manifest: SandboxControlManifest, now = Date.now()): boolean {
|
|
94
|
+
const filePath = path.join(path.dirname(manifest.publicStatusDir), 'lease.json');
|
|
95
|
+
if (!fs.existsSync(filePath)) return false;
|
|
96
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
97
|
+
const lease = JSON.parse(raw) as Partial<SandboxControlLease> | null;
|
|
98
|
+
if (!lease || lease.version !== 2
|
|
99
|
+
|| typeof lease.nonce !== 'string' || !lease.owner || !Number.isSafeInteger(lease.owner.pid)
|
|
100
|
+
|| typeof lease.owner.startTime !== 'number' || !Number.isSafeInteger(lease.owner.startTime)
|
|
101
|
+
|| !Number.isSafeInteger(lease.issuedAt)
|
|
102
|
+
|| !Number.isSafeInteger(lease.expiresAt) || typeof lease.generation !== 'string'
|
|
103
|
+
|| (lease.taskId !== null && typeof lease.taskId !== 'string')
|
|
104
|
+
|| typeof lease.branch !== 'string' || typeof lease.reason !== 'string') {
|
|
105
|
+
throw new Error('SANDBOX_CONTROL_LEASE_INVALID');
|
|
106
|
+
}
|
|
107
|
+
const stale = lease.generation !== manifest.generation || lease.taskId !== manifest.taskId
|
|
108
|
+
|| lease.branch !== manifest.branch || lease.expiresAt! <= now || !processIdentityMatches({
|
|
109
|
+
pid: lease.owner.pid!, startTime: lease.owner.startTime!
|
|
110
|
+
});
|
|
111
|
+
if (!stale) return false;
|
|
112
|
+
if (fs.readFileSync(filePath, 'utf8') !== raw) throw new Error('SANDBOX_CONTROL_LEASE_OWNERSHIP_LOST');
|
|
113
|
+
fs.unlinkSync(filePath);
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function appendSandboxControlAudit(
|
|
118
|
+
manifest: SandboxControlManifest,
|
|
119
|
+
event: string,
|
|
120
|
+
fields: Record<string, string | number | boolean | null> = {},
|
|
121
|
+
now = Date.now()
|
|
122
|
+
): void {
|
|
123
|
+
const auditPath = path.join(path.dirname(manifest.publicStatusDir), 'audit.ndjson');
|
|
124
|
+
if (fs.existsSync(auditPath) && fs.statSync(auditPath).size >= SANDBOX_CONTROL_AUDIT_MAX_BYTES) {
|
|
125
|
+
fs.rmSync(`${auditPath}.1`, { force: true });
|
|
126
|
+
fs.renameSync(auditPath, `${auditPath}.1`);
|
|
127
|
+
}
|
|
128
|
+
fs.appendFileSync(auditPath, `${JSON.stringify({ version: 1, event, generation: manifest.generation, at: now, ...fields })}\n`, {
|
|
129
|
+
encoding: 'utf8', mode: 0o600
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function executionPath(manifest: SandboxControlManifest, requestId: string): string {
|
|
134
|
+
return path.join(manifest.processingDir, requestId, 'execution.json');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function readExecution(filePath: string): SandboxControlExecution {
|
|
138
|
+
const execution = readJsonFile(filePath) as Partial<SandboxControlExecution> | null;
|
|
139
|
+
if (!execution || execution.version !== 2 || typeof execution.generation !== 'string'
|
|
140
|
+
|| typeof execution.requestId !== 'string' || typeof execution.nonce !== 'string'
|
|
141
|
+
|| !execution.child || !Number.isSafeInteger(execution.child.pid) || typeof execution.child.startTime !== 'number'
|
|
142
|
+
|| !Number.isSafeInteger(execution.child.startTime)
|
|
143
|
+
|| !['prepared', 'running', 'terminating'].includes(execution.phase ?? '')
|
|
144
|
+
|| !Number.isSafeInteger(execution.updatedAt)) throw new Error('SANDBOX_CONTROL_EXECUTION_INVALID');
|
|
145
|
+
return execution as SandboxControlExecution;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function terminateSandboxControlExecution(
|
|
149
|
+
execution: SandboxControlExecution,
|
|
150
|
+
options: {
|
|
151
|
+
platform?: NodeJS.Platform;
|
|
152
|
+
timeoutMs?: number;
|
|
153
|
+
deadlineAt?: number;
|
|
154
|
+
forceAt?: number;
|
|
155
|
+
allowForce?: boolean;
|
|
156
|
+
} = {}
|
|
157
|
+
): boolean {
|
|
158
|
+
const platform = options.platform ?? process.platform;
|
|
159
|
+
const processGroupId = execution.child.processGroupId;
|
|
160
|
+
const processGroupAlive = (groupId: number): boolean => {
|
|
161
|
+
try {
|
|
162
|
+
const rows = execFileSync('ps', ['-axo', 'pgid=,stat='], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
163
|
+
return rows.split('\n').some((row) => {
|
|
164
|
+
const match = row.trim().match(/^(\d+)\s+(\S+)/);
|
|
165
|
+
return Number(match?.[1]) === groupId && !match?.[2]?.startsWith('Z');
|
|
166
|
+
});
|
|
167
|
+
} catch {
|
|
168
|
+
try {
|
|
169
|
+
process.kill(-groupId, 0);
|
|
170
|
+
return true;
|
|
171
|
+
} catch (error) {
|
|
172
|
+
return (error as NodeJS.ErrnoException).code === 'EPERM';
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
const treeAlive = (): boolean => {
|
|
177
|
+
if (platform === 'win32' || !processGroupId) {
|
|
178
|
+
return processIdentityMatches({ pid: execution.child.pid, startTime: execution.child.startTime });
|
|
179
|
+
}
|
|
180
|
+
return processGroupAlive(processGroupId);
|
|
181
|
+
};
|
|
182
|
+
if (!treeAlive()) return true;
|
|
183
|
+
const command = buildProcessTreeStopCommand(processGroupId ?? execution.child.pid, platform);
|
|
184
|
+
try {
|
|
185
|
+
if (command.kind === 'exec') execFileSync(command.command, command.args, { stdio: 'ignore' });
|
|
186
|
+
else process.kill(command.pid, command.signal);
|
|
187
|
+
} catch {
|
|
188
|
+
// The identity check below distinguishes an already-exited tree from a failed termination.
|
|
189
|
+
}
|
|
190
|
+
const deadline = options.deadlineAt ?? (Date.now() + (options.timeoutMs ?? SANDBOX_CONTROL_EXECUTION_STOP_MS));
|
|
191
|
+
const forceAt = options.forceAt ?? (options.deadlineAt === undefined
|
|
192
|
+
? deadline
|
|
193
|
+
: Math.min(deadline, Date.now() + Math.floor(Math.max(0, deadline - Date.now()) / 2)));
|
|
194
|
+
while (Date.now() < forceAt) {
|
|
195
|
+
if (!treeAlive()) return true;
|
|
196
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
|
|
197
|
+
}
|
|
198
|
+
if (platform !== 'win32' && options.allowForce !== false
|
|
199
|
+
&& (options.deadlineAt === undefined || Date.now() < deadline)) {
|
|
200
|
+
try {
|
|
201
|
+
process.kill(processGroupId ? -processGroupId : execution.child.pid, 'SIGKILL');
|
|
202
|
+
} catch {
|
|
203
|
+
// The final identity check handles an already-exited tree.
|
|
204
|
+
}
|
|
205
|
+
const killDeadline = options.deadlineAt
|
|
206
|
+
?? (Date.now() + (options.timeoutMs ?? SANDBOX_CONTROL_EXECUTION_STOP_MS));
|
|
207
|
+
while (Date.now() < killDeadline) {
|
|
208
|
+
if (!treeAlive()) return true;
|
|
209
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return !treeAlive();
|
|
213
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type SandboxControlBackoffStep = Readonly<{
|
|
2
|
+
delayMs: number;
|
|
3
|
+
nextDelayMs: number;
|
|
4
|
+
}>;
|
|
5
|
+
|
|
6
|
+
export function nextSandboxControlBackoff(currentDelayMs: number, capMs: number): SandboxControlBackoffStep {
|
|
7
|
+
const delayMs = Math.min(currentDelayMs, capMs);
|
|
8
|
+
return {
|
|
9
|
+
delayMs,
|
|
10
|
+
nextDelayMs: Math.min(capMs, delayMs * 2)
|
|
11
|
+
};
|
|
12
|
+
}
|
package/lib/sandbox/index.ts
CHANGED
|
@@ -15,9 +15,9 @@ 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> | --
|
|
18
|
+
rm <branch> | --unbound | --purge
|
|
19
19
|
Remove one sandbox, all sandboxes not bound to an
|
|
20
|
-
active task (--
|
|
20
|
+
active task (--unbound), or tear down everything (--purge)
|
|
21
21
|
start [--recreate] <branch | TASK-id | N>
|
|
22
22
|
Verify or recover an existing sandbox container;
|
|
23
23
|
optionally replace only the container on failure
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { run, runSafe } from './shell.ts';
|
|
3
3
|
import { removeDirRecursive } from '../remove-dir.ts';
|
|
4
|
+
import { verifyWorktreePermit } from './worktree-safety.ts';
|
|
5
|
+
import type { WorktreeRemovalPermit } from './worktree-safety.ts';
|
|
4
6
|
|
|
5
7
|
export function assertManagedPath(root: string, target: string): void {
|
|
6
8
|
const resolvedRoot = path.resolve(root);
|
|
@@ -22,18 +24,31 @@ export function removeWorktreeDir(
|
|
|
22
24
|
repoRoot: string,
|
|
23
25
|
worktreeBase: string,
|
|
24
26
|
dir: string,
|
|
25
|
-
|
|
27
|
+
permit: WorktreeRemovalPermit,
|
|
28
|
+
{ runFn = run, runSafeFn = runSafe, allowRegisteredPathFallback = false }: {
|
|
26
29
|
runFn?: typeof run;
|
|
27
30
|
runSafeFn?: typeof runSafe;
|
|
31
|
+
allowRegisteredPathFallback?: boolean;
|
|
28
32
|
} = {}
|
|
29
33
|
): void {
|
|
34
|
+
assertManagedPath(worktreeBase, dir);
|
|
35
|
+
if (path.resolve(permit.snapshot.worktree) !== path.resolve(dir)) {
|
|
36
|
+
throw new Error(`Worktree permit target mismatch: ${dir}`);
|
|
37
|
+
}
|
|
38
|
+
verifyWorktreePermit(permit);
|
|
30
39
|
try {
|
|
31
40
|
runFn('git', ['-C', repoRoot, 'worktree', 'remove', dir, '--force']);
|
|
32
|
-
} catch {
|
|
41
|
+
} catch (error) {
|
|
42
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
43
|
+
const registeredPathMismatch = /(?:is not|not a valid) working tree|not a working tree/i.test(message);
|
|
44
|
+
if (!allowRegisteredPathFallback || !registeredPathMismatch) {
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
33
47
|
// On WSL2 the worktree is registered under its `/mnt/<drive>/...` path, so
|
|
34
48
|
// `git worktree remove <windows-path>` cannot match it. Delete the managed
|
|
35
49
|
// directory directly, then prune the now-dangling worktree metadata so the
|
|
36
50
|
// branch is no longer reported as checked out.
|
|
51
|
+
verifyWorktreePermit(permit);
|
|
37
52
|
removeManagedDir(worktreeBase, dir);
|
|
38
53
|
runSafeFn('git', ['-C', repoRoot, 'worktree', 'prune']);
|
|
39
54
|
}
|
package/lib/sandbox/mounts.ts
CHANGED
|
@@ -24,10 +24,12 @@ export function sandboxCoreBindMounts(
|
|
|
24
24
|
shellConfigHostDir?: string;
|
|
25
25
|
workspaceViewRoot: string;
|
|
26
26
|
controlDir: string;
|
|
27
|
+
controlStatusDir: string;
|
|
27
28
|
taskSource?: string;
|
|
28
29
|
taskId?: string;
|
|
29
30
|
}
|
|
30
31
|
): SandboxBindMountDeclaration[] {
|
|
32
|
+
const taskBound = Boolean(overrides.taskSource && overrides.taskId);
|
|
31
33
|
const mounts: SandboxBindMountDeclaration[] = [
|
|
32
34
|
{
|
|
33
35
|
hostPaths: overrides.worktree
|
|
@@ -41,6 +43,15 @@ export function sandboxCoreBindMounts(
|
|
|
41
43
|
containerPath: '/workspace/.agents/workspace',
|
|
42
44
|
readOnly: true
|
|
43
45
|
},
|
|
46
|
+
];
|
|
47
|
+
if (taskBound) {
|
|
48
|
+
mounts.push({
|
|
49
|
+
hostPaths: [overrides.taskSource!],
|
|
50
|
+
containerPath: path.posix.join('/workspace/.agents/workspace/active', overrides.taskId!),
|
|
51
|
+
readOnly: false
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
mounts.push(
|
|
44
55
|
{
|
|
45
56
|
hostPaths: [shareCommonDir(config)],
|
|
46
57
|
containerPath: '/share/common',
|
|
@@ -58,15 +69,12 @@ export function sandboxCoreBindMounts(
|
|
|
58
69
|
containerPath: '/home/devuser/.host-shell-config',
|
|
59
70
|
readOnly: true
|
|
60
71
|
}
|
|
61
|
-
|
|
62
|
-
if (overrides.taskSource && overrides.taskId) {
|
|
63
|
-
mounts.splice(2, 0, {
|
|
64
|
-
hostPaths: [overrides.taskSource],
|
|
65
|
-
containerPath: path.posix.join('/workspace/.agents/workspace/active', overrides.taskId),
|
|
66
|
-
readOnly: false
|
|
67
|
-
});
|
|
68
|
-
}
|
|
72
|
+
);
|
|
69
73
|
mounts.push({
|
|
74
|
+
hostPaths: [overrides.controlStatusDir],
|
|
75
|
+
containerPath: '/run/agent-infra/control-status',
|
|
76
|
+
readOnly: true
|
|
77
|
+
}, {
|
|
70
78
|
hostPaths: [overrides.controlDir],
|
|
71
79
|
containerPath: '/run/agent-infra/control',
|
|
72
80
|
readOnly: false
|