@fitlab-ai/agent-infra 0.9.6 → 0.9.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/internal-cli.js +10 -0
- package/dist/lib/agent-clients/adapters/claude-code.js +3 -3
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +47 -5
- package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +162 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +245 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +398 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +23 -6
- package/dist/lib/decide.js +68 -14
- package/dist/lib/defaults.json +10 -2
- package/dist/lib/git/workflow.js +30 -3
- package/dist/lib/init.js +1 -1
- package/dist/lib/internal/codex-lifecycle.js +69 -1
- package/dist/lib/internal/codex-sandbox-controller.js +106 -0
- package/dist/lib/internal/release-workflow.js +1 -3
- package/dist/lib/internal/task-activity.js +57 -7
- package/dist/lib/internal/task-event.js +57 -4
- package/dist/lib/internal/task-ledger.js +59 -5
- package/dist/lib/internal/task-lifecycle.js +59 -8
- package/dist/lib/internal/task-orchestration.js +48 -8
- package/dist/lib/internal/task-override.js +131 -0
- package/dist/lib/internal/task-review.js +64 -6
- package/dist/lib/internal/task-verify.js +65 -3
- package/dist/lib/internal/task-warning.js +57 -4
- package/dist/lib/platform/context.js +6 -4
- package/dist/lib/platform/github-client.js +55 -2
- package/dist/lib/platform/pr-summary.js +5 -3
- package/dist/lib/process-data/checkpoint.js +319 -0
- package/dist/lib/process-data/index.js +128 -60
- package/dist/lib/process-data/materialize.js +84 -0
- package/dist/lib/process-data/normalize.js +34 -1
- package/dist/lib/process-data/reconcile.js +55 -1
- package/dist/lib/process-data/sources.js +422 -1
- package/dist/lib/process-data/store.js +341 -11
- package/dist/lib/runtime/agent-runtime.js +56 -0
- package/dist/lib/sandbox/commands/create.js +394 -264
- package/dist/lib/sandbox/commands/rm.js +64 -15
- package/dist/lib/sandbox/commands/start.js +4 -3
- package/dist/lib/sandbox/control/container-identity.js +64 -0
- package/dist/lib/sandbox/control/executor.js +23 -9
- package/dist/lib/sandbox/control/lifecycle.js +590 -61
- package/dist/lib/sandbox/control/protocol.js +7 -0
- package/dist/lib/sandbox/control/server.js +202 -26
- package/dist/lib/sandbox/control/state.js +51 -25
- package/dist/lib/sandbox/control/timing.js +8 -0
- package/dist/lib/sandbox/engine.js +14 -7
- package/dist/lib/sandbox/mounts.js +7 -0
- package/dist/lib/sandbox/recovery.js +135 -50
- package/dist/lib/sandbox/workspace-view.js +59 -20
- package/dist/lib/server/daemon.js +1 -1
- package/dist/lib/server/process-control.js +11 -1
- package/dist/lib/server/process-state.js +107 -20
- package/dist/lib/task/activity-intent.js +4 -1
- package/dist/lib/task/artifact-lifecycle.js +82 -11
- package/dist/lib/task/artifact-receipts.js +128 -0
- package/dist/lib/task/codex-orchestration.js +81 -6
- package/dist/lib/task/commands/validate.js +1 -1
- package/dist/lib/task/create.js +10 -2
- package/dist/lib/task/decision-intents.js +4 -2
- package/dist/lib/task/delegation-receipts.js +120 -17
- package/dist/lib/task/events.js +148 -7
- package/dist/lib/task/guard-override.js +31 -0
- package/dist/lib/task/human-override.js +1240 -0
- package/dist/lib/task/ledger-intents.js +6 -3
- package/dist/lib/task/ledger.js +13 -1
- package/dist/lib/task/lifecycle.js +69 -14
- package/dist/lib/task/orchestration.js +293 -18
- package/dist/lib/task/review-finalization.js +3 -2
- package/dist/lib/task/sections.js +5 -1
- package/dist/lib/task/task-execution-lock.js +5 -4
- package/dist/lib/task/verification-engine.js +80 -22
- package/dist/lib/task/verification.js +13 -1
- package/dist/lib/task/workflow-warning-intents.js +4 -2
- package/dist/lib/task/write.js +2 -1
- package/dist/lifecycle-build-manifest.json +6 -0
- package/lib/agent-clients/adapters/claude-code.ts +3 -3
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +62 -4
- package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +227 -0
- package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +322 -0
- package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +26 -0
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +480 -0
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +35 -5
- package/lib/decide.ts +59 -14
- package/lib/defaults.json +10 -2
- package/lib/git/workflow.ts +25 -2
- package/lib/init.ts +1 -1
- package/lib/internal/codex-lifecycle.ts +68 -1
- package/lib/internal/codex-sandbox-controller.ts +109 -0
- package/lib/internal/release-workflow.ts +1 -3
- package/lib/internal/task-activity.ts +46 -6
- package/lib/internal/task-event.ts +53 -4
- package/lib/internal/task-ledger.ts +48 -4
- package/lib/internal/task-lifecycle.ts +56 -12
- package/lib/internal/task-orchestration.ts +43 -8
- package/lib/internal/task-override.ts +138 -0
- package/lib/internal/task-review.ts +51 -6
- package/lib/internal/task-verify.ts +52 -3
- package/lib/internal/task-warning.ts +44 -3
- package/lib/platform/context.ts +8 -5
- package/lib/platform/github-client.ts +61 -3
- package/lib/platform/pr-summary.ts +10 -3
- package/lib/process-data/checkpoint.ts +335 -0
- package/lib/process-data/index.ts +112 -60
- package/lib/process-data/materialize.ts +83 -0
- package/lib/process-data/normalize.ts +29 -1
- package/lib/process-data/reconcile.ts +64 -2
- package/lib/process-data/sources.ts +449 -2
- package/lib/process-data/store.ts +304 -11
- package/lib/process-data/types.ts +124 -2
- package/lib/runtime/agent-runtime.ts +76 -0
- package/lib/sandbox/commands/create.ts +139 -6
- package/lib/sandbox/commands/rm.ts +64 -14
- package/lib/sandbox/commands/start.ts +4 -3
- package/lib/sandbox/config.ts +2 -1
- package/lib/sandbox/control/container-identity.ts +93 -0
- package/lib/sandbox/control/executor.ts +24 -12
- package/lib/sandbox/control/lifecycle.ts +685 -67
- package/lib/sandbox/control/protocol.ts +35 -6
- package/lib/sandbox/control/server.ts +184 -24
- package/lib/sandbox/control/state.ts +67 -33
- package/lib/sandbox/control/timing.ts +12 -0
- package/lib/sandbox/engine.ts +20 -9
- package/lib/sandbox/mounts.ts +8 -0
- package/lib/sandbox/recovery.ts +159 -49
- package/lib/sandbox/workspace-view.ts +73 -22
- package/lib/server/daemon.ts +1 -1
- package/lib/server/process-control.ts +13 -3
- package/lib/server/process-state.ts +129 -27
- package/lib/task/activity-intent.ts +4 -2
- package/lib/task/artifact-lifecycle.ts +73 -10
- package/lib/task/artifact-receipts.ts +161 -0
- package/lib/task/codex-orchestration.ts +91 -5
- package/lib/task/commands/validate.ts +1 -1
- package/lib/task/create.ts +11 -2
- package/lib/task/decision-intents.ts +4 -2
- package/lib/task/delegation-receipts.ts +205 -25
- package/lib/task/events.ts +132 -6
- package/lib/task/guard-override.ts +50 -0
- package/lib/task/human-override.ts +1503 -0
- package/lib/task/ledger-intents.ts +6 -3
- package/lib/task/ledger.ts +16 -1
- package/lib/task/lifecycle.ts +71 -14
- package/lib/task/orchestration.ts +443 -22
- package/lib/task/review-finalization.ts +6 -2
- package/lib/task/sections.ts +6 -2
- package/lib/task/task-execution-lock.ts +11 -9
- package/lib/task/verification-engine.ts +93 -32
- package/lib/task/verification.ts +15 -1
- package/lib/task/workflow-warning-intents.ts +4 -2
- package/lib/task/write.ts +4 -1
- package/package.json +1 -1
- package/templates/.agents/README.en.md +7 -0
- package/templates/.agents/README.zh-CN.md +5 -0
- package/templates/.agents/hooks/lifecycle-delegation.js +33 -4
- package/templates/.agents/rules/lifecycle-orchestration.en.md +10 -3
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +10 -3
- package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
- package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +1 -1
- package/templates/.agents/rules/review-handshake.en.md +3 -1
- package/templates/.agents/rules/review-handshake.zh-CN.md +3 -1
- package/templates/.agents/skills/code-task/reference/dual-mode.en.md +3 -1
- package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +3 -1
- package/templates/.agents/skills/complete-task/SKILL.en.md +4 -0
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +4 -0
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +6 -6
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +6 -6
- package/templates/.agents/skills/run-manual-validation/config/verify.en.json +14 -3
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +14 -3
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +67 -0
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +67 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +25 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +25 -0
- package/templates/.agents/skills/run-task/SKILL.en.md +9 -7
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +9 -7
- package/templates/.agents/skills/run-task/reference/host-validation.en.md +5 -1
- package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +5 -1
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +75 -3
- package/templates/.agents/workspace/README.en.md +0 -26
- package/templates/.agents/workspace/README.zh-CN.md +0 -26
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { validateTaskCreateCandidate, type TaskCreateCandidateV1 } from '../../task/create.ts';
|
|
2
|
+
import type { ProcessIdentity } from '../../server/process-state.ts';
|
|
2
3
|
|
|
3
4
|
export const SANDBOX_CONTROL_MAX_BYTES = 64 * 1024;
|
|
4
5
|
export const SANDBOX_CONTROL_ADMISSION_WINDOW_MS = 2_000;
|
|
@@ -6,13 +7,41 @@ export const SANDBOX_CONTROL_STATUS_INTERVAL_MS = 250;
|
|
|
6
7
|
export const SANDBOX_CONTROL_STATUS_STALE_MS = 1_500;
|
|
7
8
|
export const SANDBOX_CONTROL_FUTURE_SKEW_MS = 1_000;
|
|
8
9
|
export const SANDBOX_CONTROL_FAMILIES = ['task-lifecycle', 'task-orchestration', 'task-create'] as const;
|
|
10
|
+
export type SandboxControlTimingPolicy = Readonly<{
|
|
11
|
+
controlTickMs: number;
|
|
12
|
+
parkedBindingInitialMs: number;
|
|
13
|
+
slowCheckMs: number;
|
|
14
|
+
containerHeartbeatMs: number;
|
|
15
|
+
quiesceDeadlineMs: number;
|
|
16
|
+
}>;
|
|
17
|
+
export const DEFAULT_SANDBOX_CONTROL_TIMING: SandboxControlTimingPolicy = Object.freeze({
|
|
18
|
+
controlTickMs: 250,
|
|
19
|
+
parkedBindingInitialMs: 1_000,
|
|
20
|
+
slowCheckMs: 5_000,
|
|
21
|
+
containerHeartbeatMs: 5_000,
|
|
22
|
+
quiesceDeadlineMs: 7_000
|
|
23
|
+
} as const);
|
|
9
24
|
|
|
10
25
|
export type SandboxControlFamily = typeof SANDBOX_CONTROL_FAMILIES[number];
|
|
11
|
-
export type
|
|
12
|
-
|
|
26
|
+
export type SandboxControlContainerIdentity = Readonly<{
|
|
27
|
+
id: string;
|
|
28
|
+
labels: Readonly<Record<string, string>>;
|
|
29
|
+
}>;
|
|
30
|
+
export type SandboxControlManifestBase = Readonly<{
|
|
31
|
+
engine: string; repoRoot: string; worktreeRoot: string; project: string; container: string;
|
|
32
|
+
containerIdentity: SandboxControlContainerIdentity;
|
|
13
33
|
branch: string; mode: 'task-bound' | 'branch-only'; taskId: string | null; token: string;
|
|
14
34
|
generation: string; channelDir: string; publicStatusDir: string; processingDir: string;
|
|
15
35
|
}>;
|
|
36
|
+
export type SandboxControlLegacyManifest = SandboxControlManifestBase & Readonly<{ version: 4 }>;
|
|
37
|
+
export type SandboxControlManifest = SandboxControlManifestBase & Readonly<{ version: 5; runtimeDir: string }>;
|
|
38
|
+
export type SandboxControlManifestLike = SandboxControlLegacyManifest | SandboxControlManifest;
|
|
39
|
+
export type SandboxControlBrokerOwner = ProcessIdentity & Readonly<{
|
|
40
|
+
version: 3;
|
|
41
|
+
brokerId: string;
|
|
42
|
+
token: string;
|
|
43
|
+
generation: string;
|
|
44
|
+
}>;
|
|
16
45
|
type RequestBase = Readonly<{
|
|
17
46
|
version: 2; id: string; token: string; generation: string; issuedAt: number; expiresAt: number;
|
|
18
47
|
}>;
|
|
@@ -29,17 +58,17 @@ export type SandboxControlResponse = Readonly<{
|
|
|
29
58
|
stdout: string; stderr: string; error: SandboxControlError | null;
|
|
30
59
|
}>;
|
|
31
60
|
export type SandboxControlStatus = Readonly<{
|
|
32
|
-
version:
|
|
61
|
+
version: 2; generation: string; broker: ProcessIdentity & { brokerId: string };
|
|
33
62
|
state: 'starting' | 'healthy' | 'busy' | 'parked'; reasonCode: string | null;
|
|
34
63
|
activeRequestId: string | null; updatedAt: number;
|
|
35
64
|
}>;
|
|
36
65
|
export type SandboxControlLease = Readonly<{
|
|
37
|
-
version:
|
|
66
|
+
version: 2; generation: string; nonce: string; owner: ProcessIdentity;
|
|
38
67
|
issuedAt: number; expiresAt: number; taskId: string | null; branch: string; reason: string;
|
|
39
68
|
}>;
|
|
40
69
|
export type SandboxControlExecution = Readonly<{
|
|
41
|
-
version:
|
|
42
|
-
child:
|
|
70
|
+
version: 2; generation: string; requestId: string; nonce: string;
|
|
71
|
+
child: ProcessIdentity & { processGroupId: number | null };
|
|
43
72
|
phase: 'prepared' | 'running' | 'terminating'; updatedAt: number;
|
|
44
73
|
}>;
|
|
45
74
|
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
3
4
|
import { getProcessStartTime } from '../../server/process-state.ts';
|
|
4
5
|
import { assertGitWorktreeBinding } from '../../git/worktree-identity.ts';
|
|
5
6
|
import {
|
|
6
7
|
controlError,
|
|
7
|
-
|
|
8
|
+
DEFAULT_SANDBOX_CONTROL_TIMING,
|
|
8
9
|
type SandboxControlManifest,
|
|
9
10
|
type SandboxControlRequest,
|
|
10
|
-
type SandboxControlResponse
|
|
11
|
+
type SandboxControlResponse,
|
|
12
|
+
type SandboxControlTimingPolicy
|
|
11
13
|
} from './protocol.ts';
|
|
12
14
|
import { prepareSandboxControlExecution, type PreparedSandboxControlExecution, type SandboxControlExecutionResult } from './executor.ts';
|
|
13
15
|
import {
|
|
@@ -21,11 +23,15 @@ import {
|
|
|
21
23
|
writeSandboxControlStatus
|
|
22
24
|
} from './state.ts';
|
|
23
25
|
import { validateSandboxControlRequest } from './protocol.ts';
|
|
26
|
+
import { inspectSandboxControlContainer, type ContainerObservation } from './container-identity.ts';
|
|
24
27
|
import {
|
|
25
28
|
acquireSandboxControlBrokerStartup,
|
|
29
|
+
garbageCollectSandboxControlRoot,
|
|
26
30
|
isSandboxControlRootQuiescing,
|
|
27
31
|
readSandboxControlManifest
|
|
28
32
|
} from './lifecycle.ts';
|
|
33
|
+
import type { BrokerOwner } from './lifecycle.ts';
|
|
34
|
+
import { nextSandboxControlBackoff } from './timing.ts';
|
|
29
35
|
|
|
30
36
|
type ActiveExecution = {
|
|
31
37
|
request: SandboxControlRequest;
|
|
@@ -105,9 +111,10 @@ function bindingReason(manifest: SandboxControlManifest): string | null {
|
|
|
105
111
|
}
|
|
106
112
|
}
|
|
107
113
|
|
|
108
|
-
function recoverProcessing(manifest: SandboxControlManifest):
|
|
114
|
+
function recoverProcessing(manifest: SandboxControlManifest, brokerOwns: () => boolean): boolean {
|
|
109
115
|
for (const entry of fs.readdirSync(manifest.processingDir, { withFileTypes: true })) {
|
|
110
116
|
if (!entry.isDirectory() || !/^[a-f0-9-]{16,64}$/.test(entry.name)) continue;
|
|
117
|
+
if (!brokerOwns()) return false;
|
|
111
118
|
const descriptor = executionPath(manifest, entry.name);
|
|
112
119
|
const existingResponse = responsePath(manifest, entry.name);
|
|
113
120
|
if (fs.existsSync(descriptor)) {
|
|
@@ -115,6 +122,7 @@ function recoverProcessing(manifest: SandboxControlManifest): void {
|
|
|
115
122
|
if (!terminateSandboxControlExecution(execution)) {
|
|
116
123
|
throw new Error(`SANDBOX_CONTROL_EXECUTION_STILL_RUNNING: ${entry.name}`);
|
|
117
124
|
}
|
|
125
|
+
if (!brokerOwns()) return false;
|
|
118
126
|
appendSandboxControlAudit(manifest, 'orphan-tree-terminated', { requestId: entry.name });
|
|
119
127
|
let terminal = false;
|
|
120
128
|
if (fs.existsSync(existingResponse)) {
|
|
@@ -126,12 +134,18 @@ function recoverProcessing(manifest: SandboxControlManifest): void {
|
|
|
126
134
|
terminal = false;
|
|
127
135
|
}
|
|
128
136
|
}
|
|
129
|
-
if (!terminal)
|
|
137
|
+
if (!terminal) {
|
|
138
|
+
if (!brokerOwns()) return false;
|
|
139
|
+
writeResponse(manifest, unknown(entry.name));
|
|
140
|
+
}
|
|
130
141
|
} else {
|
|
142
|
+
if (!brokerOwns()) return false;
|
|
131
143
|
writeResponse(manifest, notExecuted(entry.name));
|
|
132
144
|
}
|
|
145
|
+
if (!brokerOwns()) return false;
|
|
133
146
|
fs.rmSync(path.join(manifest.processingDir, entry.name), { recursive: true, force: true });
|
|
134
147
|
}
|
|
148
|
+
return true;
|
|
135
149
|
}
|
|
136
150
|
|
|
137
151
|
function delay(ms: number): Promise<void> {
|
|
@@ -144,19 +158,42 @@ export function sandboxControlSafeEnv(env: NodeJS.ProcessEnv = process.env): Nod
|
|
|
144
158
|
|
|
145
159
|
export async function serveSandboxControl(
|
|
146
160
|
manifestPath: string,
|
|
147
|
-
signal: AbortSignal = new AbortController().signal
|
|
161
|
+
signal: AbortSignal = new AbortController().signal,
|
|
162
|
+
options: {
|
|
163
|
+
timing?: SandboxControlTimingPolicy;
|
|
164
|
+
inspectContainer?: (manifest: SandboxControlManifest) => Promise<ContainerObservation>;
|
|
165
|
+
bindingCheck?: (manifest: SandboxControlManifest) => string | null;
|
|
166
|
+
} = {}
|
|
148
167
|
): Promise<void> {
|
|
168
|
+
const timing = options.timing ?? DEFAULT_SANDBOX_CONTROL_TIMING;
|
|
169
|
+
const inspectContainer = options.inspectContainer ?? ((value: SandboxControlManifest) => inspectSandboxControlContainer(value));
|
|
170
|
+
const bindingCheck = options.bindingCheck ?? bindingReason;
|
|
149
171
|
const manifest = readSandboxControlManifest(manifestPath);
|
|
150
172
|
const root = path.dirname(manifestPath);
|
|
151
173
|
const startTime = getProcessStartTime(process.pid);
|
|
152
174
|
if (!startTime) throw new Error('SANDBOX_CONTROL_BROKER_IDENTITY_UNAVAILABLE');
|
|
153
|
-
const
|
|
175
|
+
const brokerId = randomUUID();
|
|
176
|
+
const releaseStartup = await acquireSandboxControlBrokerStartup(root, { pid: process.pid, startTime, brokerId });
|
|
154
177
|
const requestsDir = path.join(manifest.channelDir, 'requests');
|
|
155
178
|
const responsesDir = path.join(manifest.channelDir, 'responses');
|
|
156
179
|
const consumedDir = path.join(root, 'consumed');
|
|
157
|
-
const broker = {
|
|
180
|
+
const broker: BrokerOwner = {
|
|
181
|
+
version: 3,
|
|
182
|
+
pid: process.pid,
|
|
183
|
+
startTime,
|
|
184
|
+
brokerId,
|
|
185
|
+
token: manifest.token,
|
|
186
|
+
generation: manifest.generation
|
|
187
|
+
};
|
|
158
188
|
const brokerPath = path.join(root, 'broker.json');
|
|
159
|
-
const brokerRecord = `${JSON.stringify(
|
|
189
|
+
const brokerRecord = `${JSON.stringify(broker)}\n`;
|
|
190
|
+
const brokerOwns = (): boolean => {
|
|
191
|
+
try {
|
|
192
|
+
return fs.readFileSync(brokerPath, 'utf8') === brokerRecord;
|
|
193
|
+
} catch {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
};
|
|
160
197
|
try {
|
|
161
198
|
for (const directory of [manifest.channelDir, requestsDir, responsesDir, consumedDir, manifest.publicStatusDir, manifest.processingDir]) {
|
|
162
199
|
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
@@ -178,11 +215,20 @@ export async function serveSandboxControl(
|
|
|
178
215
|
let active: ActiveExecution | null = null;
|
|
179
216
|
let lastState = '';
|
|
180
217
|
let lastStatusAt = 0;
|
|
218
|
+
let nextBindingCheckAt = 0;
|
|
219
|
+
let nextContainerHeartbeatAt = Date.now() + timing.containerHeartbeatMs;
|
|
220
|
+
let containerReasonCode: string | null = null;
|
|
221
|
+
let containerBackoffMs = timing.parkedBindingInitialMs;
|
|
222
|
+
let bindingReasonCode: string | null = null;
|
|
223
|
+
let bindingBackoffMs: number = timing.parkedBindingInitialMs;
|
|
181
224
|
try {
|
|
225
|
+
if (!brokerOwns()) return;
|
|
182
226
|
writeSandboxControlStatus(manifest, broker, 'starting', null, null);
|
|
227
|
+
if (!brokerOwns()) return;
|
|
183
228
|
appendSandboxControlAudit(manifest, 'broker-start', { pid: broker.pid });
|
|
184
|
-
recoverProcessing(manifest);
|
|
229
|
+
if (!recoverProcessing(manifest, brokerOwns)) return;
|
|
185
230
|
while (!signal.aborted) {
|
|
231
|
+
if (!brokerOwns()) break;
|
|
186
232
|
let current: SandboxControlManifest;
|
|
187
233
|
try {
|
|
188
234
|
current = readSandboxControlManifest(manifestPath);
|
|
@@ -190,54 +236,123 @@ export async function serveSandboxControl(
|
|
|
190
236
|
break;
|
|
191
237
|
}
|
|
192
238
|
if (current.token !== manifest.token || current.generation !== manifest.generation) break;
|
|
239
|
+
const heartbeatNow = Date.now();
|
|
240
|
+
if (heartbeatNow >= nextContainerHeartbeatAt) {
|
|
241
|
+
let observation: ContainerObservation;
|
|
242
|
+
try {
|
|
243
|
+
observation = await inspectContainer(manifest);
|
|
244
|
+
} catch (error) {
|
|
245
|
+
observation = { state: 'unknown', reason: controlError(error).code };
|
|
246
|
+
}
|
|
247
|
+
if (!brokerOwns()) break;
|
|
248
|
+
if (observation.state === 'found') {
|
|
249
|
+
containerReasonCode = null;
|
|
250
|
+
containerBackoffMs = timing.parkedBindingInitialMs;
|
|
251
|
+
nextContainerHeartbeatAt = Date.now() + timing.containerHeartbeatMs;
|
|
252
|
+
} else if (observation.state === 'unknown') {
|
|
253
|
+
containerReasonCode = 'SANDBOX_CONTROL_CONTAINER_UNKNOWN';
|
|
254
|
+
appendSandboxControlAudit(manifest, 'container-heartbeat-unknown', { reason: observation.reason });
|
|
255
|
+
const backoff = nextSandboxControlBackoff(containerBackoffMs, timing.slowCheckMs);
|
|
256
|
+
nextContainerHeartbeatAt = Date.now() + backoff.delayMs;
|
|
257
|
+
containerBackoffMs = backoff.nextDelayMs;
|
|
258
|
+
} else {
|
|
259
|
+
try {
|
|
260
|
+
await garbageCollectSandboxControlRoot(root, {
|
|
261
|
+
timeoutMs: timing.quiesceDeadlineMs,
|
|
262
|
+
inspectContainer: (timeoutMs) => inspectContainer(manifest),
|
|
263
|
+
selfOwner: broker
|
|
264
|
+
});
|
|
265
|
+
active = null;
|
|
266
|
+
return;
|
|
267
|
+
} catch (error) {
|
|
268
|
+
if (!brokerOwns()) break;
|
|
269
|
+
containerReasonCode = 'SANDBOX_CONTROL_CONTAINER_ABSENT_GC_FAILED';
|
|
270
|
+
appendSandboxControlAudit(manifest, 'container-gc-failed', { reason: controlError(error).code });
|
|
271
|
+
const backoff = nextSandboxControlBackoff(containerBackoffMs, timing.slowCheckMs);
|
|
272
|
+
nextContainerHeartbeatAt = Date.now() + backoff.delayMs;
|
|
273
|
+
containerBackoffMs = backoff.nextDelayMs;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
193
277
|
|
|
194
278
|
if (active?.settled) {
|
|
279
|
+
if (!brokerOwns()) break;
|
|
195
280
|
if (active.result) {
|
|
281
|
+
if (!brokerOwns()) break;
|
|
196
282
|
writeResponse(manifest, {
|
|
197
283
|
version: 2, id: active.request.id, phase: 'completed', exitCode: active.result.exitCode,
|
|
198
284
|
stdout: active.result.stdout, stderr: active.result.stderr, error: null
|
|
199
285
|
});
|
|
200
286
|
} else {
|
|
287
|
+
if (!brokerOwns()) break;
|
|
201
288
|
writeResponse(manifest, unknown(active.request.id));
|
|
202
289
|
}
|
|
290
|
+
if (!brokerOwns()) break;
|
|
203
291
|
fs.rmSync(path.join(manifest.processingDir, active.request.id), { recursive: true, force: true });
|
|
204
292
|
active = null;
|
|
205
293
|
}
|
|
206
294
|
|
|
207
295
|
let reasonCode: string | null = null;
|
|
208
296
|
try {
|
|
297
|
+
if (!brokerOwns()) break;
|
|
209
298
|
if (cleanupStaleSandboxControlLease(manifest)) {
|
|
299
|
+
if (!brokerOwns()) break;
|
|
210
300
|
appendSandboxControlAudit(manifest, 'lease-stale-cleanup');
|
|
211
301
|
}
|
|
212
302
|
if (readActiveLease(manifest)) reasonCode = 'SANDBOX_CONTROL_HANDOFF_ACTIVE';
|
|
213
303
|
} catch {
|
|
214
304
|
reasonCode = 'SANDBOX_CONTROL_HANDOFF_ACTIVE';
|
|
215
305
|
}
|
|
216
|
-
|
|
306
|
+
const now = Date.now();
|
|
307
|
+
if (now >= nextBindingCheckAt) {
|
|
308
|
+
bindingReasonCode = bindingCheck(manifest);
|
|
309
|
+
if (bindingReasonCode) {
|
|
310
|
+
const backoff = nextSandboxControlBackoff(bindingBackoffMs, timing.slowCheckMs);
|
|
311
|
+
nextBindingCheckAt = now + backoff.delayMs;
|
|
312
|
+
bindingBackoffMs = backoff.nextDelayMs;
|
|
313
|
+
} else {
|
|
314
|
+
nextBindingCheckAt = now + timing.slowCheckMs;
|
|
315
|
+
bindingBackoffMs = timing.parkedBindingInitialMs;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
reasonCode ??= containerReasonCode;
|
|
319
|
+
reasonCode ??= bindingReasonCode;
|
|
217
320
|
const state = reasonCode ? 'parked' : active ? 'busy' : 'healthy';
|
|
218
321
|
const stateKey = `${state}:${reasonCode ?? ''}:${active?.request.id ?? ''}`;
|
|
219
|
-
|
|
220
|
-
if (stateKey !== lastState || now - lastStatusAt >= SANDBOX_CONTROL_STATUS_INTERVAL_MS) {
|
|
322
|
+
if (brokerOwns() && (stateKey !== lastState || now - lastStatusAt >= timing.controlTickMs)) {
|
|
221
323
|
writeSandboxControlStatus(manifest, broker, state, reasonCode, active?.request.id ?? null, now);
|
|
222
324
|
lastStatusAt = now;
|
|
223
325
|
}
|
|
224
|
-
if (stateKey !== lastState) {
|
|
326
|
+
if (brokerOwns() && stateKey !== lastState) {
|
|
225
327
|
appendSandboxControlAudit(manifest, 'broker-state', { state, reasonCode, requestId: active?.request.id ?? null });
|
|
226
328
|
lastState = stateKey;
|
|
227
329
|
}
|
|
228
330
|
|
|
331
|
+
let retiring = false;
|
|
229
332
|
for (const name of fs.readdirSync(requestsDir).sort()) {
|
|
230
333
|
if (!/^[a-f0-9-]{16,64}\.json$/.test(name)) continue;
|
|
231
334
|
const id = name.slice(0, -5);
|
|
232
335
|
const source = path.join(requestsDir, name);
|
|
233
336
|
let claimed: string | null = null;
|
|
234
337
|
try {
|
|
338
|
+
if (!brokerOwns()) {
|
|
339
|
+
retiring = true;
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
235
342
|
claimed = claimRequest(manifest, source, id);
|
|
343
|
+
if (!brokerOwns()) {
|
|
344
|
+
retiring = true;
|
|
345
|
+
break;
|
|
346
|
+
}
|
|
236
347
|
consumeRequest(consumedDir, id);
|
|
348
|
+
if (!brokerOwns()) {
|
|
349
|
+
retiring = true;
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
237
352
|
if (reasonCode) throw new Error(reasonCode);
|
|
238
353
|
if (active) throw new Error('SANDBOX_CONTROL_BUSY');
|
|
239
354
|
const request = validateSandboxControlRequest(JSON.parse(fs.readFileSync(claimed, 'utf8')), manifest);
|
|
240
|
-
if (
|
|
355
|
+
if (bindingCheck(manifest)) throw new Error('SANDBOX_WORKTREE_BINDING_LOST');
|
|
241
356
|
if (readActiveLease(manifest)) throw new Error('SANDBOX_CONTROL_HANDOFF_ACTIVE');
|
|
242
357
|
const prepared = await prepareSandboxControlExecution({
|
|
243
358
|
manifest, manifestPath, request, requestPath: claimed, internalCliPath: process.argv[1]!
|
|
@@ -248,37 +363,82 @@ export async function serveSandboxControl(
|
|
|
248
363
|
(result) => { execution.result = result; execution.settled = true; },
|
|
249
364
|
(error) => { execution.failure = error; execution.settled = true; }
|
|
250
365
|
);
|
|
366
|
+
if (!brokerOwns()) {
|
|
367
|
+
prepared.terminate(false);
|
|
368
|
+
active = null;
|
|
369
|
+
retiring = true;
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
if (!brokerOwns()) {
|
|
373
|
+
prepared.terminate(false);
|
|
374
|
+
active = null;
|
|
375
|
+
retiring = true;
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
251
378
|
writeResponse(manifest, {
|
|
252
379
|
version: 2, id, phase: 'accepted', exitCode: null, stdout: '', stderr: '', error: null
|
|
253
380
|
});
|
|
381
|
+
if (!brokerOwns()) {
|
|
382
|
+
prepared.terminate(false);
|
|
383
|
+
active = null;
|
|
384
|
+
retiring = true;
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
254
387
|
try {
|
|
255
|
-
prepared.start();
|
|
388
|
+
prepared.start(brokerOwns);
|
|
256
389
|
} catch (error) {
|
|
257
|
-
|
|
390
|
+
const owned = brokerOwns();
|
|
391
|
+
prepared.terminate(owned);
|
|
392
|
+
if (!owned || !brokerOwns()) {
|
|
393
|
+
active = null;
|
|
394
|
+
retiring = true;
|
|
395
|
+
break;
|
|
396
|
+
}
|
|
258
397
|
writeResponse(manifest, unknown(id));
|
|
398
|
+
if (!brokerOwns()) {
|
|
399
|
+
active = null;
|
|
400
|
+
retiring = true;
|
|
401
|
+
break;
|
|
402
|
+
}
|
|
259
403
|
fs.rmSync(path.join(manifest.processingDir, id), { recursive: true, force: true });
|
|
260
404
|
active = null;
|
|
261
405
|
claimed = null;
|
|
406
|
+
if (!brokerOwns()) {
|
|
407
|
+
retiring = true;
|
|
408
|
+
break;
|
|
409
|
+
}
|
|
262
410
|
appendSandboxControlAudit(manifest, 'executor-gate-failed', { requestId: id });
|
|
263
411
|
continue;
|
|
264
412
|
}
|
|
265
413
|
} catch (error) {
|
|
414
|
+
if (!brokerOwns()) {
|
|
415
|
+
retiring = true;
|
|
416
|
+
break;
|
|
417
|
+
}
|
|
266
418
|
writeResponse(manifest, rejected(id, error));
|
|
419
|
+
if (!brokerOwns()) {
|
|
420
|
+
retiring = true;
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
267
423
|
if (claimed) fs.rmSync(path.dirname(claimed), { recursive: true, force: true });
|
|
268
424
|
}
|
|
269
425
|
}
|
|
270
|
-
|
|
426
|
+
if (retiring || !brokerOwns()) break;
|
|
427
|
+
await delay(timing.controlTickMs);
|
|
271
428
|
}
|
|
272
429
|
} finally {
|
|
273
430
|
if (active) {
|
|
274
|
-
|
|
275
|
-
|
|
431
|
+
const owned = brokerOwns();
|
|
432
|
+
active.prepared.terminate(owned);
|
|
433
|
+
if (owned && brokerOwns()) writeResponse(manifest, unknown(active.request.id));
|
|
276
434
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
435
|
+
if (brokerOwns()) {
|
|
436
|
+
appendSandboxControlAudit(manifest, 'broker-stop', { pid: broker.pid, brokerId: broker.brokerId });
|
|
437
|
+
try {
|
|
438
|
+
if (fs.readFileSync(brokerPath, 'utf8') === brokerRecord) fs.unlinkSync(brokerPath);
|
|
439
|
+
} catch {
|
|
440
|
+
// A newer owner or recovery path owns the record.
|
|
441
|
+
}
|
|
282
442
|
}
|
|
283
443
|
}
|
|
284
444
|
}
|
|
@@ -3,11 +3,12 @@ import fs from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { randomUUID } from 'node:crypto';
|
|
5
5
|
import { buildProcessTreeStopCommand } from '../../server/process-control.ts';
|
|
6
|
-
import {
|
|
6
|
+
import { getProcessIdentityState } from '../../server/process-state.ts';
|
|
7
|
+
import type { ProcessIdentity, ProcessIdentityProbe } from '../../server/process-state.ts';
|
|
7
8
|
import type {
|
|
8
9
|
SandboxControlExecution,
|
|
9
10
|
SandboxControlLease,
|
|
10
|
-
|
|
11
|
+
SandboxControlManifestLike,
|
|
11
12
|
SandboxControlStatus
|
|
12
13
|
} from './protocol.ts';
|
|
13
14
|
|
|
@@ -35,20 +36,20 @@ export function readJsonFile(filePath: string): unknown {
|
|
|
35
36
|
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
export function statusPath(manifest:
|
|
39
|
+
export function statusPath(manifest: SandboxControlManifestLike): string {
|
|
39
40
|
return path.join(manifest.publicStatusDir, 'status.json');
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
export function writeSandboxControlStatus(
|
|
43
|
-
manifest:
|
|
44
|
-
broker:
|
|
44
|
+
manifest: SandboxControlManifestLike,
|
|
45
|
+
broker: ProcessIdentity & { brokerId: string },
|
|
45
46
|
state: SandboxControlStatus['state'],
|
|
46
47
|
reasonCode: string | null,
|
|
47
48
|
activeRequestId: string | null,
|
|
48
49
|
now = Date.now()
|
|
49
50
|
): SandboxControlStatus {
|
|
50
51
|
const status: SandboxControlStatus = {
|
|
51
|
-
version:
|
|
52
|
+
version: 2, generation: manifest.generation, broker, state, reasonCode, activeRequestId, updatedAt: now
|
|
52
53
|
};
|
|
53
54
|
atomicWriteJson(statusPath(manifest), status, 0o600, false);
|
|
54
55
|
return status;
|
|
@@ -56,8 +57,10 @@ export function writeSandboxControlStatus(
|
|
|
56
57
|
|
|
57
58
|
export function parseSandboxControlStatus(value: unknown): SandboxControlStatus {
|
|
58
59
|
const status = value as Partial<SandboxControlStatus> | null;
|
|
59
|
-
if (!status || status.version !==
|
|
60
|
-
|| !status.broker || !Number.isSafeInteger(status.broker.pid) || typeof status.broker.startTime !== '
|
|
60
|
+
if (!status || status.version !== 2 || typeof status.generation !== 'string'
|
|
61
|
+
|| !status.broker || !Number.isSafeInteger(status.broker.pid) || typeof status.broker.startTime !== 'number'
|
|
62
|
+
|| !Number.isSafeInteger(status.broker.startTime) || typeof status.broker.brokerId !== 'string'
|
|
63
|
+
|| status.broker.brokerId.length === 0
|
|
61
64
|
|| !['starting', 'healthy', 'busy', 'parked'].includes(status.state ?? '')
|
|
62
65
|
|| (status.reasonCode !== null && typeof status.reasonCode !== 'string')
|
|
63
66
|
|| (status.activeRequestId !== null && typeof status.activeRequestId !== 'string')
|
|
@@ -69,13 +72,14 @@ export function readSandboxControlStatus(directory: string): SandboxControlStatu
|
|
|
69
72
|
return parseSandboxControlStatus(readJsonFile(path.join(directory, 'status.json')));
|
|
70
73
|
}
|
|
71
74
|
|
|
72
|
-
export function readActiveLease(manifest:
|
|
75
|
+
export function readActiveLease(manifest: SandboxControlManifestLike, now = Date.now()): SandboxControlLease | null {
|
|
73
76
|
const filePath = path.join(path.dirname(manifest.publicStatusDir), 'lease.json');
|
|
74
77
|
if (!fs.existsSync(filePath)) return null;
|
|
75
78
|
const lease = readJsonFile(filePath) as Partial<SandboxControlLease> | null;
|
|
76
|
-
if (!lease || lease.version !==
|
|
79
|
+
if (!lease || lease.version !== 2
|
|
77
80
|
|| typeof lease.nonce !== 'string' || !lease.owner || !Number.isSafeInteger(lease.owner.pid)
|
|
78
|
-
|| typeof lease.owner.startTime !== '
|
|
81
|
+
|| typeof lease.owner.startTime !== 'number' || !Number.isSafeInteger(lease.owner.startTime)
|
|
82
|
+
|| !Number.isSafeInteger(lease.issuedAt)
|
|
79
83
|
|| !Number.isSafeInteger(lease.expiresAt) || typeof lease.generation !== 'string'
|
|
80
84
|
|| (lease.taskId !== null && typeof lease.taskId !== 'string')
|
|
81
85
|
|| typeof lease.branch !== 'string' || typeof lease.reason !== 'string') {
|
|
@@ -86,23 +90,30 @@ export function readActiveLease(manifest: SandboxControlManifest, now = Date.now
|
|
|
86
90
|
return lease as SandboxControlLease;
|
|
87
91
|
}
|
|
88
92
|
|
|
89
|
-
export function cleanupStaleSandboxControlLease(
|
|
93
|
+
export function cleanupStaleSandboxControlLease(
|
|
94
|
+
manifest: SandboxControlManifestLike,
|
|
95
|
+
now = Date.now(),
|
|
96
|
+
options: Readonly<{ identityProbe?: ProcessIdentityProbe }> = {}
|
|
97
|
+
): boolean {
|
|
90
98
|
const filePath = path.join(path.dirname(manifest.publicStatusDir), 'lease.json');
|
|
91
99
|
if (!fs.existsSync(filePath)) return false;
|
|
92
100
|
const raw = fs.readFileSync(filePath, 'utf8');
|
|
93
101
|
const lease = JSON.parse(raw) as Partial<SandboxControlLease> | null;
|
|
94
|
-
if (!lease || lease.version !==
|
|
102
|
+
if (!lease || lease.version !== 2
|
|
95
103
|
|| typeof lease.nonce !== 'string' || !lease.owner || !Number.isSafeInteger(lease.owner.pid)
|
|
96
|
-
|| typeof lease.owner.startTime !== '
|
|
104
|
+
|| typeof lease.owner.startTime !== 'number' || !Number.isSafeInteger(lease.owner.startTime)
|
|
105
|
+
|| !Number.isSafeInteger(lease.issuedAt)
|
|
97
106
|
|| !Number.isSafeInteger(lease.expiresAt) || typeof lease.generation !== 'string'
|
|
98
107
|
|| (lease.taskId !== null && typeof lease.taskId !== 'string')
|
|
99
108
|
|| typeof lease.branch !== 'string' || typeof lease.reason !== 'string') {
|
|
100
109
|
throw new Error('SANDBOX_CONTROL_LEASE_INVALID');
|
|
101
110
|
}
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
version: 1, pid: lease.owner.pid!, startTime: lease.owner.startTime!
|
|
111
|
+
const ownerState = (options.identityProbe ?? getProcessIdentityState)({
|
|
112
|
+
pid: lease.owner.pid!, startTime: lease.owner.startTime!
|
|
105
113
|
});
|
|
114
|
+
if (ownerState === 'unknown') throw new Error('SANDBOX_CONTROL_LEASE_OWNER_UNAVAILABLE');
|
|
115
|
+
const stale = lease.generation !== manifest.generation || lease.taskId !== manifest.taskId
|
|
116
|
+
|| lease.branch !== manifest.branch || lease.expiresAt! <= now || ownerState === 'dead';
|
|
106
117
|
if (!stale) return false;
|
|
107
118
|
if (fs.readFileSync(filePath, 'utf8') !== raw) throw new Error('SANDBOX_CONTROL_LEASE_OWNERSHIP_LOST');
|
|
108
119
|
fs.unlinkSync(filePath);
|
|
@@ -110,7 +121,7 @@ export function cleanupStaleSandboxControlLease(manifest: SandboxControlManifest
|
|
|
110
121
|
}
|
|
111
122
|
|
|
112
123
|
export function appendSandboxControlAudit(
|
|
113
|
-
manifest:
|
|
124
|
+
manifest: SandboxControlManifestLike,
|
|
114
125
|
event: string,
|
|
115
126
|
fields: Record<string, string | number | boolean | null> = {},
|
|
116
127
|
now = Date.now()
|
|
@@ -125,15 +136,16 @@ export function appendSandboxControlAudit(
|
|
|
125
136
|
});
|
|
126
137
|
}
|
|
127
138
|
|
|
128
|
-
export function executionPath(manifest:
|
|
139
|
+
export function executionPath(manifest: SandboxControlManifestLike, requestId: string): string {
|
|
129
140
|
return path.join(manifest.processingDir, requestId, 'execution.json');
|
|
130
141
|
}
|
|
131
142
|
|
|
132
143
|
export function readExecution(filePath: string): SandboxControlExecution {
|
|
133
144
|
const execution = readJsonFile(filePath) as Partial<SandboxControlExecution> | null;
|
|
134
|
-
if (!execution || execution.version !==
|
|
145
|
+
if (!execution || execution.version !== 2 || typeof execution.generation !== 'string'
|
|
135
146
|
|| typeof execution.requestId !== 'string' || typeof execution.nonce !== 'string'
|
|
136
|
-
|| !execution.child || !Number.isSafeInteger(execution.child.pid) || typeof execution.child.startTime !== '
|
|
147
|
+
|| !execution.child || !Number.isSafeInteger(execution.child.pid) || typeof execution.child.startTime !== 'number'
|
|
148
|
+
|| !Number.isSafeInteger(execution.child.startTime)
|
|
137
149
|
|| !['prepared', 'running', 'terminating'].includes(execution.phase ?? '')
|
|
138
150
|
|| !Number.isSafeInteger(execution.updatedAt)) throw new Error('SANDBOX_CONTROL_EXECUTION_INVALID');
|
|
139
151
|
return execution as SandboxControlExecution;
|
|
@@ -141,31 +153,48 @@ export function readExecution(filePath: string): SandboxControlExecution {
|
|
|
141
153
|
|
|
142
154
|
export function terminateSandboxControlExecution(
|
|
143
155
|
execution: SandboxControlExecution,
|
|
144
|
-
options: {
|
|
156
|
+
options: {
|
|
157
|
+
platform?: NodeJS.Platform;
|
|
158
|
+
timeoutMs?: number;
|
|
159
|
+
deadlineAt?: number;
|
|
160
|
+
forceAt?: number;
|
|
161
|
+
allowForce?: boolean;
|
|
162
|
+
identityProbe?: ProcessIdentityProbe;
|
|
163
|
+
} = {}
|
|
145
164
|
): boolean {
|
|
146
165
|
const platform = options.platform ?? process.platform;
|
|
147
166
|
const processGroupId = execution.child.processGroupId;
|
|
148
|
-
const
|
|
167
|
+
const processGroupState = (groupId: number): 'alive' | 'dead' | 'unknown' => {
|
|
149
168
|
try {
|
|
150
169
|
const rows = execFileSync('ps', ['-axo', 'pgid=,stat='], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
151
170
|
return rows.split('\n').some((row) => {
|
|
152
171
|
const match = row.trim().match(/^(\d+)\s+(\S+)/);
|
|
153
172
|
return Number(match?.[1]) === groupId && !match?.[2]?.startsWith('Z');
|
|
154
|
-
});
|
|
173
|
+
}) ? 'alive' : 'dead';
|
|
155
174
|
} catch {
|
|
156
175
|
try {
|
|
157
176
|
process.kill(-groupId, 0);
|
|
158
|
-
return
|
|
177
|
+
return 'alive';
|
|
159
178
|
} catch (error) {
|
|
160
|
-
|
|
179
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
180
|
+
if (code === 'ESRCH') return 'dead';
|
|
181
|
+
if (code === 'EPERM') return 'alive';
|
|
182
|
+
return 'unknown';
|
|
161
183
|
}
|
|
162
184
|
}
|
|
163
185
|
};
|
|
164
|
-
const
|
|
186
|
+
const treeState = (): 'alive' | 'dead' | 'unknown' => {
|
|
165
187
|
if (platform === 'win32' || !processGroupId) {
|
|
166
|
-
return
|
|
188
|
+
return (options.identityProbe ?? getProcessIdentityState)({
|
|
189
|
+
pid: execution.child.pid, startTime: execution.child.startTime
|
|
190
|
+
});
|
|
167
191
|
}
|
|
168
|
-
return
|
|
192
|
+
return processGroupState(processGroupId);
|
|
193
|
+
};
|
|
194
|
+
const treeAlive = (): boolean => {
|
|
195
|
+
const state = treeState();
|
|
196
|
+
if (state === 'unknown') throw new Error('SANDBOX_CONTROL_EXECUTION_OWNER_UNAVAILABLE');
|
|
197
|
+
return state === 'alive';
|
|
169
198
|
};
|
|
170
199
|
if (!treeAlive()) return true;
|
|
171
200
|
const command = buildProcessTreeStopCommand(processGroupId ?? execution.child.pid, platform);
|
|
@@ -175,18 +204,23 @@ export function terminateSandboxControlExecution(
|
|
|
175
204
|
} catch {
|
|
176
205
|
// The identity check below distinguishes an already-exited tree from a failed termination.
|
|
177
206
|
}
|
|
178
|
-
const deadline = Date.now() + (options.timeoutMs ?? SANDBOX_CONTROL_EXECUTION_STOP_MS);
|
|
179
|
-
|
|
207
|
+
const deadline = options.deadlineAt ?? (Date.now() + (options.timeoutMs ?? SANDBOX_CONTROL_EXECUTION_STOP_MS));
|
|
208
|
+
const forceAt = options.forceAt ?? (options.deadlineAt === undefined
|
|
209
|
+
? deadline
|
|
210
|
+
: Math.min(deadline, Date.now() + Math.floor(Math.max(0, deadline - Date.now()) / 2)));
|
|
211
|
+
while (Date.now() < forceAt) {
|
|
180
212
|
if (!treeAlive()) return true;
|
|
181
213
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
|
|
182
214
|
}
|
|
183
|
-
if (platform !== 'win32'
|
|
215
|
+
if (platform !== 'win32' && options.allowForce !== false
|
|
216
|
+
&& (options.deadlineAt === undefined || Date.now() < deadline)) {
|
|
184
217
|
try {
|
|
185
218
|
process.kill(processGroupId ? -processGroupId : execution.child.pid, 'SIGKILL');
|
|
186
219
|
} catch {
|
|
187
220
|
// The final identity check handles an already-exited tree.
|
|
188
221
|
}
|
|
189
|
-
const killDeadline =
|
|
222
|
+
const killDeadline = options.deadlineAt
|
|
223
|
+
?? (Date.now() + (options.timeoutMs ?? SANDBOX_CONTROL_EXECUTION_STOP_MS));
|
|
190
224
|
while (Date.now() < killDeadline) {
|
|
191
225
|
if (!treeAlive()) return true;
|
|
192
226
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type SandboxControlBackoffStep = Readonly<{
|
|
2
|
+
delayMs: number;
|
|
3
|
+
nextDelayMs: number;
|
|
4
|
+
}>;
|
|
5
|
+
|
|
6
|
+
export function nextSandboxControlBackoff(currentDelayMs: number, capMs: number): SandboxControlBackoffStep {
|
|
7
|
+
const delayMs = Math.min(currentDelayMs, capMs);
|
|
8
|
+
return {
|
|
9
|
+
delayMs,
|
|
10
|
+
nextDelayMs: Math.min(capMs, delayMs * 2)
|
|
11
|
+
};
|
|
12
|
+
}
|