@fitlab-ai/agent-infra 0.9.7 → 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 +5 -0
- package/dist/lib/agent-clients/adapters/claude-code.js +3 -3
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +2 -1
- package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +2 -1
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +30 -2
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +2 -1
- package/dist/lib/decide.js +68 -14
- package/dist/lib/init.js +1 -1
- 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 +11 -3
- 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 +5 -3
- package/dist/lib/runtime/agent-runtime.js +56 -0
- package/dist/lib/sandbox/commands/create.js +389 -311
- package/dist/lib/sandbox/control/container-identity.js +1 -1
- package/dist/lib/sandbox/control/executor.js +10 -2
- package/dist/lib/sandbox/control/lifecycle.js +392 -72
- package/dist/lib/sandbox/control/state.js +27 -11
- package/dist/lib/sandbox/engine.js +14 -7
- package/dist/lib/sandbox/mounts.js +20 -5
- package/dist/lib/sandbox/recovery.js +78 -10
- package/dist/lib/sandbox/workspace-view.js +62 -40
- package/dist/lib/server/process-state.js +51 -1
- package/dist/lib/task/activity-intent.js +4 -1
- package/dist/lib/task/decision-intents.js +4 -2
- package/dist/lib/task/delegation-receipts.js +37 -16
- package/dist/lib/task/events.js +9 -5
- 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/orchestration.js +60 -6
- package/dist/lib/task/review-finalization.js +3 -2
- package/dist/lib/task/verification-engine.js +40 -14
- 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 +3 -3
- package/lib/agent-clients/adapters/claude-code.ts +3 -3
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +2 -1
- package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +2 -1
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +29 -2
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +2 -1
- package/lib/decide.ts +59 -14
- package/lib/init.ts +1 -1
- 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 +12 -4
- 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 +10 -3
- package/lib/runtime/agent-runtime.ts +76 -0
- package/lib/sandbox/commands/create.ts +88 -4
- package/lib/sandbox/config.ts +2 -1
- package/lib/sandbox/control/container-identity.ts +3 -3
- package/lib/sandbox/control/executor.ts +10 -2
- package/lib/sandbox/control/lifecycle.ts +463 -76
- package/lib/sandbox/control/protocol.ts +5 -2
- package/lib/sandbox/control/state.ts +35 -18
- package/lib/sandbox/engine.ts +20 -9
- package/lib/sandbox/mounts.ts +23 -5
- package/lib/sandbox/recovery.ts +90 -9
- package/lib/sandbox/workspace-view.ts +70 -40
- package/lib/server/process-state.ts +48 -1
- package/lib/task/activity-intent.ts +4 -2
- package/lib/task/decision-intents.ts +4 -2
- package/lib/task/delegation-receipts.ts +46 -18
- package/lib/task/events.ts +10 -5
- 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/orchestration.ts +89 -7
- package/lib/task/review-finalization.ts +6 -2
- package/lib/task/verification-engine.ts +42 -16
- 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/hooks/lifecycle-delegation.js +8 -3
- package/templates/.agents/rules/lifecycle-orchestration.en.md +3 -2
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +3 -2
- 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
|
@@ -27,12 +27,15 @@ export type SandboxControlContainerIdentity = Readonly<{
|
|
|
27
27
|
id: string;
|
|
28
28
|
labels: Readonly<Record<string, string>>;
|
|
29
29
|
}>;
|
|
30
|
-
export type
|
|
31
|
-
|
|
30
|
+
export type SandboxControlManifestBase = Readonly<{
|
|
31
|
+
engine: string; repoRoot: string; worktreeRoot: string; project: string; container: string;
|
|
32
32
|
containerIdentity: SandboxControlContainerIdentity;
|
|
33
33
|
branch: string; mode: 'task-bound' | 'branch-only'; taskId: string | null; token: string;
|
|
34
34
|
generation: string; channelDir: string; publicStatusDir: string; processingDir: string;
|
|
35
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;
|
|
36
39
|
export type SandboxControlBrokerOwner = ProcessIdentity & Readonly<{
|
|
37
40
|
version: 3;
|
|
38
41
|
brokerId: string;
|
|
@@ -3,12 +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 {
|
|
7
|
-
import type { ProcessIdentity } from '../../server/process-state.ts';
|
|
6
|
+
import { getProcessIdentityState } from '../../server/process-state.ts';
|
|
7
|
+
import type { ProcessIdentity, ProcessIdentityProbe } from '../../server/process-state.ts';
|
|
8
8
|
import type {
|
|
9
9
|
SandboxControlExecution,
|
|
10
10
|
SandboxControlLease,
|
|
11
|
-
|
|
11
|
+
SandboxControlManifestLike,
|
|
12
12
|
SandboxControlStatus
|
|
13
13
|
} from './protocol.ts';
|
|
14
14
|
|
|
@@ -36,12 +36,12 @@ export function readJsonFile(filePath: string): unknown {
|
|
|
36
36
|
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
export function statusPath(manifest:
|
|
39
|
+
export function statusPath(manifest: SandboxControlManifestLike): string {
|
|
40
40
|
return path.join(manifest.publicStatusDir, 'status.json');
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
export function writeSandboxControlStatus(
|
|
44
|
-
manifest:
|
|
44
|
+
manifest: SandboxControlManifestLike,
|
|
45
45
|
broker: ProcessIdentity & { brokerId: string },
|
|
46
46
|
state: SandboxControlStatus['state'],
|
|
47
47
|
reasonCode: string | null,
|
|
@@ -72,7 +72,7 @@ export function readSandboxControlStatus(directory: string): SandboxControlStatu
|
|
|
72
72
|
return parseSandboxControlStatus(readJsonFile(path.join(directory, 'status.json')));
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
export function readActiveLease(manifest:
|
|
75
|
+
export function readActiveLease(manifest: SandboxControlManifestLike, now = Date.now()): SandboxControlLease | null {
|
|
76
76
|
const filePath = path.join(path.dirname(manifest.publicStatusDir), 'lease.json');
|
|
77
77
|
if (!fs.existsSync(filePath)) return null;
|
|
78
78
|
const lease = readJsonFile(filePath) as Partial<SandboxControlLease> | null;
|
|
@@ -90,7 +90,11 @@ export function readActiveLease(manifest: SandboxControlManifest, now = Date.now
|
|
|
90
90
|
return lease as SandboxControlLease;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
export function cleanupStaleSandboxControlLease(
|
|
93
|
+
export function cleanupStaleSandboxControlLease(
|
|
94
|
+
manifest: SandboxControlManifestLike,
|
|
95
|
+
now = Date.now(),
|
|
96
|
+
options: Readonly<{ identityProbe?: ProcessIdentityProbe }> = {}
|
|
97
|
+
): boolean {
|
|
94
98
|
const filePath = path.join(path.dirname(manifest.publicStatusDir), 'lease.json');
|
|
95
99
|
if (!fs.existsSync(filePath)) return false;
|
|
96
100
|
const raw = fs.readFileSync(filePath, 'utf8');
|
|
@@ -104,10 +108,12 @@ export function cleanupStaleSandboxControlLease(manifest: SandboxControlManifest
|
|
|
104
108
|
|| typeof lease.branch !== 'string' || typeof lease.reason !== 'string') {
|
|
105
109
|
throw new Error('SANDBOX_CONTROL_LEASE_INVALID');
|
|
106
110
|
}
|
|
107
|
-
const
|
|
108
|
-
|| lease.branch !== manifest.branch || lease.expiresAt! <= now || !processIdentityMatches({
|
|
111
|
+
const ownerState = (options.identityProbe ?? getProcessIdentityState)({
|
|
109
112
|
pid: lease.owner.pid!, startTime: lease.owner.startTime!
|
|
110
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';
|
|
111
117
|
if (!stale) return false;
|
|
112
118
|
if (fs.readFileSync(filePath, 'utf8') !== raw) throw new Error('SANDBOX_CONTROL_LEASE_OWNERSHIP_LOST');
|
|
113
119
|
fs.unlinkSync(filePath);
|
|
@@ -115,7 +121,7 @@ export function cleanupStaleSandboxControlLease(manifest: SandboxControlManifest
|
|
|
115
121
|
}
|
|
116
122
|
|
|
117
123
|
export function appendSandboxControlAudit(
|
|
118
|
-
manifest:
|
|
124
|
+
manifest: SandboxControlManifestLike,
|
|
119
125
|
event: string,
|
|
120
126
|
fields: Record<string, string | number | boolean | null> = {},
|
|
121
127
|
now = Date.now()
|
|
@@ -130,7 +136,7 @@ export function appendSandboxControlAudit(
|
|
|
130
136
|
});
|
|
131
137
|
}
|
|
132
138
|
|
|
133
|
-
export function executionPath(manifest:
|
|
139
|
+
export function executionPath(manifest: SandboxControlManifestLike, requestId: string): string {
|
|
134
140
|
return path.join(manifest.processingDir, requestId, 'execution.json');
|
|
135
141
|
}
|
|
136
142
|
|
|
@@ -153,31 +159,42 @@ export function terminateSandboxControlExecution(
|
|
|
153
159
|
deadlineAt?: number;
|
|
154
160
|
forceAt?: number;
|
|
155
161
|
allowForce?: boolean;
|
|
162
|
+
identityProbe?: ProcessIdentityProbe;
|
|
156
163
|
} = {}
|
|
157
164
|
): boolean {
|
|
158
165
|
const platform = options.platform ?? process.platform;
|
|
159
166
|
const processGroupId = execution.child.processGroupId;
|
|
160
|
-
const
|
|
167
|
+
const processGroupState = (groupId: number): 'alive' | 'dead' | 'unknown' => {
|
|
161
168
|
try {
|
|
162
169
|
const rows = execFileSync('ps', ['-axo', 'pgid=,stat='], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
163
170
|
return rows.split('\n').some((row) => {
|
|
164
171
|
const match = row.trim().match(/^(\d+)\s+(\S+)/);
|
|
165
172
|
return Number(match?.[1]) === groupId && !match?.[2]?.startsWith('Z');
|
|
166
|
-
});
|
|
173
|
+
}) ? 'alive' : 'dead';
|
|
167
174
|
} catch {
|
|
168
175
|
try {
|
|
169
176
|
process.kill(-groupId, 0);
|
|
170
|
-
return
|
|
177
|
+
return 'alive';
|
|
171
178
|
} catch (error) {
|
|
172
|
-
|
|
179
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
180
|
+
if (code === 'ESRCH') return 'dead';
|
|
181
|
+
if (code === 'EPERM') return 'alive';
|
|
182
|
+
return 'unknown';
|
|
173
183
|
}
|
|
174
184
|
}
|
|
175
185
|
};
|
|
176
|
-
const
|
|
186
|
+
const treeState = (): 'alive' | 'dead' | 'unknown' => {
|
|
177
187
|
if (platform === 'win32' || !processGroupId) {
|
|
178
|
-
return
|
|
188
|
+
return (options.identityProbe ?? getProcessIdentityState)({
|
|
189
|
+
pid: execution.child.pid, startTime: execution.child.startTime
|
|
190
|
+
});
|
|
179
191
|
}
|
|
180
|
-
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';
|
|
181
198
|
};
|
|
182
199
|
if (!treeAlive()) return true;
|
|
183
200
|
const command = buildProcessTreeStopCommand(processGroupId ?? execution.child.pid, platform);
|
package/lib/sandbox/engine.ts
CHANGED
|
@@ -18,10 +18,20 @@ const PLATFORM_DEFAULTS = Object.freeze({
|
|
|
18
18
|
win32: ENGINES.WSL2
|
|
19
19
|
});
|
|
20
20
|
|
|
21
|
+
export type SandboxEngineInput = string | Record<string, string | null | undefined> | null | undefined;
|
|
22
|
+
|
|
21
23
|
type EngineConfig = EffectiveSandboxConfig & {
|
|
22
|
-
engine?:
|
|
24
|
+
engine?: SandboxEngineInput;
|
|
23
25
|
};
|
|
24
26
|
|
|
27
|
+
function isPerPlatformEngine(engine: SandboxEngineInput): engine is Record<string, string | null | undefined> {
|
|
28
|
+
return typeof engine === 'object' && engine !== null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function resolveEngineForPlatform(engine: SandboxEngineInput, os: string): string | null | undefined {
|
|
32
|
+
return isPerPlatformEngine(engine) ? engine[os] : engine;
|
|
33
|
+
}
|
|
34
|
+
|
|
25
35
|
type EngineDependencies = {
|
|
26
36
|
platformFn?: typeof platform;
|
|
27
37
|
runFn?: RunFns['run'];
|
|
@@ -83,33 +93,34 @@ function ensureBuildKit(engine: string, runOkEngineFn: typeof runOkEngine): void
|
|
|
83
93
|
}
|
|
84
94
|
|
|
85
95
|
export function validateSandboxEngine(
|
|
86
|
-
engine:
|
|
96
|
+
engine: SandboxEngineInput,
|
|
87
97
|
{ platformFn = platform }: Pick<EngineDependencies, 'platformFn'> = {}
|
|
88
98
|
): string | null {
|
|
89
|
-
|
|
99
|
+
const os = platformFn();
|
|
100
|
+
const resolved = resolveEngineForPlatform(engine, os);
|
|
101
|
+
if (resolved === null || resolved === undefined) {
|
|
90
102
|
return null;
|
|
91
103
|
}
|
|
92
104
|
|
|
93
|
-
|
|
94
|
-
if (!(engine in ADAPTERS)) {
|
|
105
|
+
if (!(resolved in ADAPTERS)) {
|
|
95
106
|
const known = Object.keys(ADAPTERS).join(', ');
|
|
96
107
|
throw new Error(
|
|
97
|
-
`sandbox: invalid "sandbox.engine" value "${
|
|
108
|
+
`sandbox: invalid "sandbox.engine" value "${resolved}" (unknown sandbox engine). `
|
|
98
109
|
+ `Valid engines: ${known}.`
|
|
99
110
|
);
|
|
100
111
|
}
|
|
101
112
|
|
|
102
|
-
const adapter = ADAPTERS[
|
|
113
|
+
const adapter = ADAPTERS[resolved as keyof typeof ADAPTERS];
|
|
103
114
|
if (!adapter.supportedPlatforms.includes(os)) {
|
|
104
115
|
const supported = enginesForPlatform(os);
|
|
105
116
|
const supportedList = supported.length > 0 ? supported.join(', ') : 'none';
|
|
106
117
|
throw new Error(
|
|
107
|
-
`sandbox: "sandbox.engine" value "${
|
|
118
|
+
`sandbox: "sandbox.engine" value "${resolved}" is not supported on ${os}. `
|
|
108
119
|
+ `Supported engines on ${os}: ${supportedList}.`
|
|
109
120
|
);
|
|
110
121
|
}
|
|
111
122
|
|
|
112
|
-
return
|
|
123
|
+
return resolved;
|
|
113
124
|
}
|
|
114
125
|
|
|
115
126
|
export function detectEngine(
|
package/lib/sandbox/mounts.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
worktreeDirCandidates
|
|
7
7
|
} from './constants.ts';
|
|
8
8
|
import type { SandboxConfig } from './config.ts';
|
|
9
|
+
import { sandboxWorkspaceViewStatePaths } from './workspace-view.ts';
|
|
9
10
|
|
|
10
11
|
export type SandboxBindMountDeclaration = {
|
|
11
12
|
hostPaths: string[];
|
|
@@ -25,11 +26,25 @@ export function sandboxCoreBindMounts(
|
|
|
25
26
|
workspaceViewRoot: string;
|
|
26
27
|
controlDir: string;
|
|
27
28
|
controlStatusDir: string;
|
|
29
|
+
runtimeDir?: string;
|
|
28
30
|
taskSource?: string;
|
|
29
31
|
taskId?: string;
|
|
30
32
|
}
|
|
31
33
|
): SandboxBindMountDeclaration[] {
|
|
32
34
|
const taskBound = Boolean(overrides.taskSource && overrides.taskId);
|
|
35
|
+
const workspaceMounts = sandboxWorkspaceViewStatePaths(overrides.workspaceViewRoot).map(
|
|
36
|
+
({ state, hostPath }) => taskBound && state === 'active'
|
|
37
|
+
? {
|
|
38
|
+
hostPaths: [path.join(hostPath, '.short-ids.json')],
|
|
39
|
+
containerPath: '/workspace/.agents/workspace/active/.short-ids.json',
|
|
40
|
+
readOnly: true
|
|
41
|
+
}
|
|
42
|
+
: {
|
|
43
|
+
hostPaths: [hostPath],
|
|
44
|
+
containerPath: path.posix.join('/workspace/.agents/workspace', state),
|
|
45
|
+
readOnly: true
|
|
46
|
+
}
|
|
47
|
+
);
|
|
33
48
|
const mounts: SandboxBindMountDeclaration[] = [
|
|
34
49
|
{
|
|
35
50
|
hostPaths: overrides.worktree
|
|
@@ -38,11 +53,7 @@ export function sandboxCoreBindMounts(
|
|
|
38
53
|
containerPath: '/workspace',
|
|
39
54
|
readOnly: false
|
|
40
55
|
},
|
|
41
|
-
|
|
42
|
-
hostPaths: [overrides.workspaceViewRoot],
|
|
43
|
-
containerPath: '/workspace/.agents/workspace',
|
|
44
|
-
readOnly: true
|
|
45
|
-
},
|
|
56
|
+
...workspaceMounts,
|
|
46
57
|
];
|
|
47
58
|
if (taskBound) {
|
|
48
59
|
mounts.push({
|
|
@@ -79,5 +90,12 @@ export function sandboxCoreBindMounts(
|
|
|
79
90
|
containerPath: '/run/agent-infra/control',
|
|
80
91
|
readOnly: false
|
|
81
92
|
});
|
|
93
|
+
if (taskBound && overrides.runtimeDir) {
|
|
94
|
+
mounts.push({
|
|
95
|
+
hostPaths: [overrides.runtimeDir],
|
|
96
|
+
containerPath: '/run/agent-infra/runtime',
|
|
97
|
+
readOnly: false
|
|
98
|
+
});
|
|
99
|
+
}
|
|
82
100
|
return mounts;
|
|
83
101
|
}
|
package/lib/sandbox/recovery.ts
CHANGED
|
@@ -49,7 +49,7 @@ import {
|
|
|
49
49
|
startSandboxContainer,
|
|
50
50
|
type SandboxRow
|
|
51
51
|
} from './commands/list-running.ts';
|
|
52
|
-
import {
|
|
52
|
+
import { getProcessIdentityState, getProcessStartTime, removePidFileIfMatches } from '../server/process-state.ts';
|
|
53
53
|
import {
|
|
54
54
|
acquireSandboxControlBrokerStartup,
|
|
55
55
|
garbageCollectSandboxControlRoot,
|
|
@@ -93,6 +93,9 @@ export type SandboxRecoverySnapshot = {
|
|
|
93
93
|
actualBranch: string | null;
|
|
94
94
|
expectedWorkspace: SandboxWorkspaceIdentity;
|
|
95
95
|
actualWorkspace: SandboxContainerWorkspaceIdentity;
|
|
96
|
+
workspaceTopology?: 'legacy-parent' | 'per-state' | 'unknown';
|
|
97
|
+
taskView?: { path: string; readable: boolean };
|
|
98
|
+
runtimeStoreOk?: boolean;
|
|
96
99
|
runtimeCapabilityOk?: boolean;
|
|
97
100
|
unexpectedCapabilityMounts?: string[];
|
|
98
101
|
mounts: Array<{
|
|
@@ -251,7 +254,9 @@ export async function startSandboxControlBroker(repoRoot: string, manifestPath:
|
|
|
251
254
|
if (broker === null || broker.version !== 3 || owner === null) {
|
|
252
255
|
throw new Error('SANDBOX_CONTROL_BROKER_VERSION_INVALID: rebuild the sandbox container before starting a new broker');
|
|
253
256
|
}
|
|
254
|
-
const
|
|
257
|
+
const ownerState = getProcessIdentityState({ pid: owner.pid, startTime: owner.startTime });
|
|
258
|
+
if (ownerState === 'unknown') throw new Error('SANDBOX_CONTROL_BROKER_OWNER_UNAVAILABLE');
|
|
259
|
+
const live = ownerState === 'alive';
|
|
255
260
|
if (
|
|
256
261
|
live
|
|
257
262
|
&& owner !== null
|
|
@@ -349,6 +354,14 @@ async function ensureSandboxControlBroker(params: {
|
|
|
349
354
|
token?: unknown;
|
|
350
355
|
generation?: unknown;
|
|
351
356
|
};
|
|
357
|
+
const validIdentity = typeof broker.pid === 'number'
|
|
358
|
+
&& Number.isSafeInteger(broker.pid)
|
|
359
|
+
&& typeof broker.startTime === 'number'
|
|
360
|
+
&& Number.isSafeInteger(broker.startTime);
|
|
361
|
+
const ownerState = validIdentity
|
|
362
|
+
? getProcessIdentityState({ pid: broker.pid as number, startTime: broker.startTime as number })
|
|
363
|
+
: 'dead';
|
|
364
|
+
if (ownerState === 'unknown') throw new Error('SANDBOX_CONTROL_BROKER_OWNER_UNAVAILABLE');
|
|
352
365
|
if (
|
|
353
366
|
broker.version === 3
|
|
354
367
|
&& typeof broker.pid === 'number'
|
|
@@ -359,7 +372,7 @@ async function ensureSandboxControlBroker(params: {
|
|
|
359
372
|
&& broker.brokerId.length > 0
|
|
360
373
|
&& broker.token === validatedManifest.token
|
|
361
374
|
&& broker.generation === validatedManifest.generation
|
|
362
|
-
&&
|
|
375
|
+
&& ownerState === 'alive'
|
|
363
376
|
) {
|
|
364
377
|
try {
|
|
365
378
|
const status = readSandboxControlStatus(control.statusDir);
|
|
@@ -379,7 +392,8 @@ async function ensureSandboxControlBroker(params: {
|
|
|
379
392
|
});
|
|
380
393
|
return;
|
|
381
394
|
}
|
|
382
|
-
} catch {
|
|
395
|
+
} catch (error) {
|
|
396
|
+
if (error instanceof Error && error.message === 'SANDBOX_CONTROL_BROKER_OWNER_UNAVAILABLE') throw error;
|
|
383
397
|
// A missing, stale, or malformed broker record is replaced below.
|
|
384
398
|
}
|
|
385
399
|
await startSandboxControlBroker(params.config.repoRoot, control.manifestPath);
|
|
@@ -416,6 +430,30 @@ export function classifySandboxRecovery(snapshot: SandboxRecoverySnapshot): Sand
|
|
|
416
430
|
message: 'Container runtime capability signature does not match the current sandbox plan.'
|
|
417
431
|
});
|
|
418
432
|
}
|
|
433
|
+
if (snapshot.workspaceTopology === 'legacy-parent') {
|
|
434
|
+
findings.push({
|
|
435
|
+
repairKind: 'hard-failure',
|
|
436
|
+
code: 'SANDBOX_WORKSPACE_TOPOLOGY_MISMATCH',
|
|
437
|
+
message: 'Sandbox uses the legacy workspace parent-mount topology; recreate the container explicitly to migrate it.',
|
|
438
|
+
path: '/workspace/.agents/workspace'
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
if (snapshot.taskView && !snapshot.taskView.readable) {
|
|
442
|
+
findings.push({
|
|
443
|
+
repairKind: 'hard-failure',
|
|
444
|
+
code: 'SANDBOX_TASK_VIEW_UNREADABLE',
|
|
445
|
+
message: `Bound task view is not readable at ${snapshot.taskView.path}.`,
|
|
446
|
+
path: snapshot.taskView.path
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
if (snapshot.runtimeStoreOk === false) {
|
|
450
|
+
findings.push({
|
|
451
|
+
repairKind: 'hard-failure',
|
|
452
|
+
code: 'SANDBOX_RUNTIME_STORE_UNAVAILABLE',
|
|
453
|
+
message: 'Task-bound runtime store is not writable and readable by devuser.',
|
|
454
|
+
path: '/run/agent-infra/runtime'
|
|
455
|
+
});
|
|
456
|
+
}
|
|
419
457
|
for (const mountPath of snapshot.unexpectedCapabilityMounts ?? []) {
|
|
420
458
|
findings.push({
|
|
421
459
|
repairKind: 'hard-failure',
|
|
@@ -425,6 +463,10 @@ export function classifySandboxRecovery(snapshot: SandboxRecoverySnapshot): Sand
|
|
|
425
463
|
}
|
|
426
464
|
|
|
427
465
|
for (const mount of snapshot.mounts) {
|
|
466
|
+
if (
|
|
467
|
+
snapshot.workspaceTopology === 'legacy-parent'
|
|
468
|
+
&& mount.path.startsWith('/workspace/.agents/workspace')
|
|
469
|
+
) continue;
|
|
428
470
|
if (
|
|
429
471
|
mount.actualType !== mount.expectedType
|
|
430
472
|
|| !mount.sourceMatches
|
|
@@ -517,6 +559,20 @@ function inspectContainer(
|
|
|
517
559
|
return parsed[0] as DockerInspection;
|
|
518
560
|
}
|
|
519
561
|
|
|
562
|
+
function workspaceTopology(mountsByDestination: ReadonlyMap<string, DockerMount>): SandboxRecoverySnapshot['workspaceTopology'] {
|
|
563
|
+
if (mountsByDestination.has('/workspace/.agents/workspace')) return 'legacy-parent';
|
|
564
|
+
const perState = [
|
|
565
|
+
'/workspace/.agents/workspace/active',
|
|
566
|
+
'/workspace/.agents/workspace/active/.short-ids.json',
|
|
567
|
+
'/workspace/.agents/workspace/completed',
|
|
568
|
+
'/workspace/.agents/workspace/blocked',
|
|
569
|
+
'/workspace/.agents/workspace/archive'
|
|
570
|
+
];
|
|
571
|
+
return perState.some((destination) => mountsByDestination.has(destination))
|
|
572
|
+
? 'per-state'
|
|
573
|
+
: 'unknown';
|
|
574
|
+
}
|
|
575
|
+
|
|
520
576
|
function probe(
|
|
521
577
|
engine: string,
|
|
522
578
|
container: string,
|
|
@@ -596,6 +652,7 @@ function expectedMounts(params: {
|
|
|
596
652
|
controlStatusDir: control.statusDir,
|
|
597
653
|
...(params.workspace.mode === 'task-bound'
|
|
598
654
|
? {
|
|
655
|
+
runtimeDir: control.runtimeDir,
|
|
599
656
|
taskSource: assertSandboxTaskSource(config.repoRoot, params.workspace.taskId),
|
|
600
657
|
taskId: params.workspace.taskId
|
|
601
658
|
}
|
|
@@ -729,6 +786,10 @@ export function collectSandboxRecoverySnapshot(params: {
|
|
|
729
786
|
const capabilityPlan = createSandboxCapabilityPlan(params.config);
|
|
730
787
|
const workspace = params.workspace ?? { mode: 'branch-only' as const };
|
|
731
788
|
const tools = [...capabilityPlan.tools];
|
|
789
|
+
const topology = workspaceTopology(mountsByDestination);
|
|
790
|
+
const taskViewPath = workspace.mode === 'task-bound'
|
|
791
|
+
? `/workspace/.agents/workspace/active/${workspace.taskId}/task.md`
|
|
792
|
+
: null;
|
|
732
793
|
const seeds = declaredTmpfsSeedEntries(tools).map((seed) => {
|
|
733
794
|
const mounted = mountsByDestination.get(seed.stagingPath)?.Type === 'bind';
|
|
734
795
|
return {
|
|
@@ -759,6 +820,15 @@ export function collectSandboxRecoverySnapshot(params: {
|
|
|
759
820
|
runOkFn
|
|
760
821
|
)
|
|
761
822
|
}));
|
|
823
|
+
const runtimeStoreOk = workspace.mode === 'task-bound'
|
|
824
|
+
? probe(
|
|
825
|
+
params.engine,
|
|
826
|
+
params.container,
|
|
827
|
+
'probe="$1/.agent-infra-runtime-$$"; trap \'rm -f -- "$probe"\' EXIT; printf runtime-probe > "$probe" && test "$(cat -- "$probe")" = runtime-probe',
|
|
828
|
+
['/run/agent-infra/runtime'],
|
|
829
|
+
runOkFn
|
|
830
|
+
)
|
|
831
|
+
: undefined;
|
|
762
832
|
const labels = inspection.Config?.Labels ?? {};
|
|
763
833
|
const branchLabel = labels[sandboxBranchLabel(params.config)];
|
|
764
834
|
const containerWorkspace = parseSandboxWorkspaceIdentity(labels, {
|
|
@@ -820,6 +890,20 @@ export function collectSandboxRecoverySnapshot(params: {
|
|
|
820
890
|
actualBranch: typeof branchLabel === 'string' ? branchLabel : null,
|
|
821
891
|
expectedWorkspace: workspace,
|
|
822
892
|
actualWorkspace: containerWorkspace,
|
|
893
|
+
workspaceTopology: topology,
|
|
894
|
+
...(taskViewPath === null ? {} : {
|
|
895
|
+
taskView: {
|
|
896
|
+
path: taskViewPath,
|
|
897
|
+
readable: probe(
|
|
898
|
+
params.engine,
|
|
899
|
+
params.container,
|
|
900
|
+
'test -r "$1"',
|
|
901
|
+
[taskViewPath],
|
|
902
|
+
runOkFn
|
|
903
|
+
)
|
|
904
|
+
}
|
|
905
|
+
}),
|
|
906
|
+
...(runtimeStoreOk === undefined ? {} : { runtimeStoreOk }),
|
|
823
907
|
identityOk: typeof inspection.Id === 'string' && inspection.Id.length > 0
|
|
824
908
|
&& branchLabel === params.branch
|
|
825
909
|
&& sameSandboxWorkspaceIdentity(containerWorkspace, workspace),
|
|
@@ -996,11 +1080,13 @@ function assess(params: {
|
|
|
996
1080
|
function canRetryFreshReadiness(findings: SandboxRecoveryFinding[]): boolean {
|
|
997
1081
|
return findings.length > 0 && findings.every((finding) =>
|
|
998
1082
|
finding.repairKind === 'hard-failure'
|
|
1083
|
+
&& finding.code !== 'SANDBOX_WORKSPACE_TOPOLOGY_MISMATCH'
|
|
999
1084
|
&& finding.path !== undefined
|
|
1000
1085
|
&& (
|
|
1001
1086
|
finding.path.startsWith('/workspace/.agents/workspace')
|
|
1002
1087
|
|| finding.path === '/home/devuser/.host-shell-config'
|
|
1003
1088
|
|| finding.path === '/home/devuser/.bash_aliases'
|
|
1089
|
+
|| finding.path === '/run/agent-infra/runtime'
|
|
1004
1090
|
)
|
|
1005
1091
|
);
|
|
1006
1092
|
}
|
|
@@ -1236,14 +1322,9 @@ export async function ensureSandboxReady(params: EnsureSandboxReadyParams): Prom
|
|
|
1236
1322
|
: `Sandbox recovery failed in place. Replacing only the container. ${dataBoundary}`;
|
|
1237
1323
|
warnings.push(warning);
|
|
1238
1324
|
(params.writeWarning ?? ((message) => process.stderr.write(`${message}\n`)))(warning);
|
|
1239
|
-
const runVerboseFn = deps?.runVerbose ?? runVerboseEngine;
|
|
1240
1325
|
let replacementResult: SandboxReadyResult | null = null;
|
|
1241
1326
|
let replacementFailure: unknown = null;
|
|
1242
1327
|
try {
|
|
1243
|
-
if (params.row.running) {
|
|
1244
|
-
runVerboseFn(params.engine, 'docker', ['stop', params.row.name]);
|
|
1245
|
-
}
|
|
1246
|
-
runVerboseFn(params.engine, 'docker', ['rm', params.row.name]);
|
|
1247
1328
|
await params.recreate(params.branch);
|
|
1248
1329
|
|
|
1249
1330
|
const rows = (deps?.fetchRows ?? fetchSandboxRows)(
|
|
@@ -50,6 +50,7 @@ export type SandboxControlSetup = Readonly<{
|
|
|
50
50
|
root: string;
|
|
51
51
|
channelDir: string;
|
|
52
52
|
statusDir: string;
|
|
53
|
+
runtimeDir: string;
|
|
53
54
|
manifestPath: string;
|
|
54
55
|
token: string;
|
|
55
56
|
generation: string;
|
|
@@ -60,7 +61,7 @@ export function sandboxControlPaths(params: Readonly<{
|
|
|
60
61
|
project: string;
|
|
61
62
|
container: string;
|
|
62
63
|
identity: SandboxWorkspaceIdentity;
|
|
63
|
-
}>): Readonly<{ root: string; channelDir: string; statusDir: string; processingDir: string; manifestPath: string }> {
|
|
64
|
+
}>): Readonly<{ root: string; channelDir: string; statusDir: string; processingDir: string; runtimeDir: string; manifestPath: string }> {
|
|
64
65
|
const identityKey = params.identity.mode === 'task-bound'
|
|
65
66
|
? `task-bound:${params.identity.taskId}`
|
|
66
67
|
: 'branch-only';
|
|
@@ -71,6 +72,7 @@ export function sandboxControlPaths(params: Readonly<{
|
|
|
71
72
|
channelDir: path.join(root, 'channel'),
|
|
72
73
|
statusDir: path.join(root, 'public'),
|
|
73
74
|
processingDir: path.join(root, 'processing'),
|
|
75
|
+
runtimeDir: path.join(root, 'runtime'),
|
|
74
76
|
manifestPath: path.join(root, 'manifest.json')
|
|
75
77
|
};
|
|
76
78
|
}
|
|
@@ -85,6 +87,32 @@ function assertSafeDirectory(directory: string, expectedBase: string): void {
|
|
|
85
87
|
}
|
|
86
88
|
}
|
|
87
89
|
|
|
90
|
+
function assertSafeMountTargetPath(
|
|
91
|
+
target: string,
|
|
92
|
+
worktreeRoot: string,
|
|
93
|
+
canonicalWorktreeRoot: string
|
|
94
|
+
): void {
|
|
95
|
+
const relative = path.relative(worktreeRoot, target);
|
|
96
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
97
|
+
throw new Error(`Sandbox workspace mount target escapes its worktree: ${target}`);
|
|
98
|
+
}
|
|
99
|
+
let current = worktreeRoot;
|
|
100
|
+
for (const segment of ['', ...relative.split(path.sep).filter(Boolean)]) {
|
|
101
|
+
current = segment ? path.join(current, segment) : current;
|
|
102
|
+
try {
|
|
103
|
+
if (fs.lstatSync(current).isSymbolicLink()) {
|
|
104
|
+
throw new Error(`Sandbox workspace mount target must not contain a symbolic link: ${current}`);
|
|
105
|
+
}
|
|
106
|
+
const canonicalRelative = path.relative(canonicalWorktreeRoot, fs.realpathSync.native(current));
|
|
107
|
+
if (canonicalRelative.startsWith('..') || path.isAbsolute(canonicalRelative)) {
|
|
108
|
+
throw new Error(`Sandbox workspace mount target escapes its real worktree: ${current}`);
|
|
109
|
+
}
|
|
110
|
+
} catch (error) {
|
|
111
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
88
116
|
export function assertSandboxTaskSource(repoRoot: string, taskId: string): string {
|
|
89
117
|
if (!/^TASK-\d{8}-\d{6}$/.test(taskId)) throw new Error('SANDBOX_TASK_SOURCE_INVALID');
|
|
90
118
|
const activeRoot = fs.realpathSync.native(path.join(repoRoot, '.agents', 'workspace', 'active'));
|
|
@@ -100,41 +128,22 @@ export function assertSandboxTaskSource(repoRoot: string, taskId: string): strin
|
|
|
100
128
|
return canonical;
|
|
101
129
|
}
|
|
102
130
|
|
|
103
|
-
export function
|
|
104
|
-
const
|
|
105
|
-
const
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
} else {
|
|
113
|
-
fs.mkdirSync(agentsRoot, { mode: 0o700 });
|
|
114
|
-
}
|
|
115
|
-
const canonicalAgents = fs.realpathSync.native(agentsRoot);
|
|
116
|
-
if (path.dirname(canonicalAgents) !== canonicalWorktree) {
|
|
117
|
-
throw new Error(`Sandbox workspace mount ancestor escapes the worktree: ${agentsRoot}`);
|
|
131
|
+
export function prepareSandboxWorkspaceMountTargets(worktreeRoot: string): void {
|
|
132
|
+
const resolvedWorktreeRoot = path.resolve(worktreeRoot);
|
|
133
|
+
const canonicalWorktreeRoot = fs.realpathSync.native(resolvedWorktreeRoot);
|
|
134
|
+
const workspaceRoot = path.resolve(resolvedWorktreeRoot, '.agents', 'workspace');
|
|
135
|
+
const statePaths = sandboxWorkspaceViewStatePaths(workspaceRoot);
|
|
136
|
+
const registryTarget = path.join(workspaceRoot, 'active', '.short-ids.json');
|
|
137
|
+
for (const target of [workspaceRoot, ...statePaths.map(({ hostPath }) => hostPath), registryTarget]) {
|
|
138
|
+
assertSafeMountTargetPath(target, resolvedWorktreeRoot, canonicalWorktreeRoot);
|
|
118
139
|
}
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if (stat.isSymbolicLink()) {
|
|
124
|
-
throw new Error(`Sandbox workspace mount target must not be a symbolic link: ${target}`);
|
|
125
|
-
}
|
|
126
|
-
if (!stat.isDirectory()) {
|
|
127
|
-
throw new Error(`Sandbox workspace mount target must be a directory: ${target}`);
|
|
128
|
-
}
|
|
129
|
-
if (path.dirname(fs.realpathSync.native(target)) !== canonicalAgents) {
|
|
130
|
-
throw new Error(`Sandbox workspace mount target escapes the worktree: ${target}`);
|
|
131
|
-
}
|
|
132
|
-
} else {
|
|
133
|
-
fs.mkdirSync(target, { mode: 0o700 });
|
|
140
|
+
fs.mkdirSync(workspaceRoot, { recursive: true, mode: 0o700 });
|
|
141
|
+
for (const { hostPath } of statePaths) {
|
|
142
|
+
fs.mkdirSync(hostPath, { recursive: true, mode: 0o700 });
|
|
143
|
+
fs.chmodSync(hostPath, 0o700);
|
|
134
144
|
}
|
|
135
|
-
fs.chmodSync(
|
|
136
|
-
fs.
|
|
137
|
-
return target;
|
|
145
|
+
fs.chmodSync(workspaceRoot, 0o700);
|
|
146
|
+
fs.closeSync(fs.openSync(registryTarget, 'a', 0o600));
|
|
138
147
|
}
|
|
139
148
|
|
|
140
149
|
export function materializeSandboxWorkspaceView(params: Readonly<{
|
|
@@ -181,16 +190,36 @@ export function materializeSandboxControl(params: Readonly<{
|
|
|
181
190
|
branch: string;
|
|
182
191
|
identity: SandboxWorkspaceIdentity;
|
|
183
192
|
engine?: string;
|
|
193
|
+
replacementLease?: Readonly<{
|
|
194
|
+
root: string;
|
|
195
|
+
assertOwned(): void;
|
|
196
|
+
}>;
|
|
184
197
|
}>): SandboxControlSetup {
|
|
185
|
-
const { root, channelDir, statusDir, processingDir, manifestPath } = sandboxControlPaths(params);
|
|
198
|
+
const { root, channelDir, statusDir, processingDir, runtimeDir, manifestPath } = sandboxControlPaths(params);
|
|
186
199
|
assertSafeDirectory(root, path.resolve(params.base));
|
|
200
|
+
if (fs.existsSync(root)) {
|
|
201
|
+
if (!params.replacementLease || path.resolve(params.replacementLease.root) !== root) {
|
|
202
|
+
throw new Error('SANDBOX_CONTROL_REPLACEMENT_REQUIRED');
|
|
203
|
+
}
|
|
204
|
+
params.replacementLease.assertOwned();
|
|
205
|
+
}
|
|
187
206
|
const consumedDir = path.join(root, 'consumed');
|
|
188
207
|
assertSafeDirectory(consumedDir, root);
|
|
189
208
|
fs.rmSync(consumedDir, { recursive: true, force: true });
|
|
190
209
|
fs.mkdirSync(consumedDir, { recursive: true, mode: 0o700 });
|
|
191
|
-
for (const directory of [statusDir, processingDir]) {
|
|
210
|
+
for (const directory of [statusDir, processingDir, runtimeDir]) {
|
|
192
211
|
assertSafeDirectory(directory, root);
|
|
212
|
+
if (directory === runtimeDir) fs.rmSync(directory, { recursive: true, force: true });
|
|
213
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
214
|
+
}
|
|
215
|
+
for (const directory of [
|
|
216
|
+
path.join(runtimeDir, 'clients'),
|
|
217
|
+
path.join(runtimeDir, 'clients', 'codex', 'capabilities'),
|
|
218
|
+
path.join(runtimeDir, 'clients', 'codex', 'lifecycle')
|
|
219
|
+
]) {
|
|
220
|
+
assertSafeDirectory(directory, runtimeDir);
|
|
193
221
|
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
222
|
+
fs.chmodSync(directory, 0o700);
|
|
194
223
|
}
|
|
195
224
|
for (const queue of ['requests', 'responses']) {
|
|
196
225
|
const directory = path.join(channelDir, queue);
|
|
@@ -202,7 +231,7 @@ export function materializeSandboxControl(params: Readonly<{
|
|
|
202
231
|
const generation = randomBytes(16).toString('hex');
|
|
203
232
|
const repoRoot = fs.realpathSync.native(params.repoRoot);
|
|
204
233
|
const manifest: SandboxControlManifest = {
|
|
205
|
-
version:
|
|
234
|
+
version: 5,
|
|
206
235
|
engine: params.engine ?? 'docker',
|
|
207
236
|
repoRoot,
|
|
208
237
|
worktreeRoot: fs.realpathSync.native(params.worktreeRoot),
|
|
@@ -216,10 +245,11 @@ export function materializeSandboxControl(params: Readonly<{
|
|
|
216
245
|
generation,
|
|
217
246
|
channelDir,
|
|
218
247
|
publicStatusDir: statusDir,
|
|
219
|
-
processingDir
|
|
248
|
+
processingDir,
|
|
249
|
+
runtimeDir
|
|
220
250
|
};
|
|
221
251
|
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest)}\n`, { mode: 0o600 });
|
|
222
|
-
return { root, channelDir, statusDir, manifestPath, token, generation };
|
|
252
|
+
return { root, channelDir, statusDir, runtimeDir, manifestPath, token, generation };
|
|
223
253
|
}
|
|
224
254
|
|
|
225
255
|
export function finalizeSandboxControlManifest(
|
|
@@ -228,7 +258,7 @@ export function finalizeSandboxControlManifest(
|
|
|
228
258
|
): void {
|
|
229
259
|
if (!identity.engine || !identity.id) throw new Error('SANDBOX_CONTROL_CONTAINER_ID_INVALID');
|
|
230
260
|
const manifest = JSON.parse(fs.readFileSync(setup.manifestPath, 'utf8')) as Record<string, unknown>;
|
|
231
|
-
manifest.version =
|
|
261
|
+
manifest.version = 5;
|
|
232
262
|
manifest.engine = identity.engine;
|
|
233
263
|
manifest.containerIdentity = { id: identity.id, labels: { ...identity.labels } };
|
|
234
264
|
const temporary = `${setup.manifestPath}.${process.pid}.${randomBytes(8).toString('hex')}.tmp`;
|