@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,14 +1,17 @@
|
|
|
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';
|
|
6
7
|
import { DEFAULT_SANDBOX_CONTROL_TIMING } from './protocol.js';
|
|
7
8
|
import { inspectSandboxControlContainer } from './container-identity.js';
|
|
8
9
|
import { parseSandboxControlStatus, readExecution, terminateSandboxControlExecution } from './state.js';
|
|
9
10
|
const DEFAULT_QUIESCE_TIMEOUT_MS = DEFAULT_SANDBOX_CONTROL_TIMING.quiesceDeadlineMs;
|
|
10
11
|
const QUIESCING_FILE = 'quiescing.json';
|
|
11
12
|
const BROKER_STARTING_FILE = 'broker-starting.json';
|
|
13
|
+
const REPLACEMENT_FILE = 'replacement.json';
|
|
14
|
+
const REPLACEMENT_STATE_SUFFIX = '.replacement-state.json';
|
|
12
15
|
async function awaitWithDeadline(operation, deadlineAt, errorCode) {
|
|
13
16
|
const remaining = deadlineAt - Date.now();
|
|
14
17
|
if (remaining <= 0)
|
|
@@ -76,14 +79,14 @@ function readStartupOwner(filePath) {
|
|
|
76
79
|
return null;
|
|
77
80
|
}
|
|
78
81
|
}
|
|
79
|
-
async function waitForStartupTransition(root, timeoutMs) {
|
|
82
|
+
async function waitForStartupTransition(root, timeoutMs, identityProbe = getProcessIdentityState) {
|
|
80
83
|
const transitionPath = path.join(root, BROKER_STARTING_FILE);
|
|
81
84
|
const deadline = Date.now() + timeoutMs;
|
|
82
|
-
while (
|
|
85
|
+
while (true) {
|
|
83
86
|
const transition = readStartupOwner(transitionPath);
|
|
84
87
|
if (!fs.existsSync(transitionPath))
|
|
85
88
|
return;
|
|
86
|
-
if (transition && !ownerLive(transition.owner)) {
|
|
89
|
+
if (transition && !ownerLive(transition.owner, identityProbe)) {
|
|
87
90
|
try {
|
|
88
91
|
if (fs.readFileSync(transitionPath, 'utf8') === transition.raw)
|
|
89
92
|
fs.unlinkSync(transitionPath);
|
|
@@ -93,11 +96,13 @@ async function waitForStartupTransition(root, timeoutMs) {
|
|
|
93
96
|
}
|
|
94
97
|
continue;
|
|
95
98
|
}
|
|
99
|
+
if (Date.now() >= deadline)
|
|
100
|
+
break;
|
|
96
101
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
97
102
|
}
|
|
98
103
|
throw new Error('SANDBOX_CONTROL_BROKER_START_TRANSITION');
|
|
99
104
|
}
|
|
100
|
-
export async function acquireSandboxControlBrokerStartup(root, owner, timeoutMs = 5_000) {
|
|
105
|
+
export async function acquireSandboxControlBrokerStartup(root, owner, timeoutMs = 5_000, options = {}) {
|
|
101
106
|
const resolvedRoot = path.resolve(root);
|
|
102
107
|
const transitionPath = path.join(resolvedRoot, BROKER_STARTING_FILE);
|
|
103
108
|
const record = `${JSON.stringify({ version: 2, ...owner })}\n`;
|
|
@@ -131,7 +136,7 @@ export async function acquireSandboxControlBrokerStartup(root, owner, timeoutMs
|
|
|
131
136
|
if (error.code !== 'EEXIST')
|
|
132
137
|
throw error;
|
|
133
138
|
const transition = readStartupOwner(transitionPath);
|
|
134
|
-
if (transition && !ownerLive(transition.owner)) {
|
|
139
|
+
if (transition && !ownerLive(transition.owner, options.identityProbe)) {
|
|
135
140
|
try {
|
|
136
141
|
if (fs.readFileSync(transitionPath, 'utf8') === transition.raw)
|
|
137
142
|
fs.unlinkSync(transitionPath);
|
|
@@ -146,40 +151,342 @@ export async function acquireSandboxControlBrokerStartup(root, owner, timeoutMs
|
|
|
146
151
|
}
|
|
147
152
|
throw new Error('SANDBOX_CONTROL_BROKER_START_TRANSITION');
|
|
148
153
|
}
|
|
149
|
-
|
|
154
|
+
function replacementPath(root) {
|
|
155
|
+
return path.join(root, REPLACEMENT_FILE);
|
|
156
|
+
}
|
|
157
|
+
function replacementStatePath(root) {
|
|
158
|
+
const resolvedRoot = path.resolve(root);
|
|
159
|
+
return path.join(path.dirname(resolvedRoot), `.${path.basename(resolvedRoot)}${REPLACEMENT_STATE_SUFFIX}`);
|
|
160
|
+
}
|
|
161
|
+
function replacementSnapshotPath(root, transitionId) {
|
|
162
|
+
const resolvedRoot = path.resolve(root);
|
|
163
|
+
return path.join(path.dirname(resolvedRoot), `.${path.basename(resolvedRoot)}.replacement-${transitionId}.snapshot`);
|
|
164
|
+
}
|
|
165
|
+
function writeAtomicText(filePath, content) {
|
|
166
|
+
const temporary = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
167
|
+
try {
|
|
168
|
+
fs.writeFileSync(temporary, content, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
169
|
+
fs.renameSync(temporary, filePath);
|
|
170
|
+
}
|
|
171
|
+
finally {
|
|
172
|
+
fs.rmSync(temporary, { force: true });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function readReplacementState(root) {
|
|
176
|
+
const filePath = replacementStatePath(root);
|
|
177
|
+
if (!fs.existsSync(filePath))
|
|
178
|
+
return null;
|
|
179
|
+
if (!regularFile(filePath))
|
|
180
|
+
throw new Error('SANDBOX_CONTROL_REPLACEMENT_RECOVERY_INVALID');
|
|
181
|
+
try {
|
|
182
|
+
const value = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
183
|
+
const resolvedRoot = path.resolve(root);
|
|
184
|
+
if (value.version !== 1 || (value.phase !== 'prepared' && value.phase !== 'committed')
|
|
185
|
+
|| value.root !== resolvedRoot || typeof value.transitionId !== 'string' || value.transitionId.length === 0
|
|
186
|
+
|| value.snapshotRoot !== replacementSnapshotPath(resolvedRoot, value.transitionId)) {
|
|
187
|
+
throw new Error('invalid replacement state');
|
|
188
|
+
}
|
|
189
|
+
return value;
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
throw new Error('SANDBOX_CONTROL_REPLACEMENT_RECOVERY_INVALID');
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function writeReplacementState(state) {
|
|
196
|
+
writeAtomicText(replacementStatePath(state.root), `${JSON.stringify(state)}\n`);
|
|
197
|
+
}
|
|
198
|
+
function removeReplacementState(root) {
|
|
199
|
+
fs.rmSync(replacementStatePath(root), { force: true });
|
|
200
|
+
}
|
|
201
|
+
function readReplacementRecord(filePath) {
|
|
202
|
+
if (!fs.existsSync(filePath))
|
|
203
|
+
return null;
|
|
204
|
+
if (!regularFile(filePath))
|
|
205
|
+
throw new Error('SANDBOX_CONTROL_REPLACEMENT_INVALID');
|
|
206
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
207
|
+
try {
|
|
208
|
+
const value = JSON.parse(raw);
|
|
209
|
+
if (value.version !== 1 || typeof value.transitionId !== 'string' || value.transitionId.length === 0
|
|
210
|
+
|| !Number.isSafeInteger(value.pid) || (value.pid ?? 0) <= 0
|
|
211
|
+
|| !Number.isSafeInteger(value.startTime))
|
|
212
|
+
throw new Error('invalid replacement');
|
|
213
|
+
return { raw, record: value };
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
throw new Error('SANDBOX_CONTROL_REPLACEMENT_INVALID');
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
export function acquireSandboxControlReplacement(root, options = {}) {
|
|
220
|
+
const resolvedRoot = path.resolve(root);
|
|
221
|
+
if (fs.existsSync(resolvedRoot)) {
|
|
222
|
+
const stat = fs.lstatSync(resolvedRoot);
|
|
223
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
224
|
+
throw new Error('SANDBOX_CONTROL_CHANNEL_INVALID');
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
fs.mkdirSync(resolvedRoot, { recursive: true, mode: 0o700 });
|
|
228
|
+
}
|
|
229
|
+
const startTime = options.owner?.startTime ?? getProcessStartTime(process.pid);
|
|
230
|
+
if (startTime === null || startTime === undefined)
|
|
231
|
+
throw new Error('SANDBOX_CONTROL_REPLACEMENT_OWNER_UNAVAILABLE');
|
|
232
|
+
const owner = options.owner ?? { pid: process.pid, startTime };
|
|
233
|
+
const transitionId = randomUUID();
|
|
234
|
+
const record = `${JSON.stringify({ version: 1, transitionId, ...owner })}\n`;
|
|
235
|
+
const filePath = replacementPath(resolvedRoot);
|
|
236
|
+
const existing = readReplacementRecord(filePath);
|
|
237
|
+
if (existing) {
|
|
238
|
+
const ownerState = (options.probeOwner ?? getProcessIdentityState)(existing.record);
|
|
239
|
+
if (ownerState === 'alive') {
|
|
240
|
+
throw new Error('SANDBOX_CONTROL_REPLACEMENT_BUSY');
|
|
241
|
+
}
|
|
242
|
+
if (ownerState === 'unknown') {
|
|
243
|
+
throw new Error('SANDBOX_CONTROL_REPLACEMENT_OWNER_UNAVAILABLE');
|
|
244
|
+
}
|
|
245
|
+
if (fs.readFileSync(filePath, 'utf8') !== existing.raw) {
|
|
246
|
+
throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
|
|
247
|
+
}
|
|
248
|
+
fs.unlinkSync(filePath);
|
|
249
|
+
}
|
|
250
|
+
try {
|
|
251
|
+
fs.writeFileSync(filePath, record, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
if (error.code === 'EEXIST') {
|
|
255
|
+
throw new Error('SANDBOX_CONTROL_REPLACEMENT_BUSY');
|
|
256
|
+
}
|
|
257
|
+
throw error;
|
|
258
|
+
}
|
|
259
|
+
let released = false;
|
|
260
|
+
const verifyOwnership = () => {
|
|
261
|
+
if (released)
|
|
262
|
+
throw new Error('SANDBOX_CONTROL_REPLACEMENT_RELEASED');
|
|
263
|
+
const current = readReplacementRecord(filePath);
|
|
264
|
+
if (!current || current.raw !== record || current.record.transitionId !== transitionId) {
|
|
265
|
+
throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
return {
|
|
269
|
+
root: resolvedRoot,
|
|
270
|
+
transitionId,
|
|
271
|
+
owner,
|
|
272
|
+
assertOwned: verifyOwnership,
|
|
273
|
+
clearQuiescing() {
|
|
274
|
+
verifyOwnership();
|
|
275
|
+
const markerPath = path.join(resolvedRoot, QUIESCING_FILE);
|
|
276
|
+
if (fs.existsSync(markerPath)) {
|
|
277
|
+
if (!regularFile(markerPath))
|
|
278
|
+
throw new Error('SANDBOX_CONTROL_CHANNEL_INVALID');
|
|
279
|
+
fs.unlinkSync(markerPath);
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
release() {
|
|
283
|
+
if (released)
|
|
284
|
+
return;
|
|
285
|
+
verifyOwnership();
|
|
286
|
+
fs.unlinkSync(filePath);
|
|
287
|
+
released = true;
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
function readReplacementRaw(root) {
|
|
292
|
+
const current = readReplacementRecord(replacementPath(root));
|
|
293
|
+
if (!current)
|
|
294
|
+
throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
|
|
295
|
+
return current.raw;
|
|
296
|
+
}
|
|
297
|
+
export function beginSandboxControlReplacement(root, lease) {
|
|
298
|
+
const resolvedRoot = path.resolve(root);
|
|
299
|
+
if (path.resolve(lease.root) !== resolvedRoot)
|
|
300
|
+
throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
|
|
301
|
+
lease.assertOwned();
|
|
302
|
+
if (!fs.existsSync(resolvedRoot))
|
|
303
|
+
throw new Error('SANDBOX_CONTROL_CHANNEL_INVALID');
|
|
304
|
+
const rootStat = fs.lstatSync(resolvedRoot);
|
|
305
|
+
if (!rootStat.isDirectory() || rootStat.isSymbolicLink())
|
|
306
|
+
throw new Error('SANDBOX_CONTROL_CHANNEL_INVALID');
|
|
307
|
+
if (readReplacementState(resolvedRoot))
|
|
308
|
+
throw new Error('SANDBOX_CONTROL_REPLACEMENT_RECOVERY_REQUIRED');
|
|
309
|
+
const snapshotRoot = replacementSnapshotPath(resolvedRoot, lease.transitionId);
|
|
310
|
+
if (fs.existsSync(snapshotRoot))
|
|
311
|
+
throw new Error('SANDBOX_CONTROL_REPLACEMENT_RECOVERY_INVALID');
|
|
312
|
+
const replacementRaw = readReplacementRaw(resolvedRoot);
|
|
313
|
+
const state = {
|
|
314
|
+
version: 1,
|
|
315
|
+
phase: 'prepared',
|
|
316
|
+
root: resolvedRoot,
|
|
317
|
+
snapshotRoot,
|
|
318
|
+
transitionId: lease.transitionId
|
|
319
|
+
};
|
|
320
|
+
writeReplacementState(state);
|
|
321
|
+
let moved = false;
|
|
322
|
+
try {
|
|
323
|
+
fs.renameSync(resolvedRoot, snapshotRoot);
|
|
324
|
+
moved = true;
|
|
325
|
+
fs.mkdirSync(resolvedRoot, { recursive: true, mode: 0o700 });
|
|
326
|
+
fs.writeFileSync(replacementPath(resolvedRoot), replacementRaw, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
327
|
+
markSandboxControlRootQuiescing(resolvedRoot);
|
|
328
|
+
return { root: resolvedRoot, snapshotRoot, transitionId: lease.transitionId };
|
|
329
|
+
}
|
|
330
|
+
catch (error) {
|
|
331
|
+
if (moved) {
|
|
332
|
+
fs.rmSync(resolvedRoot, { recursive: true, force: true });
|
|
333
|
+
fs.renameSync(snapshotRoot, resolvedRoot);
|
|
334
|
+
}
|
|
335
|
+
removeReplacementState(resolvedRoot);
|
|
336
|
+
throw error;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
function isRecoverableCutoverManifestError(error) {
|
|
340
|
+
return error instanceof Error
|
|
341
|
+
&& (error.message.startsWith('SANDBOX_CONTROL_MANIFEST_')
|
|
342
|
+
|| error.message === 'SANDBOX_CONTROL_OWNER_EVIDENCE_MISSING');
|
|
343
|
+
}
|
|
344
|
+
export async function recoverSandboxControlReplacement(root, lease) {
|
|
345
|
+
const resolvedRoot = path.resolve(root);
|
|
346
|
+
if (path.resolve(lease.root) !== resolvedRoot)
|
|
347
|
+
throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
|
|
348
|
+
const state = readReplacementState(resolvedRoot);
|
|
349
|
+
if (!state)
|
|
350
|
+
return 'none';
|
|
351
|
+
lease.assertOwned();
|
|
352
|
+
if (state.phase === 'committed') {
|
|
353
|
+
fs.rmSync(state.snapshotRoot, { recursive: true, force: true });
|
|
354
|
+
removeReplacementState(resolvedRoot);
|
|
355
|
+
return 'committed';
|
|
356
|
+
}
|
|
357
|
+
if (!fs.existsSync(state.snapshotRoot)) {
|
|
358
|
+
removeReplacementState(resolvedRoot);
|
|
359
|
+
return 'none';
|
|
360
|
+
}
|
|
361
|
+
if (fs.existsSync(resolvedRoot) && fs.existsSync(path.join(resolvedRoot, 'manifest.json'))) {
|
|
362
|
+
try {
|
|
363
|
+
await quiesceSandboxControlRoot(resolvedRoot);
|
|
364
|
+
}
|
|
365
|
+
catch (error) {
|
|
366
|
+
if (!isRecoverableCutoverManifestError(error))
|
|
367
|
+
throw error;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
lease.assertOwned();
|
|
371
|
+
const replacementRaw = readReplacementRaw(resolvedRoot);
|
|
372
|
+
fs.rmSync(resolvedRoot, { recursive: true, force: true });
|
|
373
|
+
fs.renameSync(state.snapshotRoot, resolvedRoot);
|
|
374
|
+
writeAtomicText(replacementPath(resolvedRoot), replacementRaw);
|
|
375
|
+
removeReplacementState(resolvedRoot);
|
|
376
|
+
return 'restored';
|
|
377
|
+
}
|
|
378
|
+
export function commitSandboxControlReplacement(root, lease) {
|
|
379
|
+
const resolvedRoot = path.resolve(root);
|
|
380
|
+
if (path.resolve(lease.root) !== resolvedRoot)
|
|
381
|
+
throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
|
|
382
|
+
const state = readReplacementState(resolvedRoot);
|
|
383
|
+
if (!state)
|
|
384
|
+
return;
|
|
385
|
+
lease.assertOwned();
|
|
386
|
+
writeReplacementState({ ...state, phase: 'committed' });
|
|
387
|
+
fs.rmSync(state.snapshotRoot, { recursive: true, force: true });
|
|
388
|
+
removeReplacementState(resolvedRoot);
|
|
389
|
+
}
|
|
390
|
+
function readOptionalRaw(filePath) {
|
|
391
|
+
if (!fs.existsSync(filePath))
|
|
392
|
+
return null;
|
|
393
|
+
if (!regularFile(filePath))
|
|
394
|
+
throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
|
|
395
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
396
|
+
}
|
|
397
|
+
function readProcessingSnapshot(root) {
|
|
398
|
+
const processingDir = path.join(root, 'processing');
|
|
399
|
+
if (!fs.existsSync(processingDir))
|
|
400
|
+
return [];
|
|
401
|
+
const stat = fs.lstatSync(processingDir);
|
|
402
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
403
|
+
throw new Error('SANDBOX_CONTROL_EXECUTION_STILL_RUNNING');
|
|
404
|
+
return fs.readdirSync(processingDir, { withFileTypes: true })
|
|
405
|
+
.map((entry) => ({
|
|
406
|
+
name: entry.name,
|
|
407
|
+
directory: entry.isDirectory() && !entry.isSymbolicLink(),
|
|
408
|
+
executionRaw: entry.isDirectory() && !entry.isSymbolicLink()
|
|
409
|
+
? readOptionalRaw(path.join(processingDir, entry.name, 'execution.json'))
|
|
410
|
+
: null
|
|
411
|
+
}))
|
|
412
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
413
|
+
}
|
|
414
|
+
export function captureSandboxControlCutoverSnapshot(root) {
|
|
415
|
+
const resolvedRoot = path.resolve(root);
|
|
416
|
+
const manifestPath = path.join(resolvedRoot, 'manifest.json');
|
|
417
|
+
if (fs.existsSync(manifestPath))
|
|
418
|
+
readSandboxControlManifestForTransition(manifestPath);
|
|
419
|
+
return {
|
|
420
|
+
root: resolvedRoot,
|
|
421
|
+
manifestRaw: readOptionalRaw(manifestPath),
|
|
422
|
+
brokerRaw: readOptionalRaw(path.join(resolvedRoot, 'broker.json')),
|
|
423
|
+
statusRaw: readOptionalRaw(path.join(resolvedRoot, 'public', 'status.json')),
|
|
424
|
+
processing: readProcessingSnapshot(resolvedRoot)
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
export function assertSandboxControlCutoverSnapshot(snapshot) {
|
|
428
|
+
const current = captureSandboxControlCutoverSnapshot(snapshot.root);
|
|
429
|
+
if (JSON.stringify(current) !== JSON.stringify(snapshot)) {
|
|
430
|
+
throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
function readSandboxControlManifestValue(manifestPath, allowLegacy) {
|
|
150
434
|
if (!regularFile(manifestPath))
|
|
151
435
|
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
152
436
|
let manifest;
|
|
153
437
|
try {
|
|
154
|
-
|
|
438
|
+
const parsed = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
439
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
440
|
+
throw new Error('invalid manifest');
|
|
441
|
+
}
|
|
442
|
+
manifest = parsed;
|
|
155
443
|
}
|
|
156
444
|
catch {
|
|
157
445
|
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
158
446
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
||
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
||
|
|
169
|
-
||
|
|
170
|
-
|| typeof
|
|
171
|
-
||
|
|
172
|
-
||
|
|
173
|
-
||
|
|
174
|
-
|| typeof
|
|
447
|
+
const candidate = manifest;
|
|
448
|
+
if (candidate.version !== 4 && candidate.version !== 5) {
|
|
449
|
+
throw new Error('SANDBOX_CONTROL_MANIFEST_VERSION_INVALID: expected version 5; container-only recreation is required');
|
|
450
|
+
}
|
|
451
|
+
if (typeof candidate.containerIdentity !== 'object' || candidate.containerIdentity === null
|
|
452
|
+
|| Array.isArray(candidate.containerIdentity))
|
|
453
|
+
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
454
|
+
const containerIdentity = candidate.containerIdentity;
|
|
455
|
+
if (typeof candidate.engine !== 'string' || candidate.engine.length === 0
|
|
456
|
+
|| typeof candidate.repoRoot !== 'string' || typeof candidate.worktreeRoot !== 'string'
|
|
457
|
+
|| typeof candidate.project !== 'string' || typeof candidate.container !== 'string'
|
|
458
|
+
|| typeof containerIdentity.id !== 'string' || containerIdentity.id.length === 0
|
|
459
|
+
|| typeof containerIdentity.labels !== 'object' || containerIdentity.labels === null
|
|
460
|
+
|| Array.isArray(containerIdentity.labels)
|
|
461
|
+
|| Object.values(containerIdentity.labels).some((value) => typeof value !== 'string')
|
|
462
|
+
|| typeof candidate.branch !== 'string' || !['task-bound', 'branch-only'].includes(candidate.mode ?? '')
|
|
463
|
+
|| (candidate.taskId !== null && typeof candidate.taskId !== 'string')
|
|
464
|
+
|| typeof candidate.channelDir !== 'string' || typeof candidate.publicStatusDir !== 'string'
|
|
465
|
+
|| typeof candidate.processingDir !== 'string' || typeof candidate.token !== 'string'
|
|
466
|
+
|| typeof candidate.generation !== 'string')
|
|
175
467
|
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
176
468
|
const root = path.dirname(path.resolve(manifestPath));
|
|
177
|
-
if (path.resolve(
|
|
178
|
-
|| path.resolve(
|
|
179
|
-
|| path.resolve(
|
|
469
|
+
if (path.resolve(candidate.channelDir) !== path.join(root, 'channel')
|
|
470
|
+
|| path.resolve(candidate.publicStatusDir) !== path.join(root, 'public')
|
|
471
|
+
|| path.resolve(candidate.processingDir) !== path.join(root, 'processing')) {
|
|
472
|
+
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
473
|
+
}
|
|
474
|
+
if (candidate.version === 4) {
|
|
475
|
+
if (!allowLegacy) {
|
|
476
|
+
throw new Error('SANDBOX_CONTROL_MANIFEST_VERSION_INVALID: expected version 5; container-only recreation is required');
|
|
477
|
+
}
|
|
478
|
+
return candidate;
|
|
479
|
+
}
|
|
480
|
+
if (typeof candidate.runtimeDir !== 'string' || path.resolve(candidate.runtimeDir) !== path.join(root, 'runtime')) {
|
|
180
481
|
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
181
482
|
}
|
|
182
|
-
return
|
|
483
|
+
return candidate;
|
|
484
|
+
}
|
|
485
|
+
export function readSandboxControlManifest(manifestPath) {
|
|
486
|
+
return readSandboxControlManifestValue(manifestPath, false);
|
|
487
|
+
}
|
|
488
|
+
export function readSandboxControlManifestForTransition(manifestPath) {
|
|
489
|
+
return readSandboxControlManifestValue(manifestPath, true);
|
|
183
490
|
}
|
|
184
491
|
function readBrokerOwner(filePath) {
|
|
185
492
|
if (!fs.existsSync(filePath))
|
|
@@ -216,8 +523,11 @@ function sameOwner(left, right) {
|
|
|
216
523
|
return left.pid === right.pid && left.startTime === right.startTime
|
|
217
524
|
&& (!left.brokerId || !right.brokerId || left.brokerId === right.brokerId);
|
|
218
525
|
}
|
|
219
|
-
function ownerLive(owner) {
|
|
220
|
-
|
|
526
|
+
function ownerLive(owner, identityProbe = getProcessIdentityState) {
|
|
527
|
+
const state = identityProbe({ pid: owner.pid, startTime: owner.startTime });
|
|
528
|
+
if (state === 'unknown')
|
|
529
|
+
throw new Error('SANDBOX_CONTROL_OWNER_UNAVAILABLE');
|
|
530
|
+
return state === 'alive';
|
|
221
531
|
}
|
|
222
532
|
function readExecutions(manifest) {
|
|
223
533
|
if (!fs.existsSync(manifest.processingDir))
|
|
@@ -250,38 +560,45 @@ function readExecutions(manifest) {
|
|
|
250
560
|
}
|
|
251
561
|
return executions;
|
|
252
562
|
}
|
|
253
|
-
function
|
|
563
|
+
function processGroupState(groupId) {
|
|
254
564
|
try {
|
|
255
565
|
const rows = execFileSync('ps', ['-axo', 'pgid=,stat='], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
256
566
|
return rows.split('\n').some((row) => {
|
|
257
567
|
const match = row.trim().match(/^(\d+)\s+(\S+)/);
|
|
258
568
|
return Number(match?.[1]) === groupId && !match?.[2]?.startsWith('Z');
|
|
259
|
-
});
|
|
569
|
+
}) ? 'alive' : 'dead';
|
|
260
570
|
}
|
|
261
571
|
catch {
|
|
262
572
|
try {
|
|
263
573
|
process.kill(-groupId, 0);
|
|
264
|
-
return
|
|
574
|
+
return 'alive';
|
|
265
575
|
}
|
|
266
576
|
catch (error) {
|
|
267
|
-
|
|
577
|
+
const code = error.code;
|
|
578
|
+
if (code === 'ESRCH')
|
|
579
|
+
return 'dead';
|
|
580
|
+
if (code === 'EPERM')
|
|
581
|
+
return 'alive';
|
|
582
|
+
return 'unknown';
|
|
268
583
|
}
|
|
269
584
|
}
|
|
270
585
|
}
|
|
271
|
-
function executionAlive(execution, platform) {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
586
|
+
function executionAlive(execution, platform, identityProbe = getProcessIdentityState) {
|
|
587
|
+
const state = platform !== 'win32' && execution.child.processGroupId
|
|
588
|
+
? processGroupState(execution.child.processGroupId)
|
|
589
|
+
: identityProbe({ pid: execution.child.pid, startTime: execution.child.startTime });
|
|
590
|
+
if (state === 'unknown')
|
|
591
|
+
throw new Error('SANDBOX_CONTROL_EXECUTION_OWNER_UNAVAILABLE');
|
|
592
|
+
return state === 'alive';
|
|
276
593
|
}
|
|
277
|
-
async function waitForExit(owner, timeoutMs, deadlineAt) {
|
|
594
|
+
async function waitForExit(owner, timeoutMs, deadlineAt, identityProbe = getProcessIdentityState) {
|
|
278
595
|
const deadline = deadlineAt ?? (Date.now() + timeoutMs);
|
|
279
596
|
while (Date.now() < deadline) {
|
|
280
|
-
if (!ownerLive(owner))
|
|
597
|
+
if (!ownerLive(owner, identityProbe))
|
|
281
598
|
return true;
|
|
282
599
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
283
600
|
}
|
|
284
|
-
return !ownerLive(owner);
|
|
601
|
+
return !ownerLive(owner, identityProbe);
|
|
285
602
|
}
|
|
286
603
|
function signalOwner(owner, platform, force) {
|
|
287
604
|
try {
|
|
@@ -318,17 +635,18 @@ export async function quiesceSandboxControlRoot(root, options = {}) {
|
|
|
318
635
|
throw new Error('SANDBOX_CONTROL_CHANNEL_INVALID');
|
|
319
636
|
}
|
|
320
637
|
const timeoutMs = options.timeoutMs ?? options.timing?.quiesceDeadlineMs ?? DEFAULT_QUIESCE_TIMEOUT_MS;
|
|
638
|
+
const identityProbe = options.identityProbe ?? getProcessIdentityState;
|
|
321
639
|
const deadlineAt = Date.now() + timeoutMs;
|
|
322
640
|
const forceAt = forceDeadline(deadlineAt, timeoutMs);
|
|
323
641
|
markSandboxControlRootQuiescing(resolvedRoot);
|
|
324
|
-
await waitForStartupTransition(resolvedRoot, Math.max(0, forceAt - Date.now()));
|
|
642
|
+
await waitForStartupTransition(resolvedRoot, Math.max(0, forceAt - Date.now()), identityProbe);
|
|
325
643
|
const manifestPath = path.join(resolvedRoot, 'manifest.json');
|
|
326
|
-
const manifest = fs.existsSync(manifestPath) ?
|
|
644
|
+
const manifest = fs.existsSync(manifestPath) ? readSandboxControlManifestForTransition(manifestPath) : null;
|
|
327
645
|
const broker = readBrokerOwner(path.join(resolvedRoot, 'broker.json'));
|
|
328
646
|
const status = readStatusOwner(path.join(resolvedRoot, 'public', 'status.json'));
|
|
329
647
|
const statusOwner = status ? status.broker : null;
|
|
330
|
-
const brokerLive = broker ? ownerLive(broker) : false;
|
|
331
|
-
const statusLive = statusOwner ? ownerLive(statusOwner) : false;
|
|
648
|
+
const brokerLive = broker ? ownerLive(broker, identityProbe) : false;
|
|
649
|
+
const statusLive = statusOwner ? ownerLive(statusOwner, identityProbe) : false;
|
|
332
650
|
if (manifest && broker && (broker.token !== manifest.token || broker.generation !== manifest.generation)) {
|
|
333
651
|
throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
334
652
|
}
|
|
@@ -347,11 +665,11 @@ export async function quiesceSandboxControlRoot(root, options = {}) {
|
|
|
347
665
|
if (manifest && !broker && !status)
|
|
348
666
|
throw new Error('SANDBOX_CONTROL_OWNER_EVIDENCE_MISSING');
|
|
349
667
|
for (const execution of executions) {
|
|
350
|
-
terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, allowForce: false });
|
|
668
|
+
terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, allowForce: false, identityProbe });
|
|
351
669
|
}
|
|
352
670
|
const softDeadlineAt = Math.min(forceAt, Date.now() + Math.floor(Math.max(0, forceAt - Date.now()) / 2));
|
|
353
671
|
while (Date.now() < softDeadlineAt) {
|
|
354
|
-
if (!executions.some((execution) => executionAlive(execution, platform)))
|
|
672
|
+
if (!executions.some((execution) => executionAlive(execution, platform, identityProbe)))
|
|
355
673
|
break;
|
|
356
674
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
357
675
|
}
|
|
@@ -361,12 +679,12 @@ export async function quiesceSandboxControlRoot(root, options = {}) {
|
|
|
361
679
|
throw new Error('SANDBOX_CONTROL_QUIESCE_DEADLINE_EXCEEDED');
|
|
362
680
|
for (const execution of executions) {
|
|
363
681
|
if (!terminateSandboxControlExecution(execution, {
|
|
364
|
-
platform, timeoutMs: Math.max(0, deadlineAt - Date.now()), deadlineAt, forceAt: Date.now()
|
|
682
|
+
platform, timeoutMs: Math.max(0, deadlineAt - Date.now()), deadlineAt, forceAt: Date.now(), identityProbe
|
|
365
683
|
})) {
|
|
366
684
|
throw new Error(`SANDBOX_CONTROL_EXECUTION_STILL_RUNNING: ${execution.requestId}`);
|
|
367
685
|
}
|
|
368
686
|
}
|
|
369
|
-
if (executions.some((execution) => executionAlive(execution, platform))) {
|
|
687
|
+
if (executions.some((execution) => executionAlive(execution, platform, identityProbe))) {
|
|
370
688
|
throw new Error('SANDBOX_CONTROL_EXECUTION_STILL_RUNNING');
|
|
371
689
|
}
|
|
372
690
|
return broker || status ? 'stale' : 'missing';
|
|
@@ -374,15 +692,15 @@ export async function quiesceSandboxControlRoot(root, options = {}) {
|
|
|
374
692
|
const remaining = () => Math.max(0, deadlineAt - Date.now());
|
|
375
693
|
signalOwner(owner, platform, false);
|
|
376
694
|
for (const execution of executions) {
|
|
377
|
-
terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, allowForce: false });
|
|
695
|
+
terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, allowForce: false, identityProbe });
|
|
378
696
|
}
|
|
379
697
|
const softDeadlineAt = Math.min(forceAt, Date.now() + Math.floor(Math.max(0, forceAt - Date.now()) / 2));
|
|
380
698
|
while (Date.now() < softDeadlineAt) {
|
|
381
|
-
if (!ownerLive(owner) && !executions.some((execution) => executionAlive(execution, platform)))
|
|
699
|
+
if (!ownerLive(owner, identityProbe) && !executions.some((execution) => executionAlive(execution, platform, identityProbe)))
|
|
382
700
|
break;
|
|
383
701
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
384
702
|
}
|
|
385
|
-
if (!ownerLive(owner) && !executions.some((execution) => executionAlive(execution, platform))) {
|
|
703
|
+
if (!ownerLive(owner, identityProbe) && !executions.some((execution) => executionAlive(execution, platform, identityProbe))) {
|
|
386
704
|
return 'stopped';
|
|
387
705
|
}
|
|
388
706
|
if (manifest)
|
|
@@ -390,17 +708,17 @@ export async function quiesceSandboxControlRoot(root, options = {}) {
|
|
|
390
708
|
if (Date.now() >= deadlineAt)
|
|
391
709
|
throw new Error('SANDBOX_CONTROL_QUIESCE_DEADLINE_EXCEEDED');
|
|
392
710
|
for (const execution of executions) {
|
|
393
|
-
terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, deadlineAt, forceAt: Date.now() });
|
|
711
|
+
terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, deadlineAt, forceAt: Date.now(), identityProbe });
|
|
394
712
|
}
|
|
395
713
|
signalOwner(owner, platform, true);
|
|
396
714
|
while (Date.now() < deadlineAt) {
|
|
397
|
-
if (!ownerLive(owner) && !executions.some((execution) => executionAlive(execution, platform)))
|
|
715
|
+
if (!ownerLive(owner, identityProbe) && !executions.some((execution) => executionAlive(execution, platform, identityProbe)))
|
|
398
716
|
return 'stopped';
|
|
399
717
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
400
718
|
}
|
|
401
|
-
if (ownerLive(owner))
|
|
719
|
+
if (ownerLive(owner, identityProbe))
|
|
402
720
|
throw new Error('SANDBOX_CONTROL_BROKER_STILL_RUNNING');
|
|
403
|
-
if (executions.some((execution) => executionAlive(execution, platform)))
|
|
721
|
+
if (executions.some((execution) => executionAlive(execution, platform, identityProbe)))
|
|
404
722
|
throw new Error('SANDBOX_CONTROL_EXECUTION_STILL_RUNNING');
|
|
405
723
|
return 'stopped';
|
|
406
724
|
}
|
|
@@ -412,10 +730,11 @@ export async function removeSandboxControlRoot(root, options) {
|
|
|
412
730
|
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
413
731
|
throw new Error('SANDBOX_CONTROL_CHANNEL_INVALID');
|
|
414
732
|
const manifestPath = path.join(resolvedRoot, 'manifest.json');
|
|
415
|
-
const manifest =
|
|
733
|
+
const manifest = readSandboxControlManifestForTransition(manifestPath);
|
|
416
734
|
const inspectContainer = options.inspectContainer
|
|
417
735
|
?? ((timeoutMs) => inspectSandboxControlContainer(manifest, { timeoutMs }));
|
|
418
736
|
const timeoutMs = options.timeoutMs ?? options.timing?.quiesceDeadlineMs ?? DEFAULT_QUIESCE_TIMEOUT_MS;
|
|
737
|
+
const identityProbe = options.identityProbe ?? getProcessIdentityState;
|
|
419
738
|
const deadlineAt = Date.now() + timeoutMs;
|
|
420
739
|
const forceAt = forceDeadline(deadlineAt, timeoutMs);
|
|
421
740
|
const remaining = () => Math.max(0, deadlineAt - Date.now());
|
|
@@ -426,7 +745,7 @@ export async function removeSandboxControlRoot(root, options) {
|
|
|
426
745
|
throw new Error('SANDBOX_CONTROL_CONTAINER_REAPPEARED');
|
|
427
746
|
}
|
|
428
747
|
markSandboxControlRootQuiescing(resolvedRoot);
|
|
429
|
-
await waitForStartupTransition(resolvedRoot, Math.max(0, forceAt - Date.now()));
|
|
748
|
+
await waitForStartupTransition(resolvedRoot, Math.max(0, forceAt - Date.now()), identityProbe);
|
|
430
749
|
const brokerPath = path.join(resolvedRoot, 'broker.json');
|
|
431
750
|
const statusPath = path.join(resolvedRoot, 'public', 'status.json');
|
|
432
751
|
const broker = readBrokerOwner(brokerPath);
|
|
@@ -438,12 +757,12 @@ export async function removeSandboxControlRoot(root, options) {
|
|
|
438
757
|
throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
439
758
|
}
|
|
440
759
|
if (broker && status && !sameOwner(broker, status.broker)
|
|
441
|
-
&& (ownerLive(broker) || ownerLive(status.broker))) {
|
|
760
|
+
&& (ownerLive(broker, identityProbe) || ownerLive(status.broker, identityProbe))) {
|
|
442
761
|
throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
443
762
|
}
|
|
444
763
|
const platform = options.platform ?? process.platform;
|
|
445
|
-
const owner = broker && ownerLive(broker) ? broker
|
|
446
|
-
: status && ownerLive(status.broker) ? status.broker : null;
|
|
764
|
+
const owner = broker && ownerLive(broker, identityProbe) ? broker
|
|
765
|
+
: status && ownerLive(status.broker, identityProbe) ? status.broker : null;
|
|
447
766
|
const selfOwner = options.selfOwner;
|
|
448
767
|
const selfOwned = Boolean(selfOwner && broker && sameOwner(broker, selfOwner));
|
|
449
768
|
if (selfOwner && (!broker || !selfOwned || broker.token !== manifest.token || broker.generation !== manifest.generation)) {
|
|
@@ -453,11 +772,12 @@ export async function removeSandboxControlRoot(root, options) {
|
|
|
453
772
|
if (owner && !selfOwned)
|
|
454
773
|
signalOwner(owner, platform, false);
|
|
455
774
|
for (const execution of executions) {
|
|
456
|
-
terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, allowForce: false });
|
|
775
|
+
terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, allowForce: false, identityProbe });
|
|
457
776
|
}
|
|
458
777
|
const softDeadlineAt = Math.min(forceAt, Date.now() + Math.floor(Math.max(0, forceAt - Date.now()) / 2));
|
|
459
778
|
while (Date.now() < softDeadlineAt) {
|
|
460
|
-
if ((!owner || selfOwned || !ownerLive(owner
|
|
779
|
+
if ((!owner || selfOwned || !ownerLive(owner, identityProbe))
|
|
780
|
+
&& !executions.some((execution) => executionAlive(execution, platform, identityProbe)))
|
|
461
781
|
break;
|
|
462
782
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
463
783
|
}
|
|
@@ -472,7 +792,7 @@ export async function removeSandboxControlRoot(root, options) {
|
|
|
472
792
|
: 'SANDBOX_CONTROL_CONTAINER_STILL_EXISTS');
|
|
473
793
|
}
|
|
474
794
|
}
|
|
475
|
-
const currentManifest =
|
|
795
|
+
const currentManifest = readSandboxControlManifestForTransition(manifestPath);
|
|
476
796
|
if (currentManifest.token !== manifest.token || currentManifest.generation !== manifest.generation
|
|
477
797
|
|| !isSandboxControlRootQuiescing(resolvedRoot)) {
|
|
478
798
|
throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
|
|
@@ -485,10 +805,10 @@ export async function removeSandboxControlRoot(root, options) {
|
|
|
485
805
|
if (owner && currentBroker && !sameOwner(owner, currentBroker)) {
|
|
486
806
|
throw new Error('SANDBOX_CONTROL_OWNER_REPLACED');
|
|
487
807
|
}
|
|
488
|
-
if (!owner && currentBroker && ownerLive(currentBroker)) {
|
|
808
|
+
if (!owner && currentBroker && ownerLive(currentBroker, identityProbe)) {
|
|
489
809
|
throw new Error('SANDBOX_CONTROL_OWNER_REPLACED');
|
|
490
810
|
}
|
|
491
|
-
if (owner && ownerLive(owner) && !currentBroker) {
|
|
811
|
+
if (owner && ownerLive(owner, identityProbe) && !currentBroker) {
|
|
492
812
|
throw new Error('SANDBOX_CONTROL_OWNER_EVIDENCE_MISSING');
|
|
493
813
|
}
|
|
494
814
|
executions = mergeExecutions(executions, readExecutions(manifest));
|
|
@@ -496,21 +816,21 @@ export async function removeSandboxControlRoot(root, options) {
|
|
|
496
816
|
throw new Error('SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED');
|
|
497
817
|
for (const execution of executions) {
|
|
498
818
|
if (!terminateSandboxControlExecution(execution, {
|
|
499
|
-
platform, timeoutMs: remaining(), deadlineAt, forceAt: Date.now()
|
|
819
|
+
platform, timeoutMs: remaining(), deadlineAt, forceAt: Date.now(), identityProbe
|
|
500
820
|
})) {
|
|
501
821
|
throw new Error(`SANDBOX_CONTROL_EXECUTION_STILL_RUNNING: ${execution.requestId}`);
|
|
502
822
|
}
|
|
503
823
|
}
|
|
504
|
-
if (owner && currentBroker && ownerLive(currentBroker) && !selfOwned) {
|
|
824
|
+
if (owner && currentBroker && ownerLive(currentBroker, identityProbe) && !selfOwned) {
|
|
505
825
|
if (remaining() <= 0)
|
|
506
826
|
throw new Error('SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED');
|
|
507
827
|
signalOwner(currentBroker, platform, true);
|
|
508
|
-
if (remaining() <= 0 || !await waitForExit(currentBroker, remaining(), deadlineAt)) {
|
|
828
|
+
if (remaining() <= 0 || !await waitForExit(currentBroker, remaining(), deadlineAt, identityProbe)) {
|
|
509
829
|
throw new Error('SANDBOX_CONTROL_BROKER_STILL_RUNNING');
|
|
510
830
|
}
|
|
511
831
|
}
|
|
512
832
|
const finalExecutions = mergeExecutions(executions, readExecutions(manifest));
|
|
513
|
-
if (finalExecutions.some((execution) => executionAlive(execution, platform))) {
|
|
833
|
+
if (finalExecutions.some((execution) => executionAlive(execution, platform, identityProbe))) {
|
|
514
834
|
throw new Error('SANDBOX_CONTROL_EXECUTION_STILL_RUNNING');
|
|
515
835
|
}
|
|
516
836
|
const finalBrokerRaw = fs.existsSync(brokerPath) ? fs.readFileSync(brokerPath, 'utf8') : null;
|
|
@@ -520,8 +840,9 @@ export async function removeSandboxControlRoot(root, options) {
|
|
|
520
840
|
throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
521
841
|
const finalSelfOwner = Boolean(selfOwner && sameOwner(finalBroker, selfOwner)
|
|
522
842
|
&& finalBroker.token === manifest.token && finalBroker.generation === manifest.generation);
|
|
523
|
-
if (ownerLive(finalBroker) && !finalSelfOwner)
|
|
843
|
+
if (ownerLive(finalBroker, identityProbe) && !finalSelfOwner) {
|
|
524
844
|
throw new Error('SANDBOX_CONTROL_BROKER_STILL_RUNNING');
|
|
845
|
+
}
|
|
525
846
|
if (fs.readFileSync(brokerPath, 'utf8') !== finalBrokerRaw) {
|
|
526
847
|
throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
|
|
527
848
|
}
|
|
@@ -532,7 +853,8 @@ export async function removeSandboxControlRoot(root, options) {
|
|
|
532
853
|
const finalStatus = readStatusOwner(statusPath);
|
|
533
854
|
if (!finalStatus)
|
|
534
855
|
throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
535
|
-
if (ownerLive(finalStatus.broker
|
|
856
|
+
if (ownerLive(finalStatus.broker, identityProbe)
|
|
857
|
+
&& !(selfOwner && sameOwner(finalStatus.broker, selfOwner))) {
|
|
536
858
|
throw new Error('SANDBOX_CONTROL_BROKER_STILL_RUNNING');
|
|
537
859
|
}
|
|
538
860
|
if (fs.readFileSync(statusPath, 'utf8') !== finalStatusRaw) {
|