@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,12 +1,18 @@
|
|
|
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.ts';
|
|
5
|
-
import {
|
|
6
|
-
|
|
6
|
+
import {
|
|
7
|
+
getProcessIdentityState,
|
|
8
|
+
getProcessStartTime
|
|
9
|
+
} from '../../server/process-state.ts';
|
|
10
|
+
import type { ProcessIdentity, ProcessIdentityProbe, ProcessIdentityState } from '../../server/process-state.ts';
|
|
7
11
|
import type {
|
|
8
12
|
SandboxControlExecution,
|
|
13
|
+
SandboxControlManifestLike,
|
|
9
14
|
SandboxControlManifest,
|
|
15
|
+
SandboxControlLegacyManifest,
|
|
10
16
|
SandboxControlStatus,
|
|
11
17
|
SandboxControlTimingPolicy
|
|
12
18
|
} from './protocol.ts';
|
|
@@ -24,6 +30,8 @@ export type BrokerOwner = ProcessIdentity & Readonly<{ version: 3; brokerId: str
|
|
|
24
30
|
const DEFAULT_QUIESCE_TIMEOUT_MS = DEFAULT_SANDBOX_CONTROL_TIMING.quiesceDeadlineMs;
|
|
25
31
|
const QUIESCING_FILE = 'quiescing.json';
|
|
26
32
|
const BROKER_STARTING_FILE = 'broker-starting.json';
|
|
33
|
+
const REPLACEMENT_FILE = 'replacement.json';
|
|
34
|
+
const REPLACEMENT_STATE_SUFFIX = '.replacement-state.json';
|
|
27
35
|
|
|
28
36
|
async function awaitWithDeadline<T>(
|
|
29
37
|
operation: () => Promise<T>,
|
|
@@ -90,13 +98,17 @@ function readStartupOwner(filePath: string): { raw: string; owner: OwnerIdentity
|
|
|
90
98
|
}
|
|
91
99
|
}
|
|
92
100
|
|
|
93
|
-
async function waitForStartupTransition(
|
|
101
|
+
async function waitForStartupTransition(
|
|
102
|
+
root: string,
|
|
103
|
+
timeoutMs: number,
|
|
104
|
+
identityProbe: ProcessIdentityProbe = getProcessIdentityState
|
|
105
|
+
): Promise<void> {
|
|
94
106
|
const transitionPath = path.join(root, BROKER_STARTING_FILE);
|
|
95
107
|
const deadline = Date.now() + timeoutMs;
|
|
96
|
-
while (
|
|
108
|
+
while (true) {
|
|
97
109
|
const transition = readStartupOwner(transitionPath);
|
|
98
110
|
if (!fs.existsSync(transitionPath)) return;
|
|
99
|
-
if (transition && !ownerLive(transition.owner)) {
|
|
111
|
+
if (transition && !ownerLive(transition.owner, identityProbe)) {
|
|
100
112
|
try {
|
|
101
113
|
if (fs.readFileSync(transitionPath, 'utf8') === transition.raw) fs.unlinkSync(transitionPath);
|
|
102
114
|
} catch {
|
|
@@ -104,6 +116,7 @@ async function waitForStartupTransition(root: string, timeoutMs: number): Promis
|
|
|
104
116
|
}
|
|
105
117
|
continue;
|
|
106
118
|
}
|
|
119
|
+
if (Date.now() >= deadline) break;
|
|
107
120
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
108
121
|
}
|
|
109
122
|
throw new Error('SANDBOX_CONTROL_BROKER_START_TRANSITION');
|
|
@@ -112,7 +125,8 @@ async function waitForStartupTransition(root: string, timeoutMs: number): Promis
|
|
|
112
125
|
export async function acquireSandboxControlBrokerStartup(
|
|
113
126
|
root: string,
|
|
114
127
|
owner: OwnerIdentity,
|
|
115
|
-
timeoutMs = 5_000
|
|
128
|
+
timeoutMs = 5_000,
|
|
129
|
+
options: Readonly<{ identityProbe?: ProcessIdentityProbe }> = {}
|
|
116
130
|
): Promise<() => void> {
|
|
117
131
|
const resolvedRoot = path.resolve(root);
|
|
118
132
|
const transitionPath = path.join(resolvedRoot, BROKER_STARTING_FILE);
|
|
@@ -140,7 +154,7 @@ export async function acquireSandboxControlBrokerStartup(
|
|
|
140
154
|
} catch (error) {
|
|
141
155
|
if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;
|
|
142
156
|
const transition = readStartupOwner(transitionPath);
|
|
143
|
-
if (transition && !ownerLive(transition.owner)) {
|
|
157
|
+
if (transition && !ownerLive(transition.owner, options.identityProbe)) {
|
|
144
158
|
try {
|
|
145
159
|
if (fs.readFileSync(transitionPath, 'utf8') === transition.raw) fs.unlinkSync(transitionPath);
|
|
146
160
|
} catch {
|
|
@@ -154,37 +168,381 @@ export async function acquireSandboxControlBrokerStartup(
|
|
|
154
168
|
throw new Error('SANDBOX_CONTROL_BROKER_START_TRANSITION');
|
|
155
169
|
}
|
|
156
170
|
|
|
157
|
-
|
|
171
|
+
type ReplacementRecord = Readonly<{
|
|
172
|
+
version: 1;
|
|
173
|
+
transitionId: string;
|
|
174
|
+
pid: number;
|
|
175
|
+
startTime: number;
|
|
176
|
+
}>;
|
|
177
|
+
|
|
178
|
+
type ReplacementStateRecord = Readonly<{
|
|
179
|
+
version: 1;
|
|
180
|
+
phase: 'prepared' | 'committed';
|
|
181
|
+
root: string;
|
|
182
|
+
snapshotRoot: string;
|
|
183
|
+
transitionId: string;
|
|
184
|
+
}>;
|
|
185
|
+
|
|
186
|
+
export type SandboxControlReplacementCutover = Readonly<{
|
|
187
|
+
root: string;
|
|
188
|
+
snapshotRoot: string;
|
|
189
|
+
transitionId: string;
|
|
190
|
+
}>;
|
|
191
|
+
|
|
192
|
+
export type SandboxControlCutoverSnapshot = Readonly<{
|
|
193
|
+
root: string;
|
|
194
|
+
manifestRaw: string | null;
|
|
195
|
+
brokerRaw: string | null;
|
|
196
|
+
statusRaw: string | null;
|
|
197
|
+
processing: ReadonlyArray<Readonly<{ name: string; directory: boolean; executionRaw: string | null }>>;
|
|
198
|
+
}>;
|
|
199
|
+
|
|
200
|
+
export type SandboxControlReplacementLease = Readonly<{
|
|
201
|
+
root: string;
|
|
202
|
+
transitionId: string;
|
|
203
|
+
owner: ProcessIdentity;
|
|
204
|
+
assertOwned(): void;
|
|
205
|
+
clearQuiescing(): void;
|
|
206
|
+
release(): void;
|
|
207
|
+
}>;
|
|
208
|
+
|
|
209
|
+
function replacementPath(root: string): string {
|
|
210
|
+
return path.join(root, REPLACEMENT_FILE);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function replacementStatePath(root: string): string {
|
|
214
|
+
const resolvedRoot = path.resolve(root);
|
|
215
|
+
return path.join(path.dirname(resolvedRoot), `.${path.basename(resolvedRoot)}${REPLACEMENT_STATE_SUFFIX}`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function replacementSnapshotPath(root: string, transitionId: string): string {
|
|
219
|
+
const resolvedRoot = path.resolve(root);
|
|
220
|
+
return path.join(path.dirname(resolvedRoot), `.${path.basename(resolvedRoot)}.replacement-${transitionId}.snapshot`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function writeAtomicText(filePath: string, content: string): void {
|
|
224
|
+
const temporary = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
225
|
+
try {
|
|
226
|
+
fs.writeFileSync(temporary, content, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
227
|
+
fs.renameSync(temporary, filePath);
|
|
228
|
+
} finally {
|
|
229
|
+
fs.rmSync(temporary, { force: true });
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function readReplacementState(root: string): ReplacementStateRecord | null {
|
|
234
|
+
const filePath = replacementStatePath(root);
|
|
235
|
+
if (!fs.existsSync(filePath)) return null;
|
|
236
|
+
if (!regularFile(filePath)) throw new Error('SANDBOX_CONTROL_REPLACEMENT_RECOVERY_INVALID');
|
|
237
|
+
try {
|
|
238
|
+
const value = JSON.parse(fs.readFileSync(filePath, 'utf8')) as Partial<ReplacementStateRecord>;
|
|
239
|
+
const resolvedRoot = path.resolve(root);
|
|
240
|
+
if (value.version !== 1 || (value.phase !== 'prepared' && value.phase !== 'committed')
|
|
241
|
+
|| value.root !== resolvedRoot || typeof value.transitionId !== 'string' || value.transitionId.length === 0
|
|
242
|
+
|| value.snapshotRoot !== replacementSnapshotPath(resolvedRoot, value.transitionId)) {
|
|
243
|
+
throw new Error('invalid replacement state');
|
|
244
|
+
}
|
|
245
|
+
return value as ReplacementStateRecord;
|
|
246
|
+
} catch {
|
|
247
|
+
throw new Error('SANDBOX_CONTROL_REPLACEMENT_RECOVERY_INVALID');
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function writeReplacementState(state: ReplacementStateRecord): void {
|
|
252
|
+
writeAtomicText(replacementStatePath(state.root), `${JSON.stringify(state)}\n`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function removeReplacementState(root: string): void {
|
|
256
|
+
fs.rmSync(replacementStatePath(root), { force: true });
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function readReplacementRecord(filePath: string): { raw: string; record: ReplacementRecord } | null {
|
|
260
|
+
if (!fs.existsSync(filePath)) return null;
|
|
261
|
+
if (!regularFile(filePath)) throw new Error('SANDBOX_CONTROL_REPLACEMENT_INVALID');
|
|
262
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
263
|
+
try {
|
|
264
|
+
const value = JSON.parse(raw) as Partial<ReplacementRecord>;
|
|
265
|
+
if (value.version !== 1 || typeof value.transitionId !== 'string' || value.transitionId.length === 0
|
|
266
|
+
|| !Number.isSafeInteger(value.pid) || (value.pid ?? 0) <= 0
|
|
267
|
+
|| !Number.isSafeInteger(value.startTime)) throw new Error('invalid replacement');
|
|
268
|
+
return { raw, record: value as ReplacementRecord };
|
|
269
|
+
} catch {
|
|
270
|
+
throw new Error('SANDBOX_CONTROL_REPLACEMENT_INVALID');
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function acquireSandboxControlReplacement(
|
|
275
|
+
root: string,
|
|
276
|
+
options: Readonly<{
|
|
277
|
+
owner?: ProcessIdentity;
|
|
278
|
+
probeOwner?: (identity: ProcessIdentity) => ProcessIdentityState;
|
|
279
|
+
}> = {}
|
|
280
|
+
): SandboxControlReplacementLease {
|
|
281
|
+
const resolvedRoot = path.resolve(root);
|
|
282
|
+
if (fs.existsSync(resolvedRoot)) {
|
|
283
|
+
const stat = fs.lstatSync(resolvedRoot);
|
|
284
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error('SANDBOX_CONTROL_CHANNEL_INVALID');
|
|
285
|
+
} else {
|
|
286
|
+
fs.mkdirSync(resolvedRoot, { recursive: true, mode: 0o700 });
|
|
287
|
+
}
|
|
288
|
+
const startTime = options.owner?.startTime ?? getProcessStartTime(process.pid);
|
|
289
|
+
if (startTime === null || startTime === undefined) throw new Error('SANDBOX_CONTROL_REPLACEMENT_OWNER_UNAVAILABLE');
|
|
290
|
+
const owner = options.owner ?? { pid: process.pid, startTime };
|
|
291
|
+
const transitionId = randomUUID();
|
|
292
|
+
const record = `${JSON.stringify({ version: 1, transitionId, ...owner })}\n`;
|
|
293
|
+
const filePath = replacementPath(resolvedRoot);
|
|
294
|
+
const existing = readReplacementRecord(filePath);
|
|
295
|
+
if (existing) {
|
|
296
|
+
const ownerState = (options.probeOwner ?? getProcessIdentityState)(existing.record);
|
|
297
|
+
if (ownerState === 'alive') {
|
|
298
|
+
throw new Error('SANDBOX_CONTROL_REPLACEMENT_BUSY');
|
|
299
|
+
}
|
|
300
|
+
if (ownerState === 'unknown') {
|
|
301
|
+
throw new Error('SANDBOX_CONTROL_REPLACEMENT_OWNER_UNAVAILABLE');
|
|
302
|
+
}
|
|
303
|
+
if (fs.readFileSync(filePath, 'utf8') !== existing.raw) {
|
|
304
|
+
throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
|
|
305
|
+
}
|
|
306
|
+
fs.unlinkSync(filePath);
|
|
307
|
+
}
|
|
308
|
+
try {
|
|
309
|
+
fs.writeFileSync(filePath, record, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
310
|
+
} catch (error) {
|
|
311
|
+
if ((error as NodeJS.ErrnoException).code === 'EEXIST') {
|
|
312
|
+
throw new Error('SANDBOX_CONTROL_REPLACEMENT_BUSY');
|
|
313
|
+
}
|
|
314
|
+
throw error;
|
|
315
|
+
}
|
|
316
|
+
let released = false;
|
|
317
|
+
const verifyOwnership = (): void => {
|
|
318
|
+
if (released) throw new Error('SANDBOX_CONTROL_REPLACEMENT_RELEASED');
|
|
319
|
+
const current = readReplacementRecord(filePath);
|
|
320
|
+
if (!current || current.raw !== record || current.record.transitionId !== transitionId) {
|
|
321
|
+
throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
return {
|
|
325
|
+
root: resolvedRoot,
|
|
326
|
+
transitionId,
|
|
327
|
+
owner,
|
|
328
|
+
assertOwned: verifyOwnership,
|
|
329
|
+
clearQuiescing(): void {
|
|
330
|
+
verifyOwnership();
|
|
331
|
+
const markerPath = path.join(resolvedRoot, QUIESCING_FILE);
|
|
332
|
+
if (fs.existsSync(markerPath)) {
|
|
333
|
+
if (!regularFile(markerPath)) throw new Error('SANDBOX_CONTROL_CHANNEL_INVALID');
|
|
334
|
+
fs.unlinkSync(markerPath);
|
|
335
|
+
}
|
|
336
|
+
},
|
|
337
|
+
release(): void {
|
|
338
|
+
if (released) return;
|
|
339
|
+
verifyOwnership();
|
|
340
|
+
fs.unlinkSync(filePath);
|
|
341
|
+
released = true;
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function readReplacementRaw(root: string): string {
|
|
347
|
+
const current = readReplacementRecord(replacementPath(root));
|
|
348
|
+
if (!current) throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
|
|
349
|
+
return current.raw;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export function beginSandboxControlReplacement(
|
|
353
|
+
root: string,
|
|
354
|
+
lease: Readonly<{ root: string; transitionId: string; assertOwned(): void }>
|
|
355
|
+
): SandboxControlReplacementCutover {
|
|
356
|
+
const resolvedRoot = path.resolve(root);
|
|
357
|
+
if (path.resolve(lease.root) !== resolvedRoot) throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
|
|
358
|
+
lease.assertOwned();
|
|
359
|
+
if (!fs.existsSync(resolvedRoot)) throw new Error('SANDBOX_CONTROL_CHANNEL_INVALID');
|
|
360
|
+
const rootStat = fs.lstatSync(resolvedRoot);
|
|
361
|
+
if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) throw new Error('SANDBOX_CONTROL_CHANNEL_INVALID');
|
|
362
|
+
if (readReplacementState(resolvedRoot)) throw new Error('SANDBOX_CONTROL_REPLACEMENT_RECOVERY_REQUIRED');
|
|
363
|
+
const snapshotRoot = replacementSnapshotPath(resolvedRoot, lease.transitionId);
|
|
364
|
+
if (fs.existsSync(snapshotRoot)) throw new Error('SANDBOX_CONTROL_REPLACEMENT_RECOVERY_INVALID');
|
|
365
|
+
const replacementRaw = readReplacementRaw(resolvedRoot);
|
|
366
|
+
const state: ReplacementStateRecord = {
|
|
367
|
+
version: 1,
|
|
368
|
+
phase: 'prepared',
|
|
369
|
+
root: resolvedRoot,
|
|
370
|
+
snapshotRoot,
|
|
371
|
+
transitionId: lease.transitionId
|
|
372
|
+
};
|
|
373
|
+
writeReplacementState(state);
|
|
374
|
+
let moved = false;
|
|
375
|
+
try {
|
|
376
|
+
fs.renameSync(resolvedRoot, snapshotRoot);
|
|
377
|
+
moved = true;
|
|
378
|
+
fs.mkdirSync(resolvedRoot, { recursive: true, mode: 0o700 });
|
|
379
|
+
fs.writeFileSync(replacementPath(resolvedRoot), replacementRaw, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
380
|
+
markSandboxControlRootQuiescing(resolvedRoot);
|
|
381
|
+
return { root: resolvedRoot, snapshotRoot, transitionId: lease.transitionId };
|
|
382
|
+
} catch (error) {
|
|
383
|
+
if (moved) {
|
|
384
|
+
fs.rmSync(resolvedRoot, { recursive: true, force: true });
|
|
385
|
+
fs.renameSync(snapshotRoot, resolvedRoot);
|
|
386
|
+
}
|
|
387
|
+
removeReplacementState(resolvedRoot);
|
|
388
|
+
throw error;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function isRecoverableCutoverManifestError(error: unknown): boolean {
|
|
393
|
+
return error instanceof Error
|
|
394
|
+
&& (error.message.startsWith('SANDBOX_CONTROL_MANIFEST_')
|
|
395
|
+
|| error.message === 'SANDBOX_CONTROL_OWNER_EVIDENCE_MISSING');
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export async function recoverSandboxControlReplacement(
|
|
399
|
+
root: string,
|
|
400
|
+
lease: Readonly<{ root: string; assertOwned(): void }>
|
|
401
|
+
): Promise<'none' | 'restored' | 'committed'> {
|
|
402
|
+
const resolvedRoot = path.resolve(root);
|
|
403
|
+
if (path.resolve(lease.root) !== resolvedRoot) throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
|
|
404
|
+
const state = readReplacementState(resolvedRoot);
|
|
405
|
+
if (!state) return 'none';
|
|
406
|
+
lease.assertOwned();
|
|
407
|
+
if (state.phase === 'committed') {
|
|
408
|
+
fs.rmSync(state.snapshotRoot, { recursive: true, force: true });
|
|
409
|
+
removeReplacementState(resolvedRoot);
|
|
410
|
+
return 'committed';
|
|
411
|
+
}
|
|
412
|
+
if (!fs.existsSync(state.snapshotRoot)) {
|
|
413
|
+
removeReplacementState(resolvedRoot);
|
|
414
|
+
return 'none';
|
|
415
|
+
}
|
|
416
|
+
if (fs.existsSync(resolvedRoot) && fs.existsSync(path.join(resolvedRoot, 'manifest.json'))) {
|
|
417
|
+
try {
|
|
418
|
+
await quiesceSandboxControlRoot(resolvedRoot);
|
|
419
|
+
} catch (error) {
|
|
420
|
+
if (!isRecoverableCutoverManifestError(error)) throw error;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
lease.assertOwned();
|
|
424
|
+
const replacementRaw = readReplacementRaw(resolvedRoot);
|
|
425
|
+
fs.rmSync(resolvedRoot, { recursive: true, force: true });
|
|
426
|
+
fs.renameSync(state.snapshotRoot, resolvedRoot);
|
|
427
|
+
writeAtomicText(replacementPath(resolvedRoot), replacementRaw);
|
|
428
|
+
removeReplacementState(resolvedRoot);
|
|
429
|
+
return 'restored';
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
export function commitSandboxControlReplacement(
|
|
433
|
+
root: string,
|
|
434
|
+
lease: Readonly<{ root: string; assertOwned(): void }>
|
|
435
|
+
): void {
|
|
436
|
+
const resolvedRoot = path.resolve(root);
|
|
437
|
+
if (path.resolve(lease.root) !== resolvedRoot) throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
|
|
438
|
+
const state = readReplacementState(resolvedRoot);
|
|
439
|
+
if (!state) return;
|
|
440
|
+
lease.assertOwned();
|
|
441
|
+
writeReplacementState({ ...state, phase: 'committed' });
|
|
442
|
+
fs.rmSync(state.snapshotRoot, { recursive: true, force: true });
|
|
443
|
+
removeReplacementState(resolvedRoot);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function readOptionalRaw(filePath: string): string | null {
|
|
447
|
+
if (!fs.existsSync(filePath)) return null;
|
|
448
|
+
if (!regularFile(filePath)) throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
|
|
449
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function readProcessingSnapshot(root: string): SandboxControlCutoverSnapshot['processing'] {
|
|
453
|
+
const processingDir = path.join(root, 'processing');
|
|
454
|
+
if (!fs.existsSync(processingDir)) return [];
|
|
455
|
+
const stat = fs.lstatSync(processingDir);
|
|
456
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error('SANDBOX_CONTROL_EXECUTION_STILL_RUNNING');
|
|
457
|
+
return fs.readdirSync(processingDir, { withFileTypes: true })
|
|
458
|
+
.map((entry) => ({
|
|
459
|
+
name: entry.name,
|
|
460
|
+
directory: entry.isDirectory() && !entry.isSymbolicLink(),
|
|
461
|
+
executionRaw: entry.isDirectory() && !entry.isSymbolicLink()
|
|
462
|
+
? readOptionalRaw(path.join(processingDir, entry.name, 'execution.json'))
|
|
463
|
+
: null
|
|
464
|
+
}))
|
|
465
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export function captureSandboxControlCutoverSnapshot(root: string): SandboxControlCutoverSnapshot {
|
|
469
|
+
const resolvedRoot = path.resolve(root);
|
|
470
|
+
const manifestPath = path.join(resolvedRoot, 'manifest.json');
|
|
471
|
+
if (fs.existsSync(manifestPath)) readSandboxControlManifestForTransition(manifestPath);
|
|
472
|
+
return {
|
|
473
|
+
root: resolvedRoot,
|
|
474
|
+
manifestRaw: readOptionalRaw(manifestPath),
|
|
475
|
+
brokerRaw: readOptionalRaw(path.join(resolvedRoot, 'broker.json')),
|
|
476
|
+
statusRaw: readOptionalRaw(path.join(resolvedRoot, 'public', 'status.json')),
|
|
477
|
+
processing: readProcessingSnapshot(resolvedRoot)
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
export function assertSandboxControlCutoverSnapshot(snapshot: SandboxControlCutoverSnapshot): void {
|
|
482
|
+
const current = captureSandboxControlCutoverSnapshot(snapshot.root);
|
|
483
|
+
if (JSON.stringify(current) !== JSON.stringify(snapshot)) {
|
|
484
|
+
throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function readSandboxControlManifestValue(
|
|
489
|
+
manifestPath: string,
|
|
490
|
+
allowLegacy: boolean
|
|
491
|
+
): SandboxControlManifestLike {
|
|
158
492
|
if (!regularFile(manifestPath)) throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
159
|
-
let manifest:
|
|
493
|
+
let manifest: Record<string, unknown>;
|
|
160
494
|
try {
|
|
161
|
-
|
|
495
|
+
const parsed = JSON.parse(fs.readFileSync(manifestPath, 'utf8')) as unknown;
|
|
496
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
497
|
+
throw new Error('invalid manifest');
|
|
498
|
+
}
|
|
499
|
+
manifest = parsed as Record<string, unknown>;
|
|
162
500
|
} catch {
|
|
163
501
|
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
164
502
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
||
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|| typeof
|
|
174
|
-
||
|
|
175
|
-
||
|
|
176
|
-
|| typeof
|
|
177
|
-
|| (
|
|
178
|
-
||
|
|
179
|
-
|| typeof
|
|
180
|
-
|| typeof
|
|
503
|
+
const candidate = manifest as Partial<SandboxControlManifest> & Partial<SandboxControlLegacyManifest>;
|
|
504
|
+
if (candidate.version !== 4 && candidate.version !== 5) {
|
|
505
|
+
throw new Error('SANDBOX_CONTROL_MANIFEST_VERSION_INVALID: expected version 5; container-only recreation is required');
|
|
506
|
+
}
|
|
507
|
+
if (typeof candidate.containerIdentity !== 'object' || candidate.containerIdentity === null
|
|
508
|
+
|| Array.isArray(candidate.containerIdentity)) throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
509
|
+
const containerIdentity = candidate.containerIdentity as Record<string, unknown>;
|
|
510
|
+
if (typeof candidate.engine !== 'string' || candidate.engine.length === 0
|
|
511
|
+
|| typeof candidate.repoRoot !== 'string' || typeof candidate.worktreeRoot !== 'string'
|
|
512
|
+
|| typeof candidate.project !== 'string' || typeof candidate.container !== 'string'
|
|
513
|
+
|| typeof containerIdentity.id !== 'string' || containerIdentity.id.length === 0
|
|
514
|
+
|| typeof containerIdentity.labels !== 'object' || containerIdentity.labels === null
|
|
515
|
+
|| Array.isArray(containerIdentity.labels)
|
|
516
|
+
|| Object.values(containerIdentity.labels).some((value) => typeof value !== 'string')
|
|
517
|
+
|| typeof candidate.branch !== 'string' || !['task-bound', 'branch-only'].includes(candidate.mode ?? '')
|
|
518
|
+
|| (candidate.taskId !== null && typeof candidate.taskId !== 'string')
|
|
519
|
+
|| typeof candidate.channelDir !== 'string' || typeof candidate.publicStatusDir !== 'string'
|
|
520
|
+
|| typeof candidate.processingDir !== 'string' || typeof candidate.token !== 'string'
|
|
521
|
+
|| typeof candidate.generation !== 'string') throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
181
522
|
const root = path.dirname(path.resolve(manifestPath));
|
|
182
|
-
if (path.resolve(
|
|
183
|
-
|| path.resolve(
|
|
184
|
-
|| path.resolve(
|
|
523
|
+
if (path.resolve(candidate.channelDir) !== path.join(root, 'channel')
|
|
524
|
+
|| path.resolve(candidate.publicStatusDir) !== path.join(root, 'public')
|
|
525
|
+
|| path.resolve(candidate.processingDir) !== path.join(root, 'processing')) {
|
|
185
526
|
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
186
527
|
}
|
|
187
|
-
|
|
528
|
+
if (candidate.version === 4) {
|
|
529
|
+
if (!allowLegacy) {
|
|
530
|
+
throw new Error('SANDBOX_CONTROL_MANIFEST_VERSION_INVALID: expected version 5; container-only recreation is required');
|
|
531
|
+
}
|
|
532
|
+
return candidate as unknown as SandboxControlLegacyManifest;
|
|
533
|
+
}
|
|
534
|
+
if (typeof candidate.runtimeDir !== 'string' || path.resolve(candidate.runtimeDir) !== path.join(root, 'runtime')) {
|
|
535
|
+
throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
|
|
536
|
+
}
|
|
537
|
+
return candidate as unknown as SandboxControlManifest;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
export function readSandboxControlManifest(manifestPath: string): SandboxControlManifest {
|
|
541
|
+
return readSandboxControlManifestValue(manifestPath, false) as SandboxControlManifest;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
export function readSandboxControlManifestForTransition(manifestPath: string): SandboxControlManifestLike {
|
|
545
|
+
return readSandboxControlManifestValue(manifestPath, true);
|
|
188
546
|
}
|
|
189
547
|
|
|
190
548
|
function readBrokerOwner(filePath: string): BrokerOwner | null {
|
|
@@ -218,11 +576,16 @@ function sameOwner(left: OwnerIdentity, right: OwnerIdentity): boolean {
|
|
|
218
576
|
&& (!left.brokerId || !right.brokerId || left.brokerId === right.brokerId);
|
|
219
577
|
}
|
|
220
578
|
|
|
221
|
-
function ownerLive(
|
|
222
|
-
|
|
579
|
+
function ownerLive(
|
|
580
|
+
owner: OwnerIdentity,
|
|
581
|
+
identityProbe: ProcessIdentityProbe = getProcessIdentityState
|
|
582
|
+
): boolean {
|
|
583
|
+
const state = identityProbe({ pid: owner.pid, startTime: owner.startTime });
|
|
584
|
+
if (state === 'unknown') throw new Error('SANDBOX_CONTROL_OWNER_UNAVAILABLE');
|
|
585
|
+
return state === 'alive';
|
|
223
586
|
}
|
|
224
587
|
|
|
225
|
-
function readExecutions(manifest:
|
|
588
|
+
function readExecutions(manifest: SandboxControlManifestLike): SandboxControlExecution[] {
|
|
226
589
|
if (!fs.existsSync(manifest.processingDir)) return [];
|
|
227
590
|
const stat = fs.lstatSync(manifest.processingDir);
|
|
228
591
|
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error('SANDBOX_CONTROL_EXECUTION_STILL_RUNNING');
|
|
@@ -249,37 +612,50 @@ function readExecutions(manifest: SandboxControlManifest): SandboxControlExecuti
|
|
|
249
612
|
return executions;
|
|
250
613
|
}
|
|
251
614
|
|
|
252
|
-
function
|
|
615
|
+
function processGroupState(groupId: number): ProcessIdentityState {
|
|
253
616
|
try {
|
|
254
617
|
const rows = execFileSync('ps', ['-axo', 'pgid=,stat='], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
255
618
|
return rows.split('\n').some((row) => {
|
|
256
619
|
const match = row.trim().match(/^(\d+)\s+(\S+)/);
|
|
257
620
|
return Number(match?.[1]) === groupId && !match?.[2]?.startsWith('Z');
|
|
258
|
-
});
|
|
621
|
+
}) ? 'alive' : 'dead';
|
|
259
622
|
} catch {
|
|
260
623
|
try {
|
|
261
624
|
process.kill(-groupId, 0);
|
|
262
|
-
return
|
|
625
|
+
return 'alive';
|
|
263
626
|
} catch (error) {
|
|
264
|
-
|
|
627
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
628
|
+
if (code === 'ESRCH') return 'dead';
|
|
629
|
+
if (code === 'EPERM') return 'alive';
|
|
630
|
+
return 'unknown';
|
|
265
631
|
}
|
|
266
632
|
}
|
|
267
633
|
}
|
|
268
634
|
|
|
269
|
-
function executionAlive(
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
635
|
+
function executionAlive(
|
|
636
|
+
execution: SandboxControlExecution,
|
|
637
|
+
platform: NodeJS.Platform,
|
|
638
|
+
identityProbe: ProcessIdentityProbe = getProcessIdentityState
|
|
639
|
+
): boolean {
|
|
640
|
+
const state = platform !== 'win32' && execution.child.processGroupId
|
|
641
|
+
? processGroupState(execution.child.processGroupId)
|
|
642
|
+
: identityProbe({ pid: execution.child.pid, startTime: execution.child.startTime });
|
|
643
|
+
if (state === 'unknown') throw new Error('SANDBOX_CONTROL_EXECUTION_OWNER_UNAVAILABLE');
|
|
644
|
+
return state === 'alive';
|
|
274
645
|
}
|
|
275
646
|
|
|
276
|
-
async function waitForExit(
|
|
647
|
+
async function waitForExit(
|
|
648
|
+
owner: OwnerIdentity,
|
|
649
|
+
timeoutMs: number,
|
|
650
|
+
deadlineAt?: number,
|
|
651
|
+
identityProbe: ProcessIdentityProbe = getProcessIdentityState
|
|
652
|
+
): Promise<boolean> {
|
|
277
653
|
const deadline = deadlineAt ?? (Date.now() + timeoutMs);
|
|
278
654
|
while (Date.now() < deadline) {
|
|
279
|
-
if (!ownerLive(owner)) return true;
|
|
655
|
+
if (!ownerLive(owner, identityProbe)) return true;
|
|
280
656
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
281
657
|
}
|
|
282
|
-
return !ownerLive(owner);
|
|
658
|
+
return !ownerLive(owner, identityProbe);
|
|
283
659
|
}
|
|
284
660
|
|
|
285
661
|
function signalOwner(owner: OwnerIdentity, platform: NodeJS.Platform, force: boolean): void {
|
|
@@ -303,7 +679,12 @@ function mergeExecutions(...groups: readonly SandboxControlExecution[][]): Sandb
|
|
|
303
679
|
|
|
304
680
|
export async function quiesceSandboxControlRoot(
|
|
305
681
|
root: string,
|
|
306
|
-
options: {
|
|
682
|
+
options: {
|
|
683
|
+
platform?: NodeJS.Platform;
|
|
684
|
+
timeoutMs?: number;
|
|
685
|
+
timing?: SandboxControlTimingPolicy;
|
|
686
|
+
identityProbe?: ProcessIdentityProbe;
|
|
687
|
+
} = {}
|
|
307
688
|
): Promise<'missing' | 'stale' | 'stopped'> {
|
|
308
689
|
const resolvedRoot = path.resolve(root);
|
|
309
690
|
if (!fs.existsSync(resolvedRoot)) return 'missing';
|
|
@@ -317,18 +698,19 @@ export async function quiesceSandboxControlRoot(
|
|
|
317
698
|
}
|
|
318
699
|
|
|
319
700
|
const timeoutMs = options.timeoutMs ?? options.timing?.quiesceDeadlineMs ?? DEFAULT_QUIESCE_TIMEOUT_MS;
|
|
701
|
+
const identityProbe = options.identityProbe ?? getProcessIdentityState;
|
|
320
702
|
const deadlineAt = Date.now() + timeoutMs;
|
|
321
703
|
const forceAt = forceDeadline(deadlineAt, timeoutMs);
|
|
322
704
|
markSandboxControlRootQuiescing(resolvedRoot);
|
|
323
|
-
await waitForStartupTransition(resolvedRoot, Math.max(0, forceAt - Date.now()));
|
|
705
|
+
await waitForStartupTransition(resolvedRoot, Math.max(0, forceAt - Date.now()), identityProbe);
|
|
324
706
|
|
|
325
707
|
const manifestPath = path.join(resolvedRoot, 'manifest.json');
|
|
326
|
-
const manifest = fs.existsSync(manifestPath) ?
|
|
708
|
+
const manifest = fs.existsSync(manifestPath) ? readSandboxControlManifestForTransition(manifestPath) : null;
|
|
327
709
|
const broker = readBrokerOwner(path.join(resolvedRoot, 'broker.json'));
|
|
328
710
|
const status = readStatusOwner(path.join(resolvedRoot, 'public', 'status.json'));
|
|
329
711
|
const statusOwner = status ? status.broker : null;
|
|
330
|
-
const brokerLive = broker ? ownerLive(broker) : false;
|
|
331
|
-
const statusLive = statusOwner ? ownerLive(statusOwner) : false;
|
|
712
|
+
const brokerLive = broker ? ownerLive(broker, identityProbe) : false;
|
|
713
|
+
const statusLive = statusOwner ? ownerLive(statusOwner, identityProbe) : false;
|
|
332
714
|
|
|
333
715
|
if (manifest && broker && (broker.token !== manifest.token || broker.generation !== manifest.generation)) {
|
|
334
716
|
throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
@@ -347,23 +729,23 @@ export async function quiesceSandboxControlRoot(
|
|
|
347
729
|
if (!owner) {
|
|
348
730
|
if (manifest && !broker && !status) throw new Error('SANDBOX_CONTROL_OWNER_EVIDENCE_MISSING');
|
|
349
731
|
for (const execution of executions) {
|
|
350
|
-
terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, allowForce: false });
|
|
732
|
+
terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, allowForce: false, identityProbe });
|
|
351
733
|
}
|
|
352
734
|
const softDeadlineAt = Math.min(forceAt, Date.now() + Math.floor(Math.max(0, forceAt - Date.now()) / 2));
|
|
353
735
|
while (Date.now() < softDeadlineAt) {
|
|
354
|
-
if (!executions.some((execution) => executionAlive(execution, platform))) break;
|
|
736
|
+
if (!executions.some((execution) => executionAlive(execution, platform, identityProbe))) break;
|
|
355
737
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
356
738
|
}
|
|
357
739
|
if (manifest) executions = mergeExecutions(executions, readExecutions(manifest));
|
|
358
740
|
if (Date.now() >= deadlineAt) throw new Error('SANDBOX_CONTROL_QUIESCE_DEADLINE_EXCEEDED');
|
|
359
741
|
for (const execution of executions) {
|
|
360
742
|
if (!terminateSandboxControlExecution(execution, {
|
|
361
|
-
platform, timeoutMs: Math.max(0, deadlineAt - Date.now()), deadlineAt, forceAt: Date.now()
|
|
743
|
+
platform, timeoutMs: Math.max(0, deadlineAt - Date.now()), deadlineAt, forceAt: Date.now(), identityProbe
|
|
362
744
|
})) {
|
|
363
745
|
throw new Error(`SANDBOX_CONTROL_EXECUTION_STILL_RUNNING: ${execution.requestId}`);
|
|
364
746
|
}
|
|
365
747
|
}
|
|
366
|
-
if (executions.some((execution) => executionAlive(execution, platform))) {
|
|
748
|
+
if (executions.some((execution) => executionAlive(execution, platform, identityProbe))) {
|
|
367
749
|
throw new Error('SANDBOX_CONTROL_EXECUTION_STILL_RUNNING');
|
|
368
750
|
}
|
|
369
751
|
return broker || status ? 'stale' : 'missing';
|
|
@@ -372,28 +754,28 @@ export async function quiesceSandboxControlRoot(
|
|
|
372
754
|
const remaining = (): number => Math.max(0, deadlineAt - Date.now());
|
|
373
755
|
signalOwner(owner, platform, false);
|
|
374
756
|
for (const execution of executions) {
|
|
375
|
-
terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, allowForce: false });
|
|
757
|
+
terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, allowForce: false, identityProbe });
|
|
376
758
|
}
|
|
377
759
|
const softDeadlineAt = Math.min(forceAt, Date.now() + Math.floor(Math.max(0, forceAt - Date.now()) / 2));
|
|
378
760
|
while (Date.now() < softDeadlineAt) {
|
|
379
|
-
if (!ownerLive(owner) && !executions.some((execution) => executionAlive(execution, platform))) break;
|
|
761
|
+
if (!ownerLive(owner, identityProbe) && !executions.some((execution) => executionAlive(execution, platform, identityProbe))) break;
|
|
380
762
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
381
763
|
}
|
|
382
|
-
if (!ownerLive(owner) && !executions.some((execution) => executionAlive(execution, platform))) {
|
|
764
|
+
if (!ownerLive(owner, identityProbe) && !executions.some((execution) => executionAlive(execution, platform, identityProbe))) {
|
|
383
765
|
return 'stopped';
|
|
384
766
|
}
|
|
385
767
|
if (manifest) executions = mergeExecutions(executions, readExecutions(manifest));
|
|
386
768
|
if (Date.now() >= deadlineAt) throw new Error('SANDBOX_CONTROL_QUIESCE_DEADLINE_EXCEEDED');
|
|
387
769
|
for (const execution of executions) {
|
|
388
|
-
terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, deadlineAt, forceAt: Date.now() });
|
|
770
|
+
terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, deadlineAt, forceAt: Date.now(), identityProbe });
|
|
389
771
|
}
|
|
390
772
|
signalOwner(owner, platform, true);
|
|
391
773
|
while (Date.now() < deadlineAt) {
|
|
392
|
-
if (!ownerLive(owner) && !executions.some((execution) => executionAlive(execution, platform))) return 'stopped';
|
|
774
|
+
if (!ownerLive(owner, identityProbe) && !executions.some((execution) => executionAlive(execution, platform, identityProbe))) return 'stopped';
|
|
393
775
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
394
776
|
}
|
|
395
|
-
if (ownerLive(owner)) throw new Error('SANDBOX_CONTROL_BROKER_STILL_RUNNING');
|
|
396
|
-
if (executions.some((execution) => executionAlive(execution, platform))) throw new Error('SANDBOX_CONTROL_EXECUTION_STILL_RUNNING');
|
|
777
|
+
if (ownerLive(owner, identityProbe)) throw new Error('SANDBOX_CONTROL_BROKER_STILL_RUNNING');
|
|
778
|
+
if (executions.some((execution) => executionAlive(execution, platform, identityProbe))) throw new Error('SANDBOX_CONTROL_EXECUTION_STILL_RUNNING');
|
|
397
779
|
return 'stopped';
|
|
398
780
|
}
|
|
399
781
|
|
|
@@ -401,6 +783,7 @@ export type RemoveSandboxControlOptions = Readonly<{
|
|
|
401
783
|
platform?: NodeJS.Platform;
|
|
402
784
|
timeoutMs?: number;
|
|
403
785
|
timing?: SandboxControlTimingPolicy;
|
|
786
|
+
identityProbe?: ProcessIdentityProbe;
|
|
404
787
|
inspectContainer?: (timeoutMs: number) => Promise<ContainerObservation>;
|
|
405
788
|
removeContainer: (timeoutMs: number) => Promise<void>;
|
|
406
789
|
requireAbsent?: boolean;
|
|
@@ -416,10 +799,11 @@ export async function removeSandboxControlRoot(
|
|
|
416
799
|
const stat = fs.lstatSync(resolvedRoot);
|
|
417
800
|
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error('SANDBOX_CONTROL_CHANNEL_INVALID');
|
|
418
801
|
const manifestPath = path.join(resolvedRoot, 'manifest.json');
|
|
419
|
-
const manifest =
|
|
802
|
+
const manifest = readSandboxControlManifestForTransition(manifestPath);
|
|
420
803
|
const inspectContainer = options.inspectContainer
|
|
421
804
|
?? ((timeoutMs) => inspectSandboxControlContainer(manifest, { timeoutMs }));
|
|
422
805
|
const timeoutMs = options.timeoutMs ?? options.timing?.quiesceDeadlineMs ?? DEFAULT_QUIESCE_TIMEOUT_MS;
|
|
806
|
+
const identityProbe = options.identityProbe ?? getProcessIdentityState;
|
|
423
807
|
const deadlineAt = Date.now() + timeoutMs;
|
|
424
808
|
const forceAt = forceDeadline(deadlineAt, timeoutMs);
|
|
425
809
|
const remaining = (): number => Math.max(0, deadlineAt - Date.now());
|
|
@@ -432,7 +816,7 @@ export async function removeSandboxControlRoot(
|
|
|
432
816
|
}
|
|
433
817
|
|
|
434
818
|
markSandboxControlRootQuiescing(resolvedRoot);
|
|
435
|
-
await waitForStartupTransition(resolvedRoot, Math.max(0, forceAt - Date.now()));
|
|
819
|
+
await waitForStartupTransition(resolvedRoot, Math.max(0, forceAt - Date.now()), identityProbe);
|
|
436
820
|
const brokerPath = path.join(resolvedRoot, 'broker.json');
|
|
437
821
|
const statusPath = path.join(resolvedRoot, 'public', 'status.json');
|
|
438
822
|
const broker = readBrokerOwner(brokerPath);
|
|
@@ -444,12 +828,12 @@ export async function removeSandboxControlRoot(
|
|
|
444
828
|
throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
445
829
|
}
|
|
446
830
|
if (broker && status && !sameOwner(broker, status.broker)
|
|
447
|
-
&& (ownerLive(broker) || ownerLive(status.broker))) {
|
|
831
|
+
&& (ownerLive(broker, identityProbe) || ownerLive(status.broker, identityProbe))) {
|
|
448
832
|
throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
449
833
|
}
|
|
450
834
|
const platform = options.platform ?? process.platform;
|
|
451
|
-
const owner = broker && ownerLive(broker) ? broker
|
|
452
|
-
: status && ownerLive(status.broker) ? status.broker : null;
|
|
835
|
+
const owner = broker && ownerLive(broker, identityProbe) ? broker
|
|
836
|
+
: status && ownerLive(status.broker, identityProbe) ? status.broker : null;
|
|
453
837
|
const selfOwner = options.selfOwner;
|
|
454
838
|
const selfOwned = Boolean(selfOwner && broker && sameOwner(broker, selfOwner));
|
|
455
839
|
if (selfOwner && (!broker || !selfOwned || broker.token !== manifest.token || broker.generation !== manifest.generation)) {
|
|
@@ -458,11 +842,12 @@ export async function removeSandboxControlRoot(
|
|
|
458
842
|
let executions = readExecutions(manifest);
|
|
459
843
|
if (owner && !selfOwned) signalOwner(owner, platform, false);
|
|
460
844
|
for (const execution of executions) {
|
|
461
|
-
terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, allowForce: false });
|
|
845
|
+
terminateSandboxControlExecution(execution, { platform, timeoutMs: 0, allowForce: false, identityProbe });
|
|
462
846
|
}
|
|
463
847
|
const softDeadlineAt = Math.min(forceAt, Date.now() + Math.floor(Math.max(0, forceAt - Date.now()) / 2));
|
|
464
848
|
while (Date.now() < softDeadlineAt) {
|
|
465
|
-
if ((!owner || selfOwned || !ownerLive(owner
|
|
849
|
+
if ((!owner || selfOwned || !ownerLive(owner, identityProbe))
|
|
850
|
+
&& !executions.some((execution) => executionAlive(execution, platform, identityProbe))) break;
|
|
466
851
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
467
852
|
}
|
|
468
853
|
|
|
@@ -485,7 +870,7 @@ export async function removeSandboxControlRoot(
|
|
|
485
870
|
}
|
|
486
871
|
}
|
|
487
872
|
|
|
488
|
-
const currentManifest =
|
|
873
|
+
const currentManifest = readSandboxControlManifestForTransition(manifestPath);
|
|
489
874
|
if (currentManifest.token !== manifest.token || currentManifest.generation !== manifest.generation
|
|
490
875
|
|| !isSandboxControlRootQuiescing(resolvedRoot)) {
|
|
491
876
|
throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
|
|
@@ -498,10 +883,10 @@ export async function removeSandboxControlRoot(
|
|
|
498
883
|
if (owner && currentBroker && !sameOwner(owner, currentBroker)) {
|
|
499
884
|
throw new Error('SANDBOX_CONTROL_OWNER_REPLACED');
|
|
500
885
|
}
|
|
501
|
-
if (!owner && currentBroker && ownerLive(currentBroker)) {
|
|
886
|
+
if (!owner && currentBroker && ownerLive(currentBroker, identityProbe)) {
|
|
502
887
|
throw new Error('SANDBOX_CONTROL_OWNER_REPLACED');
|
|
503
888
|
}
|
|
504
|
-
if (owner && ownerLive(owner) && !currentBroker) {
|
|
889
|
+
if (owner && ownerLive(owner, identityProbe) && !currentBroker) {
|
|
505
890
|
throw new Error('SANDBOX_CONTROL_OWNER_EVIDENCE_MISSING');
|
|
506
891
|
}
|
|
507
892
|
|
|
@@ -509,21 +894,21 @@ export async function removeSandboxControlRoot(
|
|
|
509
894
|
if (Date.now() >= deadlineAt) throw new Error('SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED');
|
|
510
895
|
for (const execution of executions) {
|
|
511
896
|
if (!terminateSandboxControlExecution(execution, {
|
|
512
|
-
platform, timeoutMs: remaining(), deadlineAt, forceAt: Date.now()
|
|
897
|
+
platform, timeoutMs: remaining(), deadlineAt, forceAt: Date.now(), identityProbe
|
|
513
898
|
})) {
|
|
514
899
|
throw new Error(`SANDBOX_CONTROL_EXECUTION_STILL_RUNNING: ${execution.requestId}`);
|
|
515
900
|
}
|
|
516
901
|
}
|
|
517
|
-
if (owner && currentBroker && ownerLive(currentBroker) && !selfOwned) {
|
|
902
|
+
if (owner && currentBroker && ownerLive(currentBroker, identityProbe) && !selfOwned) {
|
|
518
903
|
if (remaining() <= 0) throw new Error('SANDBOX_CONTROL_REMOVE_DEADLINE_EXCEEDED');
|
|
519
904
|
signalOwner(currentBroker, platform, true);
|
|
520
|
-
if (remaining() <= 0 || !await waitForExit(currentBroker, remaining(), deadlineAt)) {
|
|
905
|
+
if (remaining() <= 0 || !await waitForExit(currentBroker, remaining(), deadlineAt, identityProbe)) {
|
|
521
906
|
throw new Error('SANDBOX_CONTROL_BROKER_STILL_RUNNING');
|
|
522
907
|
}
|
|
523
908
|
}
|
|
524
909
|
|
|
525
910
|
const finalExecutions = mergeExecutions(executions, readExecutions(manifest));
|
|
526
|
-
if (finalExecutions.some((execution) => executionAlive(execution, platform))) {
|
|
911
|
+
if (finalExecutions.some((execution) => executionAlive(execution, platform, identityProbe))) {
|
|
527
912
|
throw new Error('SANDBOX_CONTROL_EXECUTION_STILL_RUNNING');
|
|
528
913
|
}
|
|
529
914
|
|
|
@@ -533,7 +918,9 @@ export async function removeSandboxControlRoot(
|
|
|
533
918
|
if (!finalBroker) throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
534
919
|
const finalSelfOwner = Boolean(selfOwner && sameOwner(finalBroker, selfOwner)
|
|
535
920
|
&& finalBroker.token === manifest.token && finalBroker.generation === manifest.generation);
|
|
536
|
-
if (ownerLive(finalBroker) && !finalSelfOwner)
|
|
921
|
+
if (ownerLive(finalBroker, identityProbe) && !finalSelfOwner) {
|
|
922
|
+
throw new Error('SANDBOX_CONTROL_BROKER_STILL_RUNNING');
|
|
923
|
+
}
|
|
537
924
|
if (fs.readFileSync(brokerPath, 'utf8') !== finalBrokerRaw) {
|
|
538
925
|
throw new Error('SANDBOX_CONTROL_OWNER_TRANSITION');
|
|
539
926
|
}
|
|
@@ -543,7 +930,8 @@ export async function removeSandboxControlRoot(
|
|
|
543
930
|
if (finalStatusRaw !== null) {
|
|
544
931
|
const finalStatus = readStatusOwner(statusPath);
|
|
545
932
|
if (!finalStatus) throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
546
|
-
if (ownerLive(finalStatus.broker
|
|
933
|
+
if (ownerLive(finalStatus.broker, identityProbe)
|
|
934
|
+
&& !(selfOwner && sameOwner(finalStatus.broker, selfOwner))) {
|
|
547
935
|
throw new Error('SANDBOX_CONTROL_BROKER_STILL_RUNNING');
|
|
548
936
|
}
|
|
549
937
|
if (fs.readFileSync(statusPath, 'utf8') !== finalStatusRaw) {
|