@fitlab-ai/agent-infra 0.9.6 → 0.9.8
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 +10 -0
- package/dist/lib/agent-clients/adapters/claude-code.js +3 -3
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +47 -5
- package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +162 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +245 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +398 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +23 -6
- package/dist/lib/decide.js +68 -14
- package/dist/lib/defaults.json +10 -2
- package/dist/lib/git/workflow.js +30 -3
- package/dist/lib/init.js +1 -1
- package/dist/lib/internal/codex-lifecycle.js +69 -1
- package/dist/lib/internal/codex-sandbox-controller.js +106 -0
- package/dist/lib/internal/release-workflow.js +1 -3
- package/dist/lib/internal/task-activity.js +57 -7
- package/dist/lib/internal/task-event.js +57 -4
- 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 +48 -8
- 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/context.js +6 -4
- package/dist/lib/platform/github-client.js +55 -2
- package/dist/lib/platform/pr-summary.js +5 -3
- 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/runtime/agent-runtime.js +56 -0
- package/dist/lib/sandbox/commands/create.js +394 -264
- package/dist/lib/sandbox/commands/rm.js +64 -15
- package/dist/lib/sandbox/commands/start.js +4 -3
- package/dist/lib/sandbox/control/container-identity.js +64 -0
- package/dist/lib/sandbox/control/executor.js +23 -9
- package/dist/lib/sandbox/control/lifecycle.js +590 -61
- package/dist/lib/sandbox/control/protocol.js +7 -0
- package/dist/lib/sandbox/control/server.js +202 -26
- package/dist/lib/sandbox/control/state.js +51 -25
- package/dist/lib/sandbox/control/timing.js +8 -0
- package/dist/lib/sandbox/engine.js +14 -7
- package/dist/lib/sandbox/mounts.js +7 -0
- package/dist/lib/sandbox/recovery.js +135 -50
- package/dist/lib/sandbox/workspace-view.js +59 -20
- package/dist/lib/server/daemon.js +1 -1
- package/dist/lib/server/process-control.js +11 -1
- package/dist/lib/server/process-state.js +107 -20
- package/dist/lib/task/activity-intent.js +4 -1
- package/dist/lib/task/artifact-lifecycle.js +82 -11
- package/dist/lib/task/artifact-receipts.js +128 -0
- package/dist/lib/task/codex-orchestration.js +81 -6
- package/dist/lib/task/commands/validate.js +1 -1
- package/dist/lib/task/create.js +10 -2
- package/dist/lib/task/decision-intents.js +4 -2
- package/dist/lib/task/delegation-receipts.js +120 -17
- package/dist/lib/task/events.js +148 -7
- 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/ledger.js +13 -1
- package/dist/lib/task/lifecycle.js +69 -14
- package/dist/lib/task/orchestration.js +293 -18
- package/dist/lib/task/review-finalization.js +3 -2
- 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 +80 -22
- package/dist/lib/task/verification.js +13 -1
- package/dist/lib/task/workflow-warning-intents.js +4 -2
- package/dist/lib/task/write.js +2 -1
- package/dist/lifecycle-build-manifest.json +6 -0
- package/lib/agent-clients/adapters/claude-code.ts +3 -3
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +62 -4
- package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +227 -0
- package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +322 -0
- package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +26 -0
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +480 -0
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +35 -5
- package/lib/decide.ts +59 -14
- package/lib/defaults.json +10 -2
- package/lib/git/workflow.ts +25 -2
- package/lib/init.ts +1 -1
- package/lib/internal/codex-lifecycle.ts +68 -1
- package/lib/internal/codex-sandbox-controller.ts +109 -0
- package/lib/internal/release-workflow.ts +1 -3
- package/lib/internal/task-activity.ts +46 -6
- package/lib/internal/task-event.ts +53 -4
- package/lib/internal/task-ledger.ts +48 -4
- package/lib/internal/task-lifecycle.ts +56 -12
- package/lib/internal/task-orchestration.ts +43 -8
- 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/context.ts +8 -5
- package/lib/platform/github-client.ts +61 -3
- package/lib/platform/pr-summary.ts +10 -3
- 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/runtime/agent-runtime.ts +76 -0
- package/lib/sandbox/commands/create.ts +139 -6
- package/lib/sandbox/commands/rm.ts +64 -14
- package/lib/sandbox/commands/start.ts +4 -3
- package/lib/sandbox/config.ts +2 -1
- package/lib/sandbox/control/container-identity.ts +93 -0
- package/lib/sandbox/control/executor.ts +24 -12
- package/lib/sandbox/control/lifecycle.ts +685 -67
- package/lib/sandbox/control/protocol.ts +35 -6
- package/lib/sandbox/control/server.ts +184 -24
- package/lib/sandbox/control/state.ts +67 -33
- package/lib/sandbox/control/timing.ts +12 -0
- package/lib/sandbox/engine.ts +20 -9
- package/lib/sandbox/mounts.ts +8 -0
- package/lib/sandbox/recovery.ts +159 -49
- package/lib/sandbox/workspace-view.ts +73 -22
- package/lib/server/daemon.ts +1 -1
- package/lib/server/process-control.ts +13 -3
- package/lib/server/process-state.ts +129 -27
- package/lib/task/activity-intent.ts +4 -2
- package/lib/task/artifact-lifecycle.ts +73 -10
- package/lib/task/artifact-receipts.ts +161 -0
- package/lib/task/codex-orchestration.ts +91 -5
- package/lib/task/commands/validate.ts +1 -1
- package/lib/task/create.ts +11 -2
- package/lib/task/decision-intents.ts +4 -2
- package/lib/task/delegation-receipts.ts +205 -25
- package/lib/task/events.ts +132 -6
- 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/ledger.ts +16 -1
- package/lib/task/lifecycle.ts +71 -14
- package/lib/task/orchestration.ts +443 -22
- package/lib/task/review-finalization.ts +6 -2
- package/lib/task/sections.ts +6 -2
- package/lib/task/task-execution-lock.ts +11 -9
- package/lib/task/verification-engine.ts +93 -32
- package/lib/task/verification.ts +15 -1
- package/lib/task/workflow-warning-intents.ts +4 -2
- package/lib/task/write.ts +4 -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 +33 -4
- package/templates/.agents/rules/lifecycle-orchestration.en.md +10 -3
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +10 -3
- 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/run-manual-validation/SKILL.en.md +6 -6
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +6 -6
- package/templates/.agents/skills/run-manual-validation/config/verify.en.json +14 -3
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +14 -3
- 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 +25 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +25 -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 -1
- package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +5 -1
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +75 -3
- package/templates/.agents/workspace/README.en.md +0 -26
- package/templates/.agents/workspace/README.zh-CN.md +0 -26
|
@@ -1,12 +1,38 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
4
5
|
import { buildProcessTreeStopCommand } from '../../server/process-control.js';
|
|
5
|
-
import {
|
|
6
|
+
import { getProcessIdentityState, getProcessStartTime } from '../../server/process-state.js';
|
|
7
|
+
import { DEFAULT_SANDBOX_CONTROL_TIMING } from './protocol.js';
|
|
8
|
+
import { inspectSandboxControlContainer } from './container-identity.js';
|
|
6
9
|
import { parseSandboxControlStatus, readExecution, terminateSandboxControlExecution } from './state.js';
|
|
7
|
-
const DEFAULT_QUIESCE_TIMEOUT_MS =
|
|
10
|
+
const DEFAULT_QUIESCE_TIMEOUT_MS = DEFAULT_SANDBOX_CONTROL_TIMING.quiesceDeadlineMs;
|
|
8
11
|
const QUIESCING_FILE = 'quiescing.json';
|
|
9
12
|
const BROKER_STARTING_FILE = 'broker-starting.json';
|
|
13
|
+
const REPLACEMENT_FILE = 'replacement.json';
|
|
14
|
+
const REPLACEMENT_STATE_SUFFIX = '.replacement-state.json';
|
|
15
|
+
async function awaitWithDeadline(operation, deadlineAt, errorCode) {
|
|
16
|
+
const remaining = deadlineAt - Date.now();
|
|
17
|
+
if (remaining <= 0)
|
|
18
|
+
throw new Error(errorCode);
|
|
19
|
+
let timer;
|
|
20
|
+
try {
|
|
21
|
+
return await Promise.race([
|
|
22
|
+
operation(),
|
|
23
|
+
new Promise((_, reject) => {
|
|
24
|
+
timer = setTimeout(() => reject(new Error(errorCode)), remaining);
|
|
25
|
+
})
|
|
26
|
+
]);
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
if (timer)
|
|
30
|
+
clearTimeout(timer);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function forceDeadline(deadlineAt, timeoutMs) {
|
|
34
|
+
return Math.max(Date.now(), deadlineAt - Math.max(1, Math.floor(timeoutMs / 2)));
|
|
35
|
+
}
|
|
10
36
|
function regularFile(filePath) {
|
|
11
37
|
try {
|
|
12
38
|
const stat = fs.lstatSync(filePath);
|
|
@@ -44,8 +70,8 @@ function readStartupOwner(filePath) {
|
|
|
44
70
|
const raw = fs.readFileSync(filePath, 'utf8');
|
|
45
71
|
try {
|
|
46
72
|
const record = JSON.parse(raw);
|
|
47
|
-
if (record.version !==
|
|
48
|
-
|| typeof record.startTime !== '
|
|
73
|
+
if (record.version !== 2 || !Number.isSafeInteger(record.pid) || (record.pid ?? 0) <= 0
|
|
74
|
+
|| typeof record.startTime !== 'number' || !Number.isSafeInteger(record.startTime))
|
|
49
75
|
return null;
|
|
50
76
|
return { raw, owner: { pid: record.pid, startTime: record.startTime } };
|
|
51
77
|
}
|
|
@@ -53,14 +79,14 @@ function readStartupOwner(filePath) {
|
|
|
53
79
|
return null;
|
|
54
80
|
}
|
|
55
81
|
}
|
|
56
|
-
async function waitForStartupTransition(root, timeoutMs) {
|
|
82
|
+
async function waitForStartupTransition(root, timeoutMs, identityProbe = getProcessIdentityState) {
|
|
57
83
|
const transitionPath = path.join(root, BROKER_STARTING_FILE);
|
|
58
84
|
const deadline = Date.now() + timeoutMs;
|
|
59
85
|
while (Date.now() < deadline) {
|
|
60
86
|
const transition = readStartupOwner(transitionPath);
|
|
61
87
|
if (!fs.existsSync(transitionPath))
|
|
62
88
|
return;
|
|
63
|
-
if (transition && !ownerLive(transition.owner)) {
|
|
89
|
+
if (transition && !ownerLive(transition.owner, identityProbe)) {
|
|
64
90
|
try {
|
|
65
91
|
if (fs.readFileSync(transitionPath, 'utf8') === transition.raw)
|
|
66
92
|
fs.unlinkSync(transitionPath);
|
|
@@ -74,10 +100,10 @@ async function waitForStartupTransition(root, timeoutMs) {
|
|
|
74
100
|
}
|
|
75
101
|
throw new Error('SANDBOX_CONTROL_BROKER_START_TRANSITION');
|
|
76
102
|
}
|
|
77
|
-
export async function acquireSandboxControlBrokerStartup(root, owner, timeoutMs = 5_000) {
|
|
103
|
+
export async function acquireSandboxControlBrokerStartup(root, owner, timeoutMs = 5_000, options = {}) {
|
|
78
104
|
const resolvedRoot = path.resolve(root);
|
|
79
105
|
const transitionPath = path.join(resolvedRoot, BROKER_STARTING_FILE);
|
|
80
|
-
const record = `${JSON.stringify({ version:
|
|
106
|
+
const record = `${JSON.stringify({ version: 2, ...owner })}\n`;
|
|
81
107
|
const deadline = Date.now() + timeoutMs;
|
|
82
108
|
while (Date.now() < deadline) {
|
|
83
109
|
if (isSandboxControlRootQuiescing(resolvedRoot))
|
|
@@ -108,7 +134,7 @@ export async function acquireSandboxControlBrokerStartup(root, owner, timeoutMs
|
|
|
108
134
|
if (error.code !== 'EEXIST')
|
|
109
135
|
throw error;
|
|
110
136
|
const transition = readStartupOwner(transitionPath);
|
|
111
|
-
if (transition && !ownerLive(transition.owner)) {
|
|
137
|
+
if (transition && !ownerLive(transition.owner, options.identityProbe)) {
|
|
112
138
|
try {
|
|
113
139
|
if (fs.readFileSync(transitionPath, 'utf8') === transition.raw)
|
|
114
140
|
fs.unlinkSync(transitionPath);
|
|
@@ -123,34 +149,342 @@ export async function acquireSandboxControlBrokerStartup(root, owner, timeoutMs
|
|
|
123
149
|
}
|
|
124
150
|
throw new Error('SANDBOX_CONTROL_BROKER_START_TRANSITION');
|
|
125
151
|
}
|
|
126
|
-
|
|
152
|
+
function replacementPath(root) {
|
|
153
|
+
return path.join(root, REPLACEMENT_FILE);
|
|
154
|
+
}
|
|
155
|
+
function replacementStatePath(root) {
|
|
156
|
+
const resolvedRoot = path.resolve(root);
|
|
157
|
+
return path.join(path.dirname(resolvedRoot), `.${path.basename(resolvedRoot)}${REPLACEMENT_STATE_SUFFIX}`);
|
|
158
|
+
}
|
|
159
|
+
function replacementSnapshotPath(root, transitionId) {
|
|
160
|
+
const resolvedRoot = path.resolve(root);
|
|
161
|
+
return path.join(path.dirname(resolvedRoot), `.${path.basename(resolvedRoot)}.replacement-${transitionId}.snapshot`);
|
|
162
|
+
}
|
|
163
|
+
function writeAtomicText(filePath, content) {
|
|
164
|
+
const temporary = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
165
|
+
try {
|
|
166
|
+
fs.writeFileSync(temporary, content, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
167
|
+
fs.renameSync(temporary, filePath);
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
fs.rmSync(temporary, { force: true });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function readReplacementState(root) {
|
|
174
|
+
const filePath = replacementStatePath(root);
|
|
175
|
+
if (!fs.existsSync(filePath))
|
|
176
|
+
return null;
|
|
177
|
+
if (!regularFile(filePath))
|
|
178
|
+
throw new Error('SANDBOX_CONTROL_REPLACEMENT_RECOVERY_INVALID');
|
|
179
|
+
try {
|
|
180
|
+
const value = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
181
|
+
const resolvedRoot = path.resolve(root);
|
|
182
|
+
if (value.version !== 1 || (value.phase !== 'prepared' && value.phase !== 'committed')
|
|
183
|
+
|| value.root !== resolvedRoot || typeof value.transitionId !== 'string' || value.transitionId.length === 0
|
|
184
|
+
|| value.snapshotRoot !== replacementSnapshotPath(resolvedRoot, value.transitionId)) {
|
|
185
|
+
throw new Error('invalid replacement state');
|
|
186
|
+
}
|
|
187
|
+
return value;
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
throw new Error('SANDBOX_CONTROL_REPLACEMENT_RECOVERY_INVALID');
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
function writeReplacementState(state) {
|
|
194
|
+
writeAtomicText(replacementStatePath(state.root), `${JSON.stringify(state)}\n`);
|
|
195
|
+
}
|
|
196
|
+
function removeReplacementState(root) {
|
|
197
|
+
fs.rmSync(replacementStatePath(root), { force: true });
|
|
198
|
+
}
|
|
199
|
+
function readReplacementRecord(filePath) {
|
|
200
|
+
if (!fs.existsSync(filePath))
|
|
201
|
+
return null;
|
|
202
|
+
if (!regularFile(filePath))
|
|
203
|
+
throw new Error('SANDBOX_CONTROL_REPLACEMENT_INVALID');
|
|
204
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
205
|
+
try {
|
|
206
|
+
const value = JSON.parse(raw);
|
|
207
|
+
if (value.version !== 1 || typeof value.transitionId !== 'string' || value.transitionId.length === 0
|
|
208
|
+
|| !Number.isSafeInteger(value.pid) || (value.pid ?? 0) <= 0
|
|
209
|
+
|| !Number.isSafeInteger(value.startTime))
|
|
210
|
+
throw new Error('invalid replacement');
|
|
211
|
+
return { raw, record: value };
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
throw new Error('SANDBOX_CONTROL_REPLACEMENT_INVALID');
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
export function acquireSandboxControlReplacement(root, options = {}) {
|
|
218
|
+
const resolvedRoot = path.resolve(root);
|
|
219
|
+
if (fs.existsSync(resolvedRoot)) {
|
|
220
|
+
const stat = fs.lstatSync(resolvedRoot);
|
|
221
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
222
|
+
throw new Error('SANDBOX_CONTROL_CHANNEL_INVALID');
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
fs.mkdirSync(resolvedRoot, { recursive: true, mode: 0o700 });
|
|
226
|
+
}
|
|
227
|
+
const startTime = options.owner?.startTime ?? getProcessStartTime(process.pid);
|
|
228
|
+
if (startTime === null || startTime === undefined)
|
|
229
|
+
throw new Error('SANDBOX_CONTROL_REPLACEMENT_OWNER_UNAVAILABLE');
|
|
230
|
+
const owner = options.owner ?? { pid: process.pid, startTime };
|
|
231
|
+
const transitionId = randomUUID();
|
|
232
|
+
const record = `${JSON.stringify({ version: 1, transitionId, ...owner })}\n`;
|
|
233
|
+
const filePath = replacementPath(resolvedRoot);
|
|
234
|
+
const existing = readReplacementRecord(filePath);
|
|
235
|
+
if (existing) {
|
|
236
|
+
const ownerState = (options.probeOwner ?? getProcessIdentityState)(existing.record);
|
|
237
|
+
if (ownerState === 'alive') {
|
|
238
|
+
throw new Error('SANDBOX_CONTROL_REPLACEMENT_BUSY');
|
|
239
|
+
}
|
|
240
|
+
if (ownerState === 'unknown') {
|
|
241
|
+
throw new Error('SANDBOX_CONTROL_REPLACEMENT_OWNER_UNAVAILABLE');
|
|
242
|
+
}
|
|
243
|
+
if (fs.readFileSync(filePath, 'utf8') !== existing.raw) {
|
|
244
|
+
throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
|
|
245
|
+
}
|
|
246
|
+
fs.unlinkSync(filePath);
|
|
247
|
+
}
|
|
248
|
+
try {
|
|
249
|
+
fs.writeFileSync(filePath, record, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
if (error.code === 'EEXIST') {
|
|
253
|
+
throw new Error('SANDBOX_CONTROL_REPLACEMENT_BUSY');
|
|
254
|
+
}
|
|
255
|
+
throw error;
|
|
256
|
+
}
|
|
257
|
+
let released = false;
|
|
258
|
+
const verifyOwnership = () => {
|
|
259
|
+
if (released)
|
|
260
|
+
throw new Error('SANDBOX_CONTROL_REPLACEMENT_RELEASED');
|
|
261
|
+
const current = readReplacementRecord(filePath);
|
|
262
|
+
if (!current || current.raw !== record || current.record.transitionId !== transitionId) {
|
|
263
|
+
throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
return {
|
|
267
|
+
root: resolvedRoot,
|
|
268
|
+
transitionId,
|
|
269
|
+
owner,
|
|
270
|
+
assertOwned: verifyOwnership,
|
|
271
|
+
clearQuiescing() {
|
|
272
|
+
verifyOwnership();
|
|
273
|
+
const markerPath = path.join(resolvedRoot, QUIESCING_FILE);
|
|
274
|
+
if (fs.existsSync(markerPath)) {
|
|
275
|
+
if (!regularFile(markerPath))
|
|
276
|
+
throw new Error('SANDBOX_CONTROL_CHANNEL_INVALID');
|
|
277
|
+
fs.unlinkSync(markerPath);
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
release() {
|
|
281
|
+
if (released)
|
|
282
|
+
return;
|
|
283
|
+
verifyOwnership();
|
|
284
|
+
fs.unlinkSync(filePath);
|
|
285
|
+
released = true;
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
function readReplacementRaw(root) {
|
|
290
|
+
const current = readReplacementRecord(replacementPath(root));
|
|
291
|
+
if (!current)
|
|
292
|
+
throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
|
|
293
|
+
return current.raw;
|
|
294
|
+
}
|
|
295
|
+
export function beginSandboxControlReplacement(root, lease) {
|
|
296
|
+
const resolvedRoot = path.resolve(root);
|
|
297
|
+
if (path.resolve(lease.root) !== resolvedRoot)
|
|
298
|
+
throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
|
|
299
|
+
lease.assertOwned();
|
|
300
|
+
if (!fs.existsSync(resolvedRoot))
|
|
301
|
+
throw new Error('SANDBOX_CONTROL_CHANNEL_INVALID');
|
|
302
|
+
const rootStat = fs.lstatSync(resolvedRoot);
|
|
303
|
+
if (!rootStat.isDirectory() || rootStat.isSymbolicLink())
|
|
304
|
+
throw new Error('SANDBOX_CONTROL_CHANNEL_INVALID');
|
|
305
|
+
if (readReplacementState(resolvedRoot))
|
|
306
|
+
throw new Error('SANDBOX_CONTROL_REPLACEMENT_RECOVERY_REQUIRED');
|
|
307
|
+
const snapshotRoot = replacementSnapshotPath(resolvedRoot, lease.transitionId);
|
|
308
|
+
if (fs.existsSync(snapshotRoot))
|
|
309
|
+
throw new Error('SANDBOX_CONTROL_REPLACEMENT_RECOVERY_INVALID');
|
|
310
|
+
const replacementRaw = readReplacementRaw(resolvedRoot);
|
|
311
|
+
const state = {
|
|
312
|
+
version: 1,
|
|
313
|
+
phase: 'prepared',
|
|
314
|
+
root: resolvedRoot,
|
|
315
|
+
snapshotRoot,
|
|
316
|
+
transitionId: lease.transitionId
|
|
317
|
+
};
|
|
318
|
+
writeReplacementState(state);
|
|
319
|
+
let moved = false;
|
|
320
|
+
try {
|
|
321
|
+
fs.renameSync(resolvedRoot, snapshotRoot);
|
|
322
|
+
moved = true;
|
|
323
|
+
fs.mkdirSync(resolvedRoot, { recursive: true, mode: 0o700 });
|
|
324
|
+
fs.writeFileSync(replacementPath(resolvedRoot), replacementRaw, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
325
|
+
markSandboxControlRootQuiescing(resolvedRoot);
|
|
326
|
+
return { root: resolvedRoot, snapshotRoot, transitionId: lease.transitionId };
|
|
327
|
+
}
|
|
328
|
+
catch (error) {
|
|
329
|
+
if (moved) {
|
|
330
|
+
fs.rmSync(resolvedRoot, { recursive: true, force: true });
|
|
331
|
+
fs.renameSync(snapshotRoot, resolvedRoot);
|
|
332
|
+
}
|
|
333
|
+
removeReplacementState(resolvedRoot);
|
|
334
|
+
throw error;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
function isRecoverableCutoverManifestError(error) {
|
|
338
|
+
return error instanceof Error
|
|
339
|
+
&& (error.message.startsWith('SANDBOX_CONTROL_MANIFEST_')
|
|
340
|
+
|| error.message === 'SANDBOX_CONTROL_OWNER_EVIDENCE_MISSING');
|
|
341
|
+
}
|
|
342
|
+
export async function recoverSandboxControlReplacement(root, lease) {
|
|
343
|
+
const resolvedRoot = path.resolve(root);
|
|
344
|
+
if (path.resolve(lease.root) !== resolvedRoot)
|
|
345
|
+
throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
|
|
346
|
+
const state = readReplacementState(resolvedRoot);
|
|
347
|
+
if (!state)
|
|
348
|
+
return 'none';
|
|
349
|
+
lease.assertOwned();
|
|
350
|
+
if (state.phase === 'committed') {
|
|
351
|
+
fs.rmSync(state.snapshotRoot, { recursive: true, force: true });
|
|
352
|
+
removeReplacementState(resolvedRoot);
|
|
353
|
+
return 'committed';
|
|
354
|
+
}
|
|
355
|
+
if (!fs.existsSync(state.snapshotRoot)) {
|
|
356
|
+
removeReplacementState(resolvedRoot);
|
|
357
|
+
return 'none';
|
|
358
|
+
}
|
|
359
|
+
if (fs.existsSync(resolvedRoot) && fs.existsSync(path.join(resolvedRoot, 'manifest.json'))) {
|
|
360
|
+
try {
|
|
361
|
+
await quiesceSandboxControlRoot(resolvedRoot);
|
|
362
|
+
}
|
|
363
|
+
catch (error) {
|
|
364
|
+
if (!isRecoverableCutoverManifestError(error))
|
|
365
|
+
throw error;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
lease.assertOwned();
|
|
369
|
+
const replacementRaw = readReplacementRaw(resolvedRoot);
|
|
370
|
+
fs.rmSync(resolvedRoot, { recursive: true, force: true });
|
|
371
|
+
fs.renameSync(state.snapshotRoot, resolvedRoot);
|
|
372
|
+
writeAtomicText(replacementPath(resolvedRoot), replacementRaw);
|
|
373
|
+
removeReplacementState(resolvedRoot);
|
|
374
|
+
return 'restored';
|
|
375
|
+
}
|
|
376
|
+
export function commitSandboxControlReplacement(root, lease) {
|
|
377
|
+
const resolvedRoot = path.resolve(root);
|
|
378
|
+
if (path.resolve(lease.root) !== resolvedRoot)
|
|
379
|
+
throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
|
|
380
|
+
const state = readReplacementState(resolvedRoot);
|
|
381
|
+
if (!state)
|
|
382
|
+
return;
|
|
383
|
+
lease.assertOwned();
|
|
384
|
+
writeReplacementState({ ...state, phase: 'committed' });
|
|
385
|
+
fs.rmSync(state.snapshotRoot, { recursive: true, force: true });
|
|
386
|
+
removeReplacementState(resolvedRoot);
|
|
387
|
+
}
|
|
388
|
+
function readOptionalRaw(filePath) {
|
|
389
|
+
if (!fs.existsSync(filePath))
|
|
390
|
+
return null;
|
|
391
|
+
if (!regularFile(filePath))
|
|
392
|
+
throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
|
|
393
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
394
|
+
}
|
|
395
|
+
function readProcessingSnapshot(root) {
|
|
396
|
+
const processingDir = path.join(root, 'processing');
|
|
397
|
+
if (!fs.existsSync(processingDir))
|
|
398
|
+
return [];
|
|
399
|
+
const stat = fs.lstatSync(processingDir);
|
|
400
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
401
|
+
throw new Error('SANDBOX_CONTROL_EXECUTION_STILL_RUNNING');
|
|
402
|
+
return fs.readdirSync(processingDir, { withFileTypes: true })
|
|
403
|
+
.map((entry) => ({
|
|
404
|
+
name: entry.name,
|
|
405
|
+
directory: entry.isDirectory() && !entry.isSymbolicLink(),
|
|
406
|
+
executionRaw: entry.isDirectory() && !entry.isSymbolicLink()
|
|
407
|
+
? readOptionalRaw(path.join(processingDir, entry.name, 'execution.json'))
|
|
408
|
+
: null
|
|
409
|
+
}))
|
|
410
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
411
|
+
}
|
|
412
|
+
export function captureSandboxControlCutoverSnapshot(root) {
|
|
413
|
+
const resolvedRoot = path.resolve(root);
|
|
414
|
+
const manifestPath = path.join(resolvedRoot, 'manifest.json');
|
|
415
|
+
if (fs.existsSync(manifestPath))
|
|
416
|
+
readSandboxControlManifestForTransition(manifestPath);
|
|
417
|
+
return {
|
|
418
|
+
root: resolvedRoot,
|
|
419
|
+
manifestRaw: readOptionalRaw(manifestPath),
|
|
420
|
+
brokerRaw: readOptionalRaw(path.join(resolvedRoot, 'broker.json')),
|
|
421
|
+
statusRaw: readOptionalRaw(path.join(resolvedRoot, 'public', 'status.json')),
|
|
422
|
+
processing: readProcessingSnapshot(resolvedRoot)
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
export function assertSandboxControlCutoverSnapshot(snapshot) {
|
|
426
|
+
const current = captureSandboxControlCutoverSnapshot(snapshot.root);
|
|
427
|
+
if (JSON.stringify(current) !== JSON.stringify(snapshot)) {
|
|
428
|
+
throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
function readSandboxControlManifestValue(manifestPath, allowLegacy) {
|
|
127
432
|
if (!regularFile(manifestPath))
|
|
128
433
|
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
129
434
|
let manifest;
|
|
130
435
|
try {
|
|
131
|
-
|
|
436
|
+
const parsed = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
437
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
438
|
+
throw new Error('invalid manifest');
|
|
439
|
+
}
|
|
440
|
+
manifest = parsed;
|
|
132
441
|
}
|
|
133
442
|
catch {
|
|
134
443
|
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
135
444
|
}
|
|
136
|
-
|
|
137
|
-
|
|
445
|
+
const candidate = manifest;
|
|
446
|
+
if (candidate.version !== 4 && candidate.version !== 5) {
|
|
447
|
+
throw new Error('SANDBOX_CONTROL_MANIFEST_VERSION_INVALID: expected version 5; container-only recreation is required');
|
|
138
448
|
}
|
|
139
|
-
if (typeof
|
|
140
|
-
||
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|| typeof
|
|
145
|
-
|| typeof
|
|
449
|
+
if (typeof candidate.containerIdentity !== 'object' || candidate.containerIdentity === null
|
|
450
|
+
|| Array.isArray(candidate.containerIdentity))
|
|
451
|
+
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
452
|
+
const containerIdentity = candidate.containerIdentity;
|
|
453
|
+
if (typeof candidate.engine !== 'string' || candidate.engine.length === 0
|
|
454
|
+
|| typeof candidate.repoRoot !== 'string' || typeof candidate.worktreeRoot !== 'string'
|
|
455
|
+
|| typeof candidate.project !== 'string' || typeof candidate.container !== 'string'
|
|
456
|
+
|| typeof containerIdentity.id !== 'string' || containerIdentity.id.length === 0
|
|
457
|
+
|| typeof containerIdentity.labels !== 'object' || containerIdentity.labels === null
|
|
458
|
+
|| Array.isArray(containerIdentity.labels)
|
|
459
|
+
|| Object.values(containerIdentity.labels).some((value) => typeof value !== 'string')
|
|
460
|
+
|| typeof candidate.branch !== 'string' || !['task-bound', 'branch-only'].includes(candidate.mode ?? '')
|
|
461
|
+
|| (candidate.taskId !== null && typeof candidate.taskId !== 'string')
|
|
462
|
+
|| typeof candidate.channelDir !== 'string' || typeof candidate.publicStatusDir !== 'string'
|
|
463
|
+
|| typeof candidate.processingDir !== 'string' || typeof candidate.token !== 'string'
|
|
464
|
+
|| typeof candidate.generation !== 'string')
|
|
146
465
|
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
147
466
|
const root = path.dirname(path.resolve(manifestPath));
|
|
148
|
-
if (path.resolve(
|
|
149
|
-
|| path.resolve(
|
|
150
|
-
|| path.resolve(
|
|
467
|
+
if (path.resolve(candidate.channelDir) !== path.join(root, 'channel')
|
|
468
|
+
|| path.resolve(candidate.publicStatusDir) !== path.join(root, 'public')
|
|
469
|
+
|| path.resolve(candidate.processingDir) !== path.join(root, 'processing')) {
|
|
470
|
+
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
471
|
+
}
|
|
472
|
+
if (candidate.version === 4) {
|
|
473
|
+
if (!allowLegacy) {
|
|
474
|
+
throw new Error('SANDBOX_CONTROL_MANIFEST_VERSION_INVALID: expected version 5; container-only recreation is required');
|
|
475
|
+
}
|
|
476
|
+
return candidate;
|
|
477
|
+
}
|
|
478
|
+
if (typeof candidate.runtimeDir !== 'string' || path.resolve(candidate.runtimeDir) !== path.join(root, 'runtime')) {
|
|
151
479
|
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
152
480
|
}
|
|
153
|
-
return
|
|
481
|
+
return candidate;
|
|
482
|
+
}
|
|
483
|
+
export function readSandboxControlManifest(manifestPath) {
|
|
484
|
+
return readSandboxControlManifestValue(manifestPath, false);
|
|
485
|
+
}
|
|
486
|
+
export function readSandboxControlManifestForTransition(manifestPath) {
|
|
487
|
+
return readSandboxControlManifestValue(manifestPath, true);
|
|
154
488
|
}
|
|
155
489
|
function readBrokerOwner(filePath) {
|
|
156
490
|
if (!fs.existsSync(filePath))
|
|
@@ -164,9 +498,10 @@ function readBrokerOwner(filePath) {
|
|
|
164
498
|
catch {
|
|
165
499
|
throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
166
500
|
}
|
|
167
|
-
if (!owner || owner.version !==
|
|
168
|
-
|| typeof owner.startTime !== '
|
|
169
|
-
|| typeof owner.
|
|
501
|
+
if (!owner || owner.version !== 3 || !Number.isSafeInteger(owner.pid) || (owner.pid ?? 0) <= 0
|
|
502
|
+
|| typeof owner.startTime !== 'number' || !Number.isSafeInteger(owner.startTime)
|
|
503
|
+
|| typeof owner.brokerId !== 'string' || owner.brokerId.length === 0
|
|
504
|
+
|| typeof owner.token !== 'string' || typeof owner.generation !== 'string')
|
|
170
505
|
throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
171
506
|
return owner;
|
|
172
507
|
}
|
|
@@ -183,10 +518,14 @@ function readStatusOwner(filePath) {
|
|
|
183
518
|
}
|
|
184
519
|
}
|
|
185
520
|
function sameOwner(left, right) {
|
|
186
|
-
return left.pid === right.pid && left.startTime === right.startTime
|
|
521
|
+
return left.pid === right.pid && left.startTime === right.startTime
|
|
522
|
+
&& (!left.brokerId || !right.brokerId || left.brokerId === right.brokerId);
|
|
187
523
|
}
|
|
188
|
-
function ownerLive(owner) {
|
|
189
|
-
|
|
524
|
+
function ownerLive(owner, identityProbe = getProcessIdentityState) {
|
|
525
|
+
const state = identityProbe({ pid: owner.pid, startTime: owner.startTime });
|
|
526
|
+
if (state === 'unknown')
|
|
527
|
+
throw new Error('SANDBOX_CONTROL_OWNER_UNAVAILABLE');
|
|
528
|
+
return state === 'alive';
|
|
190
529
|
}
|
|
191
530
|
function readExecutions(manifest) {
|
|
192
531
|
if (!fs.existsSync(manifest.processingDir))
|
|
@@ -219,38 +558,45 @@ function readExecutions(manifest) {
|
|
|
219
558
|
}
|
|
220
559
|
return executions;
|
|
221
560
|
}
|
|
222
|
-
function
|
|
561
|
+
function processGroupState(groupId) {
|
|
223
562
|
try {
|
|
224
563
|
const rows = execFileSync('ps', ['-axo', 'pgid=,stat='], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
225
564
|
return rows.split('\n').some((row) => {
|
|
226
565
|
const match = row.trim().match(/^(\d+)\s+(\S+)/);
|
|
227
566
|
return Number(match?.[1]) === groupId && !match?.[2]?.startsWith('Z');
|
|
228
|
-
});
|
|
567
|
+
}) ? 'alive' : 'dead';
|
|
229
568
|
}
|
|
230
569
|
catch {
|
|
231
570
|
try {
|
|
232
571
|
process.kill(-groupId, 0);
|
|
233
|
-
return
|
|
572
|
+
return 'alive';
|
|
234
573
|
}
|
|
235
574
|
catch (error) {
|
|
236
|
-
|
|
575
|
+
const code = error.code;
|
|
576
|
+
if (code === 'ESRCH')
|
|
577
|
+
return 'dead';
|
|
578
|
+
if (code === 'EPERM')
|
|
579
|
+
return 'alive';
|
|
580
|
+
return 'unknown';
|
|
237
581
|
}
|
|
238
582
|
}
|
|
239
583
|
}
|
|
240
|
-
function executionAlive(execution, platform) {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
584
|
+
function executionAlive(execution, platform, identityProbe = getProcessIdentityState) {
|
|
585
|
+
const state = platform !== 'win32' && execution.child.processGroupId
|
|
586
|
+
? processGroupState(execution.child.processGroupId)
|
|
587
|
+
: identityProbe({ pid: execution.child.pid, startTime: execution.child.startTime });
|
|
588
|
+
if (state === 'unknown')
|
|
589
|
+
throw new Error('SANDBOX_CONTROL_EXECUTION_OWNER_UNAVAILABLE');
|
|
590
|
+
return state === 'alive';
|
|
245
591
|
}
|
|
246
|
-
async function waitForExit(owner, timeoutMs) {
|
|
247
|
-
const deadline = Date.now() + timeoutMs;
|
|
592
|
+
async function waitForExit(owner, timeoutMs, deadlineAt, identityProbe = getProcessIdentityState) {
|
|
593
|
+
const deadline = deadlineAt ?? (Date.now() + timeoutMs);
|
|
248
594
|
while (Date.now() < deadline) {
|
|
249
|
-
if (!ownerLive(owner))
|
|
595
|
+
if (!ownerLive(owner, identityProbe))
|
|
250
596
|
return true;
|
|
251
597
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
252
598
|
}
|
|
253
|
-
return !ownerLive(owner);
|
|
599
|
+
return !ownerLive(owner, identityProbe);
|
|
254
600
|
}
|
|
255
601
|
function signalOwner(owner, platform, force) {
|
|
256
602
|
try {
|
|
@@ -286,15 +632,19 @@ export async function quiesceSandboxControlRoot(root, options = {}) {
|
|
|
286
632
|
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
287
633
|
throw new Error('SANDBOX_CONTROL_CHANNEL_INVALID');
|
|
288
634
|
}
|
|
635
|
+
const timeoutMs = options.timeoutMs ?? options.timing?.quiesceDeadlineMs ?? DEFAULT_QUIESCE_TIMEOUT_MS;
|
|
636
|
+
const identityProbe = options.identityProbe ?? getProcessIdentityState;
|
|
637
|
+
const deadlineAt = Date.now() + timeoutMs;
|
|
638
|
+
const forceAt = forceDeadline(deadlineAt, timeoutMs);
|
|
289
639
|
markSandboxControlRootQuiescing(resolvedRoot);
|
|
290
|
-
await waitForStartupTransition(resolvedRoot,
|
|
640
|
+
await waitForStartupTransition(resolvedRoot, Math.max(0, forceAt - Date.now()), identityProbe);
|
|
291
641
|
const manifestPath = path.join(resolvedRoot, 'manifest.json');
|
|
292
|
-
const manifest = fs.existsSync(manifestPath) ?
|
|
642
|
+
const manifest = fs.existsSync(manifestPath) ? readSandboxControlManifestForTransition(manifestPath) : null;
|
|
293
643
|
const broker = readBrokerOwner(path.join(resolvedRoot, 'broker.json'));
|
|
294
644
|
const status = readStatusOwner(path.join(resolvedRoot, 'public', 'status.json'));
|
|
295
|
-
const statusOwner = status ?
|
|
296
|
-
const brokerLive = broker ? ownerLive(broker) : false;
|
|
297
|
-
const statusLive = statusOwner ? ownerLive(statusOwner) : false;
|
|
645
|
+
const statusOwner = status ? status.broker : null;
|
|
646
|
+
const brokerLive = broker ? ownerLive(broker, identityProbe) : false;
|
|
647
|
+
const statusLive = statusOwner ? ownerLive(statusOwner, identityProbe) : false;
|
|
298
648
|
if (manifest && broker && (broker.token !== manifest.token || broker.generation !== manifest.generation)) {
|
|
299
649
|
throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
300
650
|
}
|
|
@@ -307,42 +657,221 @@ export async function quiesceSandboxControlRoot(root, options = {}) {
|
|
|
307
657
|
if (!manifest && brokerLive)
|
|
308
658
|
throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
309
659
|
const platform = options.platform ?? process.platform;
|
|
310
|
-
const timeoutMs = options.timeoutMs ?? DEFAULT_QUIESCE_TIMEOUT_MS;
|
|
311
660
|
let executions = manifest ? readExecutions(manifest) : [];
|
|
312
661
|
const owner = brokerLive ? broker : statusLive ? statusOwner : null;
|
|
313
662
|
if (!owner) {
|
|
314
663
|
if (manifest && !broker && !status)
|
|
315
664
|
throw new Error('SANDBOX_CONTROL_OWNER_EVIDENCE_MISSING');
|
|
316
665
|
for (const execution of executions) {
|
|
317
|
-
|
|
666
|
+
terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, allowForce: false, identityProbe });
|
|
667
|
+
}
|
|
668
|
+
const softDeadlineAt = Math.min(forceAt, Date.now() + Math.floor(Math.max(0, forceAt - Date.now()) / 2));
|
|
669
|
+
while (Date.now() < softDeadlineAt) {
|
|
670
|
+
if (!executions.some((execution) => executionAlive(execution, platform, identityProbe)))
|
|
671
|
+
break;
|
|
672
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
673
|
+
}
|
|
674
|
+
if (manifest)
|
|
675
|
+
executions = mergeExecutions(executions, readExecutions(manifest));
|
|
676
|
+
if (Date.now() >= deadlineAt)
|
|
677
|
+
throw new Error('SANDBOX_CONTROL_QUIESCE_DEADLINE_EXCEEDED');
|
|
678
|
+
for (const execution of executions) {
|
|
679
|
+
if (!terminateSandboxControlExecution(execution, {
|
|
680
|
+
platform, timeoutMs: Math.max(0, deadlineAt - Date.now()), deadlineAt, forceAt: Date.now(), identityProbe
|
|
681
|
+
})) {
|
|
318
682
|
throw new Error(`SANDBOX_CONTROL_EXECUTION_STILL_RUNNING: ${execution.requestId}`);
|
|
319
683
|
}
|
|
320
684
|
}
|
|
321
|
-
if (executions.some((execution) => executionAlive(execution, platform))) {
|
|
685
|
+
if (executions.some((execution) => executionAlive(execution, platform, identityProbe))) {
|
|
322
686
|
throw new Error('SANDBOX_CONTROL_EXECUTION_STILL_RUNNING');
|
|
323
687
|
}
|
|
324
688
|
return broker || status ? 'stale' : 'missing';
|
|
325
689
|
}
|
|
690
|
+
const remaining = () => Math.max(0, deadlineAt - Date.now());
|
|
326
691
|
signalOwner(owner, platform, false);
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
692
|
+
for (const execution of executions) {
|
|
693
|
+
terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, allowForce: false, identityProbe });
|
|
694
|
+
}
|
|
695
|
+
const softDeadlineAt = Math.min(forceAt, Date.now() + Math.floor(Math.max(0, forceAt - Date.now()) / 2));
|
|
696
|
+
while (Date.now() < softDeadlineAt) {
|
|
697
|
+
if (!ownerLive(owner, identityProbe) && !executions.some((execution) => executionAlive(execution, platform, identityProbe)))
|
|
698
|
+
break;
|
|
699
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
700
|
+
}
|
|
701
|
+
if (!ownerLive(owner, identityProbe) && !executions.some((execution) => executionAlive(execution, platform, identityProbe))) {
|
|
331
702
|
return 'stopped';
|
|
332
703
|
}
|
|
333
704
|
if (manifest)
|
|
334
705
|
executions = mergeExecutions(executions, readExecutions(manifest));
|
|
706
|
+
if (Date.now() >= deadlineAt)
|
|
707
|
+
throw new Error('SANDBOX_CONTROL_QUIESCE_DEADLINE_EXCEEDED');
|
|
335
708
|
for (const execution of executions) {
|
|
336
|
-
|
|
337
|
-
throw new Error(`SANDBOX_CONTROL_EXECUTION_STILL_RUNNING: ${execution.requestId}`);
|
|
338
|
-
}
|
|
709
|
+
terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, deadlineAt, forceAt: Date.now(), identityProbe });
|
|
339
710
|
}
|
|
340
711
|
signalOwner(owner, platform, true);
|
|
341
|
-
|
|
712
|
+
while (Date.now() < deadlineAt) {
|
|
713
|
+
if (!ownerLive(owner, identityProbe) && !executions.some((execution) => executionAlive(execution, platform, identityProbe)))
|
|
714
|
+
return 'stopped';
|
|
715
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
716
|
+
}
|
|
717
|
+
if (ownerLive(owner, identityProbe))
|
|
342
718
|
throw new Error('SANDBOX_CONTROL_BROKER_STILL_RUNNING');
|
|
343
|
-
if (executions.some((execution) => executionAlive(execution, platform)))
|
|
719
|
+
if (executions.some((execution) => executionAlive(execution, platform, identityProbe)))
|
|
344
720
|
throw new Error('SANDBOX_CONTROL_EXECUTION_STILL_RUNNING');
|
|
345
|
-
}
|
|
346
721
|
return 'stopped';
|
|
347
722
|
}
|
|
723
|
+
export async function removeSandboxControlRoot(root, options) {
|
|
724
|
+
const resolvedRoot = path.resolve(root);
|
|
725
|
+
if (!fs.existsSync(resolvedRoot))
|
|
726
|
+
return;
|
|
727
|
+
const stat = fs.lstatSync(resolvedRoot);
|
|
728
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
729
|
+
throw new Error('SANDBOX_CONTROL_CHANNEL_INVALID');
|
|
730
|
+
const manifestPath = path.join(resolvedRoot, 'manifest.json');
|
|
731
|
+
const manifest = readSandboxControlManifestForTransition(manifestPath);
|
|
732
|
+
const inspectContainer = options.inspectContainer
|
|
733
|
+
?? ((timeoutMs) => inspectSandboxControlContainer(manifest, { timeoutMs }));
|
|
734
|
+
const timeoutMs = options.timeoutMs ?? options.timing?.quiesceDeadlineMs ?? DEFAULT_QUIESCE_TIMEOUT_MS;
|
|
735
|
+
const identityProbe = options.identityProbe ?? getProcessIdentityState;
|
|
736
|
+
const deadlineAt = Date.now() + timeoutMs;
|
|
737
|
+
const forceAt = forceDeadline(deadlineAt, timeoutMs);
|
|
738
|
+
const remaining = () => Math.max(0, deadlineAt - Date.now());
|
|
739
|
+
const observation = await awaitWithDeadline(() => inspectContainer(remaining()), deadlineAt, 'SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED');
|
|
740
|
+
if (observation.state === 'unknown')
|
|
741
|
+
throw new Error(`SANDBOX_CONTROL_CONTAINER_UNKNOWN: ${observation.reason}`);
|
|
742
|
+
if (options.requireAbsent && observation.state !== 'absent') {
|
|
743
|
+
throw new Error('SANDBOX_CONTROL_CONTAINER_REAPPEARED');
|
|
744
|
+
}
|
|
745
|
+
markSandboxControlRootQuiescing(resolvedRoot);
|
|
746
|
+
await waitForStartupTransition(resolvedRoot, Math.max(0, forceAt - Date.now()), identityProbe);
|
|
747
|
+
const brokerPath = path.join(resolvedRoot, 'broker.json');
|
|
748
|
+
const statusPath = path.join(resolvedRoot, 'public', 'status.json');
|
|
749
|
+
const broker = readBrokerOwner(brokerPath);
|
|
750
|
+
const status = readStatusOwner(statusPath);
|
|
751
|
+
if (broker && (broker.token !== manifest.token || broker.generation !== manifest.generation)) {
|
|
752
|
+
throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
753
|
+
}
|
|
754
|
+
if (status && status.generation !== manifest.generation) {
|
|
755
|
+
throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
756
|
+
}
|
|
757
|
+
if (broker && status && !sameOwner(broker, status.broker)
|
|
758
|
+
&& (ownerLive(broker, identityProbe) || ownerLive(status.broker, identityProbe))) {
|
|
759
|
+
throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
760
|
+
}
|
|
761
|
+
const platform = options.platform ?? process.platform;
|
|
762
|
+
const owner = broker && ownerLive(broker, identityProbe) ? broker
|
|
763
|
+
: status && ownerLive(status.broker, identityProbe) ? status.broker : null;
|
|
764
|
+
const selfOwner = options.selfOwner;
|
|
765
|
+
const selfOwned = Boolean(selfOwner && broker && sameOwner(broker, selfOwner));
|
|
766
|
+
if (selfOwner && (!broker || !selfOwned || broker.token !== manifest.token || broker.generation !== manifest.generation)) {
|
|
767
|
+
throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
|
|
768
|
+
}
|
|
769
|
+
let executions = readExecutions(manifest);
|
|
770
|
+
if (owner && !selfOwned)
|
|
771
|
+
signalOwner(owner, platform, false);
|
|
772
|
+
for (const execution of executions) {
|
|
773
|
+
terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, allowForce: false, identityProbe });
|
|
774
|
+
}
|
|
775
|
+
const softDeadlineAt = Math.min(forceAt, Date.now() + Math.floor(Math.max(0, forceAt - Date.now()) / 2));
|
|
776
|
+
while (Date.now() < softDeadlineAt) {
|
|
777
|
+
if ((!owner || selfOwned || !ownerLive(owner, identityProbe))
|
|
778
|
+
&& !executions.some((execution) => executionAlive(execution, platform, identityProbe)))
|
|
779
|
+
break;
|
|
780
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
781
|
+
}
|
|
782
|
+
if (observation.state === 'found') {
|
|
783
|
+
if (Date.now() >= deadlineAt)
|
|
784
|
+
throw new Error('SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED');
|
|
785
|
+
await awaitWithDeadline(() => options.removeContainer(remaining()), deadlineAt, 'SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED');
|
|
786
|
+
const afterRemoval = await awaitWithDeadline(() => inspectContainer(remaining()), deadlineAt, 'SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED');
|
|
787
|
+
if (afterRemoval.state !== 'absent') {
|
|
788
|
+
throw new Error(afterRemoval.state === 'unknown'
|
|
789
|
+
? `SANDBOX_CONTROL_CONTAINER_UNKNOWN: ${afterRemoval.reason}`
|
|
790
|
+
: 'SANDBOX_CONTROL_CONTAINER_STILL_EXISTS');
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
const currentManifest = readSandboxControlManifestForTransition(manifestPath);
|
|
794
|
+
if (currentManifest.token !== manifest.token || currentManifest.generation !== manifest.generation
|
|
795
|
+
|| !isSandboxControlRootQuiescing(resolvedRoot)) {
|
|
796
|
+
throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
|
|
797
|
+
}
|
|
798
|
+
const currentBroker = readBrokerOwner(brokerPath);
|
|
799
|
+
if (selfOwned && (!currentBroker || !selfOwner || !sameOwner(selfOwner, currentBroker)
|
|
800
|
+
|| currentBroker.token !== manifest.token || currentBroker.generation !== manifest.generation)) {
|
|
801
|
+
throw new Error('SANDBOX_CONTROL_OWNER_REPLACED');
|
|
802
|
+
}
|
|
803
|
+
if (owner && currentBroker && !sameOwner(owner, currentBroker)) {
|
|
804
|
+
throw new Error('SANDBOX_CONTROL_OWNER_REPLACED');
|
|
805
|
+
}
|
|
806
|
+
if (!owner && currentBroker && ownerLive(currentBroker, identityProbe)) {
|
|
807
|
+
throw new Error('SANDBOX_CONTROL_OWNER_REPLACED');
|
|
808
|
+
}
|
|
809
|
+
if (owner && ownerLive(owner, identityProbe) && !currentBroker) {
|
|
810
|
+
throw new Error('SANDBOX_CONTROL_OWNER_EVIDENCE_MISSING');
|
|
811
|
+
}
|
|
812
|
+
executions = mergeExecutions(executions, readExecutions(manifest));
|
|
813
|
+
if (Date.now() >= deadlineAt)
|
|
814
|
+
throw new Error('SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED');
|
|
815
|
+
for (const execution of executions) {
|
|
816
|
+
if (!terminateSandboxControlExecution(execution, {
|
|
817
|
+
platform, timeoutMs: remaining(), deadlineAt, forceAt: Date.now(), identityProbe
|
|
818
|
+
})) {
|
|
819
|
+
throw new Error(`SANDBOX_CONTROL_EXECUTION_STILL_RUNNING: ${execution.requestId}`);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
if (owner && currentBroker && ownerLive(currentBroker, identityProbe) && !selfOwned) {
|
|
823
|
+
if (remaining() <= 0)
|
|
824
|
+
throw new Error('SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED');
|
|
825
|
+
signalOwner(currentBroker, platform, true);
|
|
826
|
+
if (remaining() <= 0 || !await waitForExit(currentBroker, remaining(), deadlineAt, identityProbe)) {
|
|
827
|
+
throw new Error('SANDBOX_CONTROL_BROKER_STILL_RUNNING');
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
const finalExecutions = mergeExecutions(executions, readExecutions(manifest));
|
|
831
|
+
if (finalExecutions.some((execution) => executionAlive(execution, platform, identityProbe))) {
|
|
832
|
+
throw new Error('SANDBOX_CONTROL_EXECUTION_STILL_RUNNING');
|
|
833
|
+
}
|
|
834
|
+
const finalBrokerRaw = fs.existsSync(brokerPath) ? fs.readFileSync(brokerPath, 'utf8') : null;
|
|
835
|
+
if (finalBrokerRaw !== null) {
|
|
836
|
+
const finalBroker = readBrokerOwner(brokerPath);
|
|
837
|
+
if (!finalBroker)
|
|
838
|
+
throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
839
|
+
const finalSelfOwner = Boolean(selfOwner && sameOwner(finalBroker, selfOwner)
|
|
840
|
+
&& finalBroker.token === manifest.token && finalBroker.generation === manifest.generation);
|
|
841
|
+
if (ownerLive(finalBroker, identityProbe) && !finalSelfOwner) {
|
|
842
|
+
throw new Error('SANDBOX_CONTROL_BROKER_STILL_RUNNING');
|
|
843
|
+
}
|
|
844
|
+
if (fs.readFileSync(brokerPath, 'utf8') !== finalBrokerRaw) {
|
|
845
|
+
throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
|
|
846
|
+
}
|
|
847
|
+
fs.unlinkSync(brokerPath);
|
|
848
|
+
}
|
|
849
|
+
const finalStatusRaw = fs.existsSync(statusPath) ? fs.readFileSync(statusPath, 'utf8') : null;
|
|
850
|
+
if (finalStatusRaw !== null) {
|
|
851
|
+
const finalStatus = readStatusOwner(statusPath);
|
|
852
|
+
if (!finalStatus)
|
|
853
|
+
throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
854
|
+
if (ownerLive(finalStatus.broker, identityProbe)
|
|
855
|
+
&& !(selfOwner && sameOwner(finalStatus.broker, selfOwner))) {
|
|
856
|
+
throw new Error('SANDBOX_CONTROL_BROKER_STILL_RUNNING');
|
|
857
|
+
}
|
|
858
|
+
if (fs.readFileSync(statusPath, 'utf8') !== finalStatusRaw) {
|
|
859
|
+
throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
|
|
860
|
+
}
|
|
861
|
+
fs.unlinkSync(statusPath);
|
|
862
|
+
}
|
|
863
|
+
if (fs.existsSync(brokerPath) || fs.existsSync(statusPath)) {
|
|
864
|
+
throw new Error('SANDBOX_CONTROL_OWNER_EVIDENCE_REMAINS');
|
|
865
|
+
}
|
|
866
|
+
fs.rmSync(resolvedRoot, { recursive: true, force: true });
|
|
867
|
+
}
|
|
868
|
+
export async function garbageCollectSandboxControlRoot(root, options = {}) {
|
|
869
|
+
await removeSandboxControlRoot(path.resolve(root), {
|
|
870
|
+
...options,
|
|
871
|
+
requireAbsent: true,
|
|
872
|
+
removeContainer: async () => {
|
|
873
|
+
throw new Error('SANDBOX_CONTROL_CONTAINER_REAPPEARED');
|
|
874
|
+
}
|
|
875
|
+
});
|
|
876
|
+
}
|
|
348
877
|
//# sourceMappingURL=lifecycle.js.map
|