@fitlab-ai/agent-infra 0.11.12 → 0.11.14
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 +2 -7
- package/dist/lib/agent-clients/adapter.js +0 -3
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +5 -9
- package/dist/lib/agent-clients/adapters/codex-lifecycle/cli.js +1 -5
- package/dist/lib/agent-clients/adapters/codex-lifecycle/recovery.js +1 -5
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +11 -25
- package/dist/lib/agent-clients/adapters/codex-orchestration.js +1 -3
- package/dist/lib/agent-clients/adapters/codex.js +0 -2
- package/dist/lib/internal/cli-route-inventory.js +0 -1
- package/dist/lib/internal/sandbox-control.js +1 -81
- package/dist/lib/internal/task-finalization.js +29 -20
- package/dist/lib/internal/task-lifecycle.js +4 -2
- package/dist/lib/internal/task-operation-registry.js +1 -3
- package/dist/lib/internal/task-validate.js +14 -124
- package/dist/lib/platform/github-client.js +33 -3
- package/dist/lib/platform/release-notes.js +2 -2
- package/dist/lib/sandbox/commands/control-status.js +0 -1
- package/dist/lib/sandbox/commands/create.js +11 -6
- package/dist/lib/sandbox/commands/ls.js +4 -5
- package/dist/lib/sandbox/commands/rm.js +1 -1
- package/dist/lib/sandbox/config.js +12 -0
- package/dist/lib/sandbox/control/audit.js +1 -2
- package/dist/lib/sandbox/control/client.js +15 -110
- package/dist/lib/sandbox/control/executor.js +9 -59
- package/dist/lib/sandbox/control/host-recovery.js +89 -27
- package/dist/lib/sandbox/control/lifecycle.js +75 -9
- package/dist/lib/sandbox/control/protocol.js +11 -50
- package/dist/lib/sandbox/control/server.js +203 -253
- package/dist/lib/sandbox/control/task-view.js +0 -13
- package/dist/lib/sandbox/environment.js +7 -8
- package/dist/lib/sandbox/mounts.js +71 -52
- package/dist/lib/sandbox/recovery.js +30 -12
- package/dist/lib/sandbox/removal.js +37 -3
- package/dist/lib/sandbox/workspace-identity.js +18 -6
- package/dist/lib/sandbox/workspace-view.js +89 -6
- package/dist/lib/task/commands/ls.js +3 -3
- package/dist/lib/task/control-authority.js +41 -17
- package/dist/lib/task/control-recovery.js +2 -4
- package/dist/lib/task/finalization.js +27 -2
- package/dist/lib/task/lifecycle.js +294 -63
- package/dist/lib/task/orchestration.js +26 -72
- package/dist/lib/task/short-id.js +37 -27
- package/dist/lib/task/verification-engine.js +4 -11
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapter.ts +0 -27
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +6 -12
- package/lib/agent-clients/adapters/codex-lifecycle/cli.ts +1 -5
- package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +0 -7
- package/lib/agent-clients/adapters/codex-lifecycle/recovery.ts +1 -5
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +21 -27
- package/lib/agent-clients/adapters/codex-orchestration.ts +1 -3
- package/lib/agent-clients/adapters/codex.ts +0 -2
- package/lib/internal/cli-route-inventory.ts +0 -1
- package/lib/internal/sandbox-control.ts +0 -97
- package/lib/internal/task-finalization.ts +33 -19
- package/lib/internal/task-lifecycle.ts +4 -2
- package/lib/internal/task-operation-registry.ts +1 -3
- package/lib/internal/task-validate.ts +13 -127
- package/lib/platform/github-client.ts +34 -3
- package/lib/platform/release-notes.ts +2 -2
- package/lib/sandbox/commands/control-status.ts +0 -1
- package/lib/sandbox/commands/create.ts +11 -5
- package/lib/sandbox/commands/ls.ts +4 -5
- package/lib/sandbox/commands/rm.ts +1 -1
- package/lib/sandbox/config.ts +13 -0
- package/lib/sandbox/control/audit.ts +1 -2
- package/lib/sandbox/control/client.ts +17 -138
- package/lib/sandbox/control/executor.ts +8 -65
- package/lib/sandbox/control/host-recovery.ts +94 -21
- package/lib/sandbox/control/lifecycle.ts +75 -10
- package/lib/sandbox/control/protocol.ts +12 -62
- package/lib/sandbox/control/server.ts +11 -61
- package/lib/sandbox/control/task-view.ts +0 -19
- package/lib/sandbox/environment.ts +7 -8
- package/lib/sandbox/mounts.ts +93 -68
- package/lib/sandbox/recovery.ts +37 -19
- package/lib/sandbox/removal.ts +36 -5
- package/lib/sandbox/workspace-identity.ts +16 -6
- package/lib/sandbox/workspace-view.ts +81 -7
- package/lib/task/commands/ls.ts +3 -3
- package/lib/task/control-authority.ts +41 -26
- package/lib/task/control-recovery.ts +3 -5
- package/lib/task/finalization.ts +29 -2
- package/lib/task/lifecycle.ts +282 -57
- package/lib/task/orchestration.ts +30 -68
- package/lib/task/short-id.ts +33 -40
- package/lib/task/verification-engine.ts +3 -9
- package/package.json +1 -1
- package/templates/.agents/rules/README.en.md +1 -0
- package/templates/.agents/rules/README.zh-CN.md +1 -0
- package/templates/.agents/rules/lifecycle-orchestration.en.md +3 -4
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +3 -4
- package/templates/.agents/rules/strategy-pattern.en.md +25 -0
- package/templates/.agents/rules/strategy-pattern.zh-CN.md +25 -0
- package/templates/.agents/rules/task-short-id.en.md +18 -12
- package/templates/.agents/rules/task-short-id.zh-CN.md +12 -7
- package/templates/.agents/skills/code-task/reference/code-rules.en.md +4 -0
- package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +4 -0
- package/templates/.agents/skills/complete-task/SKILL.en.md +5 -26
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +5 -23
- package/templates/.agents/skills/create-release-note/SKILL.en.md +3 -1
- package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/plan-task/SKILL.en.md +7 -4
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +7 -4
- package/templates/.agents/skills/run-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +2 -2
- package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-client.js +0 -130
- package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-context.js +0 -127
- package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-operation.js +0 -131
- package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-registration.js +0 -275
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller-cli.js +0 -106
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +0 -345
- package/dist/lib/sandbox/control/agent-operation.js +0 -34
- package/lib/agent-clients/adapters/codex-lifecycle/controller-client.ts +0 -158
- package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +0 -163
- package/lib/agent-clients/adapters/codex-lifecycle/controller-operation.ts +0 -151
- package/lib/agent-clients/adapters/codex-lifecycle/controller-registration.ts +0 -352
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller-cli.ts +0 -109
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +0 -437
- package/lib/sandbox/control/agent-operation.ts +0 -49
|
@@ -2,10 +2,13 @@ import path from 'node:path';
|
|
|
2
2
|
import { shareBranchDir, shareCommonDir, shellConfigDirCandidates, worktreeDirCandidates } from './constants.js';
|
|
3
3
|
import { sandboxWorkspaceViewStatePaths } from './workspace-view.js';
|
|
4
4
|
import { SANDBOX_GIT_COMMON_DIR } from './git-metadata.js';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
function workspaceStateMounts(workspaceViewRoot, taskBound) {
|
|
6
|
+
return sandboxWorkspaceViewStatePaths(workspaceViewRoot)
|
|
7
|
+
// The writable completed-task mount below is nested under this path. Keep
|
|
8
|
+
// the worktree target visible so Docker can mount it without creating a
|
|
9
|
+
// mountpoint inside a read-only parent bind mount.
|
|
10
|
+
.filter(({ state }) => !(taskBound && state === 'completed'))
|
|
11
|
+
.map(({ state, hostPath }) => taskBound && state === 'active'
|
|
9
12
|
? {
|
|
10
13
|
hostPaths: [path.join(hostPath, '.short-ids.json')],
|
|
11
14
|
containerPath: '/workspace/.agents/workspace/active/.short-ids.json',
|
|
@@ -16,17 +19,69 @@ export function sandboxCoreBindMounts(config, branch, overrides) {
|
|
|
16
19
|
containerPath: path.posix.join('/workspace/.agents/workspace', state),
|
|
17
20
|
readOnly: true
|
|
18
21
|
});
|
|
22
|
+
}
|
|
23
|
+
function assertGitMetadataPair(overrides) {
|
|
19
24
|
if (Boolean(overrides.gitCommonDir) !== Boolean(overrides.worktreeGitFile)) {
|
|
20
25
|
throw new Error('SANDBOX_GIT_METADATA_INCOMPLETE');
|
|
21
26
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
}
|
|
28
|
+
function gitCommonMount(gitCommonDir) {
|
|
29
|
+
return gitCommonDir ? [{ hostPaths: [gitCommonDir], containerPath: SANDBOX_GIT_COMMON_DIR, readOnly: false }] : [];
|
|
30
|
+
}
|
|
31
|
+
function worktreeGitFileMount(worktreeGitFile) {
|
|
32
|
+
return worktreeGitFile ? [{ hostPaths: [worktreeGitFile], containerPath: '/workspace/.git', readOnly: true }] : [];
|
|
33
|
+
}
|
|
34
|
+
function taskWorkspaceMounts(taskSources, taskBound, overrides) {
|
|
35
|
+
if (!taskBound)
|
|
36
|
+
return [];
|
|
37
|
+
const taskId = overrides.taskId;
|
|
38
|
+
const mounts = [{
|
|
39
|
+
hostPaths: taskSources,
|
|
40
|
+
containerPath: path.posix.join('/workspace/.agents/workspace/active', taskId),
|
|
41
|
+
readOnly: false
|
|
42
|
+
}];
|
|
43
|
+
if (overrides.completedTaskSource)
|
|
44
|
+
mounts.push({
|
|
45
|
+
hostPaths: [overrides.completedTaskSource],
|
|
46
|
+
containerPath: path.posix.join('/workspace/.agents/workspace/completed', taskId),
|
|
47
|
+
readOnly: false
|
|
48
|
+
});
|
|
49
|
+
return mounts;
|
|
50
|
+
}
|
|
51
|
+
function sharedMounts(config, branch, shellConfigHostDir) {
|
|
52
|
+
return [{
|
|
53
|
+
hostPaths: [shareCommonDir(config)], containerPath: '/share/common', readOnly: false
|
|
54
|
+
}, {
|
|
55
|
+
hostPaths: [shareBranchDir(config, branch)], containerPath: '/share/branch', readOnly: false
|
|
56
|
+
}, {
|
|
57
|
+
hostPaths: shellConfigHostDir ? [shellConfigHostDir] : shellConfigDirCandidates(config, branch),
|
|
58
|
+
containerPath: '/home/devuser/.host-shell-config', readOnly: true
|
|
59
|
+
}];
|
|
60
|
+
}
|
|
61
|
+
function controlMounts(overrides, taskBound) {
|
|
62
|
+
const mounts = [{
|
|
63
|
+
hostPaths: [overrides.controlStatusDir], containerPath: '/run/agent-infra/control-status', readOnly: true
|
|
64
|
+
}, {
|
|
65
|
+
hostPaths: [overrides.controlDir], containerPath: '/run/agent-infra/control', readOnly: false
|
|
66
|
+
}];
|
|
67
|
+
if (taskBound && overrides.runtimeDir)
|
|
68
|
+
mounts.push({
|
|
69
|
+
hostPaths: [overrides.runtimeDir], containerPath: '/run/agent-infra/runtime', readOnly: false
|
|
70
|
+
});
|
|
71
|
+
if (taskBound && overrides.controlRootDir)
|
|
72
|
+
mounts.push({
|
|
73
|
+
hostPaths: [overrides.controlRootDir], containerPath: '/run/agent-infra/control-root', readOnly: false
|
|
74
|
+
});
|
|
75
|
+
return mounts;
|
|
76
|
+
}
|
|
77
|
+
export function sandboxCoreBindMounts(config, branch, overrides) {
|
|
78
|
+
const taskSources = overrides.taskSources ?? [];
|
|
79
|
+
const taskBound = Boolean(taskSources.length > 0 && overrides.taskId);
|
|
80
|
+
assertGitMetadataPair(overrides);
|
|
81
|
+
if (taskBound && !overrides.controlRootDir)
|
|
82
|
+
throw new Error('SANDBOX_CONTROL_ROOT_MISSING');
|
|
28
83
|
const mounts = [
|
|
29
|
-
...(
|
|
84
|
+
...gitCommonMount(overrides.gitCommonDir),
|
|
30
85
|
{
|
|
31
86
|
hostPaths: overrides.worktree
|
|
32
87
|
? [overrides.worktree]
|
|
@@ -34,48 +89,12 @@ export function sandboxCoreBindMounts(config, branch, overrides) {
|
|
|
34
89
|
containerPath: '/workspace',
|
|
35
90
|
readOnly: false
|
|
36
91
|
},
|
|
37
|
-
...(
|
|
38
|
-
...
|
|
92
|
+
...worktreeGitFileMount(overrides.worktreeGitFile),
|
|
93
|
+
...workspaceStateMounts(overrides.workspaceViewRoot, taskBound),
|
|
94
|
+
...taskWorkspaceMounts(taskSources, taskBound, overrides),
|
|
95
|
+
...sharedMounts(config, branch, overrides.shellConfigHostDir),
|
|
96
|
+
...controlMounts(overrides, taskBound)
|
|
39
97
|
];
|
|
40
|
-
if (taskBound) {
|
|
41
|
-
const taskMountPath = path.posix.join('/workspace/.agents/workspace/active', overrides.taskId);
|
|
42
|
-
mounts.push({
|
|
43
|
-
hostPaths: taskSources,
|
|
44
|
-
containerPath: taskMountPath,
|
|
45
|
-
readOnly: false
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
mounts.push({
|
|
49
|
-
hostPaths: [shareCommonDir(config)],
|
|
50
|
-
containerPath: '/share/common',
|
|
51
|
-
readOnly: false
|
|
52
|
-
}, {
|
|
53
|
-
hostPaths: [shareBranchDir(config, branch)],
|
|
54
|
-
containerPath: '/share/branch',
|
|
55
|
-
readOnly: false
|
|
56
|
-
}, {
|
|
57
|
-
hostPaths: overrides.shellConfigHostDir
|
|
58
|
-
? [overrides.shellConfigHostDir]
|
|
59
|
-
: shellConfigDirCandidates(config, branch),
|
|
60
|
-
containerPath: '/home/devuser/.host-shell-config',
|
|
61
|
-
readOnly: true
|
|
62
|
-
});
|
|
63
|
-
mounts.push({
|
|
64
|
-
hostPaths: [overrides.controlStatusDir],
|
|
65
|
-
containerPath: '/run/agent-infra/control-status',
|
|
66
|
-
readOnly: true
|
|
67
|
-
}, {
|
|
68
|
-
hostPaths: [overrides.controlDir],
|
|
69
|
-
containerPath: '/run/agent-infra/control',
|
|
70
|
-
readOnly: false
|
|
71
|
-
});
|
|
72
|
-
if (taskBound && overrides.runtimeDir) {
|
|
73
|
-
mounts.push({
|
|
74
|
-
hostPaths: [overrides.runtimeDir],
|
|
75
|
-
containerPath: '/run/agent-infra/runtime',
|
|
76
|
-
readOnly: false
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
98
|
return mounts;
|
|
80
99
|
}
|
|
81
100
|
//# sourceMappingURL=mounts.js.map
|
|
@@ -9,7 +9,7 @@ import { createSandboxCapabilityPlan } from './agent-client-reconciler.js';
|
|
|
9
9
|
import { toEnginePath } from './engines/wsl2-paths.js';
|
|
10
10
|
import { sandboxCoreBindMounts } from './mounts.js';
|
|
11
11
|
import { resolveSandboxGitMetadata } from './git-metadata.js';
|
|
12
|
-
import { assertSandboxTaskSource, sandboxControlPaths, sandboxWorkspaceViewPaths } from './workspace-view.js';
|
|
12
|
+
import { assertSandboxTaskSource, sandboxCompletedTaskMountSource, sandboxControlPaths, sandboxWorkspaceViewPaths } from './workspace-view.js';
|
|
13
13
|
import { resolveTaskWorkspace } from './task-resolver.js';
|
|
14
14
|
import { parseSandboxWorkspaceIdentity, sameSandboxWorkspaceIdentity } from './workspace-identity.js';
|
|
15
15
|
import { inspectWorktree } from './worktree-safety.js';
|
|
@@ -174,7 +174,6 @@ export async function startSandboxControlBroker(repoRoot, manifestPath) {
|
|
|
174
174
|
async function ensureSandboxControlBroker(params) {
|
|
175
175
|
const control = sandboxControlPaths({
|
|
176
176
|
base: params.config.controlBase ?? path.join(params.config.home, '.agent-infra', 'sandbox-control'),
|
|
177
|
-
repoRoot: params.config.repoRoot,
|
|
178
177
|
project: params.config.project,
|
|
179
178
|
container: params.container,
|
|
180
179
|
identity: params.workspace
|
|
@@ -460,25 +459,38 @@ export function startCompletedSandboxContainer(params) {
|
|
|
460
459
|
const activeRoot = path.join(params.config.repoRoot, '.agents', 'workspace', 'active');
|
|
461
460
|
const historicalSource = path.join(activeRoot, params.taskId);
|
|
462
461
|
fs.mkdirSync(activeRoot, { recursive: true });
|
|
462
|
+
let replacedEmptyRoot = false;
|
|
463
|
+
let emptyRootMode = 0o700;
|
|
463
464
|
try {
|
|
464
|
-
fs.lstatSync(historicalSource);
|
|
465
|
-
|
|
465
|
+
const existing = fs.lstatSync(historicalSource);
|
|
466
|
+
if (existing.isSymbolicLink() || !existing.isDirectory() || fs.readdirSync(historicalSource).length > 0) {
|
|
467
|
+
throw new Error(`SANDBOX_COMPLETED_SOURCE_CONFLICT: historical task source already exists at ${historicalSource}`);
|
|
468
|
+
}
|
|
469
|
+
emptyRootMode = existing.mode & 0o777;
|
|
470
|
+
fs.rmdirSync(historicalSource);
|
|
471
|
+
replacedEmptyRoot = true;
|
|
466
472
|
}
|
|
467
473
|
catch (error) {
|
|
468
474
|
if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT'))
|
|
469
475
|
throw error;
|
|
470
476
|
}
|
|
471
|
-
|
|
477
|
+
let temporaryLink = false;
|
|
472
478
|
try {
|
|
479
|
+
fs.symlinkSync(completedSource, historicalSource, process.platform === 'win32' ? 'junction' : 'dir');
|
|
480
|
+
temporaryLink = true;
|
|
473
481
|
params.start(params.engine, params.container);
|
|
474
482
|
}
|
|
475
483
|
finally {
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
484
|
+
if (temporaryLink) {
|
|
485
|
+
const temporarySource = fs.lstatSync(historicalSource);
|
|
486
|
+
const resolvedSource = fs.realpathSync.native(historicalSource);
|
|
487
|
+
if (!temporarySource.isSymbolicLink() || resolvedSource !== completedSource) {
|
|
488
|
+
throw new Error(`SANDBOX_COMPLETED_SOURCE_CHANGED: refusing to remove unexpected path at ${historicalSource}`);
|
|
489
|
+
}
|
|
490
|
+
fs.unlinkSync(historicalSource);
|
|
480
491
|
}
|
|
481
|
-
|
|
492
|
+
if (replacedEmptyRoot)
|
|
493
|
+
fs.mkdirSync(historicalSource, { mode: emptyRootMode });
|
|
482
494
|
}
|
|
483
495
|
}
|
|
484
496
|
function sameHostSource(left, right) {
|
|
@@ -510,7 +522,6 @@ function expectedMounts(params) {
|
|
|
510
522
|
});
|
|
511
523
|
const control = sandboxControlPaths({
|
|
512
524
|
base: config.controlBase ?? path.join(config.home, '.agent-infra', 'sandbox-control'),
|
|
513
|
-
repoRoot: config.repoRoot,
|
|
514
525
|
project: config.project,
|
|
515
526
|
container: params.container,
|
|
516
527
|
identity: params.workspace
|
|
@@ -519,6 +530,7 @@ function expectedMounts(params) {
|
|
|
519
530
|
? recoveryTaskSources(config.repoRoot, params.workspace.taskId)
|
|
520
531
|
: null;
|
|
521
532
|
const taskId = params.workspace.mode === 'task-bound' ? params.workspace.taskId : null;
|
|
533
|
+
const completedTaskSource = taskId ? sandboxCompletedTaskMountSource(config.repoRoot, taskId) : null;
|
|
522
534
|
const worktrees = worktreeDirCandidates(config, branch).filter((candidate) => fs.existsSync(path.join(candidate, '.git')));
|
|
523
535
|
const gitMetadata = worktrees.length === 1
|
|
524
536
|
? resolveSandboxGitMetadata(worktrees[0], control.channelDir)
|
|
@@ -532,12 +544,14 @@ function expectedMounts(params) {
|
|
|
532
544
|
}),
|
|
533
545
|
workspaceViewRoot: view.root,
|
|
534
546
|
controlDir: control.channelDir,
|
|
547
|
+
...(params.workspace.mode === 'task-bound' ? { controlRootDir: control.root } : {}),
|
|
535
548
|
controlStatusDir: control.statusDir,
|
|
536
549
|
...(taskSources === null
|
|
537
550
|
? {}
|
|
538
551
|
: {
|
|
539
552
|
runtimeDir: control.runtimeDir,
|
|
540
553
|
taskSources: taskSources.accessiblePaths,
|
|
554
|
+
completedTaskSource: completedTaskSource,
|
|
541
555
|
taskId: taskId
|
|
542
556
|
})
|
|
543
557
|
}).map((mount) => ({
|
|
@@ -548,6 +562,10 @@ function expectedMounts(params) {
|
|
|
548
562
|
&& mount.containerPath === `/workspace/.agents/workspace/active/${taskId}`
|
|
549
563
|
? { sourceAccessiblePaths: taskSources.accessiblePaths }
|
|
550
564
|
: {}),
|
|
565
|
+
...(completedTaskSource !== null && taskId !== null
|
|
566
|
+
&& mount.containerPath === `/workspace/.agents/workspace/completed/${taskId}`
|
|
567
|
+
? { sourceAccessiblePaths: [completedTaskSource] }
|
|
568
|
+
: {}),
|
|
551
569
|
expectedRW: !mount.readOnly
|
|
552
570
|
}));
|
|
553
571
|
const live = tools.flatMap((tool) => (tool.hostLiveMounts ?? []).flatMap(({ hostPath, containerSubpath }) => {
|
|
@@ -907,7 +925,7 @@ export async function ensureSandboxReady(params) {
|
|
|
907
925
|
let completedReentry = null;
|
|
908
926
|
if (params.reentry === 'completed' && params.workspace?.mode === 'task-bound') {
|
|
909
927
|
const control = sandboxControlPaths({
|
|
910
|
-
base: params.config.controlBase,
|
|
928
|
+
base: params.config.controlBase, project: params.config.project,
|
|
911
929
|
container: params.row.name, identity: params.workspace
|
|
912
930
|
});
|
|
913
931
|
const manifest = readSandboxControlManifest(control.manifestPath);
|
|
@@ -568,7 +568,7 @@ function resolveRmTarget(config, tools, cleanupTarget, options = {}) {
|
|
|
568
568
|
? matchedContainers
|
|
569
569
|
: [...new Set([...containerNameCandidates(config, effectiveBranch), ...matchedContainers])];
|
|
570
570
|
const controlRoots = containers.flatMap((container) => identities.map((identity) => sandboxControlPaths({
|
|
571
|
-
base: config.controlBase,
|
|
571
|
+
base: config.controlBase, project: config.project, container, identity
|
|
572
572
|
}).root));
|
|
573
573
|
const workspaceViewRoots = containers.flatMap((container) => identities.map((identity) => sandboxWorkspaceViewPaths({
|
|
574
574
|
base: config.workspaceViewBase, project: config.project, container, identity
|
|
@@ -758,11 +758,35 @@ async function rmPurgeCore(config, tools, prompt = {}) {
|
|
|
758
758
|
const confirm = prompt.confirm ?? p.confirm;
|
|
759
759
|
const isCancel = prompt.isCancel ?? p.isCancel;
|
|
760
760
|
p.intro(pc.cyan(`Removing all sandboxes for ${config.project}`));
|
|
761
|
+
const listed = fetchSandboxRows(engine, sandboxLabel(config), sandboxBranchLabel(config), { mode: sandboxWorkspaceModeLabel(config), taskId: sandboxTaskIdLabel(config) });
|
|
762
|
+
const rows = [...listed.running, ...listed.nonRunning];
|
|
761
763
|
const containers = runEngine(engine, 'docker', [
|
|
762
764
|
'ps', '-a', '--filter', `label=${sandboxLabel(config)}`, '--format', '{{.Names}}'
|
|
763
765
|
]).split('\n').filter(Boolean);
|
|
766
|
+
const controlRoots = rows.flatMap((row) => {
|
|
767
|
+
if (!row.branch)
|
|
768
|
+
return [];
|
|
769
|
+
const identity = row.workspaceMode === 'task-bound' && row.taskId
|
|
770
|
+
? { mode: 'task-bound', taskId: row.taskId }
|
|
771
|
+
: row.workspaceMode === 'branch-only'
|
|
772
|
+
? { mode: 'branch-only' }
|
|
773
|
+
: null;
|
|
774
|
+
if (!identity)
|
|
775
|
+
return [];
|
|
776
|
+
return [sandboxControlPaths({
|
|
777
|
+
base: config.controlBase, project: config.project,
|
|
778
|
+
container: row.name, identity
|
|
779
|
+
}).root];
|
|
780
|
+
});
|
|
781
|
+
const completedTaskIds = [...new Set(rows
|
|
782
|
+
.filter((row) => row.workspaceMode === 'task-bound' && row.taskId)
|
|
783
|
+
.map((row) => row.taskId))];
|
|
764
784
|
for (const name of containers)
|
|
765
785
|
runSafeEngine(engine, 'docker', ['rm', '-f', name]);
|
|
786
|
+
if (!sandboxContainersRemoved(engine, containers)) {
|
|
787
|
+
throw new Error(`SANDBOX_REMOVAL_CONTAINER_STILL_PRESENT: ${containers.join(', ')}`);
|
|
788
|
+
}
|
|
789
|
+
let cleanupCompleted = true;
|
|
766
790
|
const worktrees = fs.existsSync(config.worktreeBase)
|
|
767
791
|
? fs.readdirSync(config.worktreeBase).map((entry) => path.join(config.worktreeBase, entry))
|
|
768
792
|
: [];
|
|
@@ -773,6 +797,8 @@ async function rmPurgeCore(config, tools, prompt = {}) {
|
|
|
773
797
|
fs.rmSync(worktree, { recursive: true, force: true });
|
|
774
798
|
runSafe('git', ['-C', config.repoRoot, 'worktree', 'prune']);
|
|
775
799
|
}
|
|
800
|
+
else
|
|
801
|
+
cleanupCompleted = false;
|
|
776
802
|
}
|
|
777
803
|
for (const dir of projectToolDirs(config, tools))
|
|
778
804
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
@@ -780,14 +806,22 @@ async function rmPurgeCore(config, tools, prompt = {}) {
|
|
|
780
806
|
const selected = await confirm({ message: `Remove all shell config dirs in ${config.shellConfigBase}?`, initialValue: true });
|
|
781
807
|
if (!isCancel(selected) && selected)
|
|
782
808
|
fs.rmSync(config.shellConfigBase, { recursive: true, force: true });
|
|
809
|
+
else
|
|
810
|
+
cleanupCompleted = false;
|
|
783
811
|
}
|
|
784
812
|
if (fs.existsSync(config.shareBase)) {
|
|
785
813
|
const selected = await confirm({ message: `Remove all share dirs for project (${config.shareBase})?`, initialValue: true });
|
|
786
814
|
if (!isCancel(selected) && selected)
|
|
787
815
|
fs.rmSync(config.shareBase, { recursive: true, force: true });
|
|
816
|
+
else
|
|
817
|
+
cleanupCompleted = false;
|
|
788
818
|
}
|
|
789
|
-
|
|
790
|
-
|
|
819
|
+
fs.rmSync(path.join(config.workspaceViewBase, config.project), { recursive: true, force: true });
|
|
820
|
+
for (const root of new Set(controlRoots))
|
|
821
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
822
|
+
if (cleanupCompleted) {
|
|
823
|
+
for (const taskId of completedTaskIds)
|
|
824
|
+
releaseStaleShortIdRegistry(config.repoRoot, taskId);
|
|
791
825
|
}
|
|
792
826
|
const removeImage = await confirm({ message: `Remove image ${config.imageName}?`, initialValue: false });
|
|
793
827
|
if (!isCancel(removeImage) && removeImage)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { isRemovedHashShortIdInput } from '../task/short-id.js';
|
|
4
|
+
import { resolveTaskRef } from '../task/resolve-ref.js';
|
|
4
5
|
import { resolveTaskWorkspace } from './task-resolver.js';
|
|
5
6
|
const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
|
|
6
7
|
const SHORT_ID_RE = /^\d+$/;
|
|
@@ -38,6 +39,16 @@ function taskIdentity(repoRoot, taskId, registry) {
|
|
|
38
39
|
}
|
|
39
40
|
return { mode: 'task-bound', taskId, shortId: shortIds[0] };
|
|
40
41
|
}
|
|
42
|
+
function cleanupTaskId(requestedRef, repoRoot) {
|
|
43
|
+
if (TASK_ID_RE.test(requestedRef))
|
|
44
|
+
return requestedRef;
|
|
45
|
+
if (!SHORT_ID_RE.test(requestedRef))
|
|
46
|
+
return null;
|
|
47
|
+
const task = resolveTaskRef(requestedRef, { repoRoot });
|
|
48
|
+
if (!task.ok)
|
|
49
|
+
throw new Error(task.message);
|
|
50
|
+
return task.taskId;
|
|
51
|
+
}
|
|
41
52
|
export function resolveSandboxTarget(requestedRef, repoRoot) {
|
|
42
53
|
if (isRemovedHashShortIdInput(requestedRef)) {
|
|
43
54
|
throw new Error(`Invalid task short id '${requestedRef}': task short ids must use bare digits`);
|
|
@@ -83,26 +94,27 @@ export function resolveSandboxCleanupTarget(requestedRef, repoRoot, options = {}
|
|
|
83
94
|
if (isRemovedHashShortIdInput(requestedRef)) {
|
|
84
95
|
throw new Error(`Invalid task short id '${requestedRef}': task short ids must use bare digits`);
|
|
85
96
|
}
|
|
86
|
-
|
|
97
|
+
const taskId = cleanupTaskId(requestedRef, repoRoot);
|
|
98
|
+
if (taskId) {
|
|
87
99
|
let task;
|
|
88
100
|
try {
|
|
89
|
-
task = resolveTaskWorkspace(
|
|
101
|
+
task = resolveTaskWorkspace(taskId, repoRoot);
|
|
90
102
|
}
|
|
91
103
|
catch (error) {
|
|
92
|
-
if (!(error instanceof Error && error.message === `Task not found: ${
|
|
104
|
+
if (!(error instanceof Error && error.message === `Task not found: ${taskId}`))
|
|
93
105
|
throw error;
|
|
94
|
-
const branch = options.resolveMissingTask?.(
|
|
106
|
+
const branch = options.resolveMissingTask?.(taskId);
|
|
95
107
|
if (!branch)
|
|
96
108
|
throw error;
|
|
97
109
|
return {
|
|
98
110
|
requestedRef,
|
|
99
111
|
branch,
|
|
100
|
-
workspace: { mode: 'task-bound', taskId
|
|
112
|
+
workspace: { mode: 'task-bound', taskId },
|
|
101
113
|
taskState: 'unknown'
|
|
102
114
|
};
|
|
103
115
|
}
|
|
104
116
|
if (!options.allowProtected && (task.state === 'blocked' || task.state === 'archive')) {
|
|
105
|
-
throw new Error(`SANDBOX_CLEANUP_STATE_UNSUPPORTED: task ${
|
|
117
|
+
throw new Error(`SANDBOX_CLEANUP_STATE_UNSUPPORTED: task ${taskId} is ${task.state}`);
|
|
106
118
|
}
|
|
107
119
|
return {
|
|
108
120
|
requestedRef,
|
|
@@ -2,7 +2,6 @@ import { createHash, randomBytes } from 'node:crypto';
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { resolveTaskWorkspace } from './task-resolver.js';
|
|
5
|
-
import { resolveTaskRuntimeRoot } from '../task/runtime-paths.js';
|
|
6
5
|
import { createSandboxControlIdentitySentinel, writeSandboxControlIdentitySentinel } from './control/identity-sentinel.js';
|
|
7
6
|
export const SANDBOX_WORKSPACE_VIEW_STATES = Object.freeze([
|
|
8
7
|
'active',
|
|
@@ -27,7 +26,17 @@ export function sandboxWorkspaceViewPaths(params) {
|
|
|
27
26
|
}
|
|
28
27
|
export function sandboxControlPaths(params) {
|
|
29
28
|
const root = params.identity.mode === 'task-bound'
|
|
30
|
-
?
|
|
29
|
+
? (() => {
|
|
30
|
+
if (!/^TASK-\d{8}-\d{6}$/.test(params.identity.taskId)
|
|
31
|
+
|| !params.project || path.basename(params.project) !== params.project || params.project === '.' || params.project === '..') {
|
|
32
|
+
throw new Error('SANDBOX_CONTROL_ROOT_INVALID');
|
|
33
|
+
}
|
|
34
|
+
const candidate = path.resolve(params.base, params.project, 'tasks', params.identity.taskId);
|
|
35
|
+
const relative = path.relative(path.resolve(params.base), candidate);
|
|
36
|
+
if (relative.startsWith('..') || path.isAbsolute(relative))
|
|
37
|
+
throw new Error('SANDBOX_CONTROL_ROOT_INVALID');
|
|
38
|
+
return candidate;
|
|
39
|
+
})()
|
|
31
40
|
: (() => {
|
|
32
41
|
const digest = createHash('sha256').update('branch-only').digest('hex').slice(0, 16);
|
|
33
42
|
return path.resolve(params.base, params.project, params.container, digest);
|
|
@@ -50,6 +59,22 @@ function assertSafeDirectory(directory, expectedBase) {
|
|
|
50
59
|
throw new Error(`Sandbox workspace view must not be a symbolic link: ${directory}`);
|
|
51
60
|
}
|
|
52
61
|
}
|
|
62
|
+
function assertSafeTaskBoundControlRoot(basePath, root) {
|
|
63
|
+
const relative = path.relative(basePath, root);
|
|
64
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
65
|
+
throw new Error(`Sandbox control root escapes its configured base: ${root}`);
|
|
66
|
+
}
|
|
67
|
+
if (fs.existsSync(basePath) && fs.lstatSync(basePath).isSymbolicLink()) {
|
|
68
|
+
throw new Error(`Sandbox control base must not be a symbolic link: ${basePath}`);
|
|
69
|
+
}
|
|
70
|
+
let current = basePath;
|
|
71
|
+
for (const segment of relative.split(path.sep)) {
|
|
72
|
+
current = path.join(current, segment);
|
|
73
|
+
if (fs.existsSync(current) && fs.lstatSync(current).isSymbolicLink()) {
|
|
74
|
+
throw new Error(`Sandbox control root must not contain a symbolic link: ${current}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
53
78
|
function assertSafeMountTargetPath(target, worktreeRoot, canonicalWorktreeRoot) {
|
|
54
79
|
const relative = path.relative(worktreeRoot, target);
|
|
55
80
|
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
@@ -111,13 +136,16 @@ export function assertSandboxTaskSource(repoRoot, taskId) {
|
|
|
111
136
|
}
|
|
112
137
|
return canonical;
|
|
113
138
|
}
|
|
114
|
-
export function prepareSandboxWorkspaceMountTargets(worktreeRoot) {
|
|
139
|
+
export function prepareSandboxWorkspaceMountTargets(worktreeRoot, taskId) {
|
|
115
140
|
const resolvedWorktreeRoot = path.resolve(worktreeRoot);
|
|
116
141
|
const canonicalWorktreeRoot = fs.realpathSync.native(resolvedWorktreeRoot);
|
|
117
142
|
const workspaceRoot = path.resolve(resolvedWorktreeRoot, '.agents', 'workspace');
|
|
118
143
|
const statePaths = sandboxWorkspaceViewStatePaths(workspaceRoot);
|
|
119
144
|
const registryTarget = path.join(workspaceRoot, 'active', '.short-ids.json');
|
|
120
|
-
|
|
145
|
+
const completedTaskTarget = taskId ? path.join(workspaceRoot, 'completed', taskId) : null;
|
|
146
|
+
if (taskId && !/^TASK-\d{8}-\d{6}$/.test(taskId))
|
|
147
|
+
throw new Error('SANDBOX_TASK_SOURCE_INVALID');
|
|
148
|
+
for (const target of [workspaceRoot, ...statePaths.map(({ hostPath }) => hostPath), registryTarget, ...(completedTaskTarget ? [completedTaskTarget] : [])]) {
|
|
121
149
|
assertSafeMountTargetPath(target, resolvedWorktreeRoot, canonicalWorktreeRoot);
|
|
122
150
|
}
|
|
123
151
|
fs.mkdirSync(workspaceRoot, { recursive: true, mode: 0o700 });
|
|
@@ -125,9 +153,52 @@ export function prepareSandboxWorkspaceMountTargets(worktreeRoot) {
|
|
|
125
153
|
fs.mkdirSync(hostPath, { recursive: true, mode: 0o700 });
|
|
126
154
|
fs.chmodSync(hostPath, 0o700);
|
|
127
155
|
}
|
|
156
|
+
if (completedTaskTarget)
|
|
157
|
+
fs.mkdirSync(completedTaskTarget, { recursive: true, mode: 0o700 });
|
|
128
158
|
fs.chmodSync(workspaceRoot, 0o700);
|
|
129
159
|
fs.closeSync(fs.openSync(registryTarget, 'a', 0o600));
|
|
130
160
|
}
|
|
161
|
+
export function prepareSandboxCompletedTaskMountSource(repoRoot, taskId) {
|
|
162
|
+
if (!/^TASK-\d{8}-\d{6}$/.test(taskId))
|
|
163
|
+
throw new Error('SANDBOX_TASK_SOURCE_INVALID');
|
|
164
|
+
const workspaceRoot = path.resolve(repoRoot, '.agents', 'workspace');
|
|
165
|
+
fs.mkdirSync(path.join(workspaceRoot, 'completed'), { recursive: true, mode: 0o700 });
|
|
166
|
+
const completedRoot = path.join(workspaceRoot, 'completed');
|
|
167
|
+
const completedStat = fs.lstatSync(completedRoot);
|
|
168
|
+
if (!completedStat.isDirectory() || completedStat.isSymbolicLink())
|
|
169
|
+
throw new Error('SANDBOX_TASK_SOURCE_INVALID');
|
|
170
|
+
const target = path.join(completedRoot, taskId);
|
|
171
|
+
try {
|
|
172
|
+
const stat = fs.lstatSync(target);
|
|
173
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || fs.readdirSync(target).length > 0) {
|
|
174
|
+
throw new Error(`SANDBOX_COMPLETED_MOUNT_CONFLICT: ${target}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
if (error.code !== 'ENOENT')
|
|
179
|
+
throw error;
|
|
180
|
+
fs.mkdirSync(target, { mode: 0o700 });
|
|
181
|
+
}
|
|
182
|
+
return sandboxCompletedTaskMountSource(repoRoot, taskId);
|
|
183
|
+
}
|
|
184
|
+
export function sandboxCompletedTaskMountSource(repoRoot, taskId) {
|
|
185
|
+
if (!/^TASK-\d{8}-\d{6}$/.test(taskId))
|
|
186
|
+
throw new Error('SANDBOX_TASK_SOURCE_INVALID');
|
|
187
|
+
const source = path.resolve(repoRoot, '.agents', 'workspace', 'completed', taskId);
|
|
188
|
+
const stat = fs.lstatSync(source);
|
|
189
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
190
|
+
throw new Error('SANDBOX_TASK_SOURCE_INVALID');
|
|
191
|
+
const canonicalWorkspace = fs.realpathSync.native(path.join(repoRoot, '.agents', 'workspace'));
|
|
192
|
+
const canonicalSource = fs.realpathSync.native(source);
|
|
193
|
+
const canonicalRepoRoot = fs.realpathSync.native(repoRoot);
|
|
194
|
+
const workspaceRelative = path.relative(canonicalRepoRoot, canonicalWorkspace);
|
|
195
|
+
const relative = path.relative(canonicalWorkspace, canonicalSource);
|
|
196
|
+
if (workspaceRelative.startsWith('..') || path.isAbsolute(workspaceRelative)
|
|
197
|
+
|| path.dirname(canonicalSource) !== fs.realpathSync.native(path.dirname(source))
|
|
198
|
+
|| relative.startsWith('..') || path.isAbsolute(relative))
|
|
199
|
+
throw new Error('SANDBOX_TASK_SOURCE_INVALID');
|
|
200
|
+
return canonicalSource;
|
|
201
|
+
}
|
|
131
202
|
export function materializeSandboxWorkspaceView(params) {
|
|
132
203
|
const projectRoot = path.resolve(params.base, params.project);
|
|
133
204
|
const { root } = sandboxWorkspaceViewPaths(params);
|
|
@@ -157,7 +228,12 @@ export function materializeSandboxWorkspaceView(params) {
|
|
|
157
228
|
}
|
|
158
229
|
export function materializeSandboxControl(params) {
|
|
159
230
|
const { root, channelDir, statusDir, processingDir, runtimeDir, manifestPath } = sandboxControlPaths(params);
|
|
160
|
-
|
|
231
|
+
if (params.identity.mode === 'task-bound') {
|
|
232
|
+
assertSafeTaskBoundControlRoot(path.resolve(params.base), root);
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
assertSafeDirectory(root, path.resolve(params.base));
|
|
236
|
+
}
|
|
161
237
|
if (fs.existsSync(root)) {
|
|
162
238
|
if (!params.replacementLease || path.resolve(params.replacementLease.root) !== root) {
|
|
163
239
|
throw new Error('SANDBOX_CONTROL_REPLACEMENT_REQUIRED');
|
|
@@ -215,9 +291,16 @@ export function finalizeSandboxControlManifest(setup, identity) {
|
|
|
215
291
|
containerIdentity: { id: identity.id, labels: { ...identity.labels } },
|
|
216
292
|
authorityEvidence: identity.authorityEvidence
|
|
217
293
|
};
|
|
294
|
+
const persistedManifest = {
|
|
295
|
+
...manifest,
|
|
296
|
+
channelDir: 'channel',
|
|
297
|
+
publicStatusDir: 'public',
|
|
298
|
+
processingDir: 'processing',
|
|
299
|
+
runtimeDir: 'runtime'
|
|
300
|
+
};
|
|
218
301
|
const temporary = `${setup.manifestPath}.${process.pid}.${randomBytes(8).toString('hex')}.tmp`;
|
|
219
302
|
try {
|
|
220
|
-
fs.writeFileSync(temporary, `${JSON.stringify(
|
|
303
|
+
fs.writeFileSync(temporary, `${JSON.stringify(persistedManifest)}\n`, { mode: 0o600, flag: 'wx' });
|
|
221
304
|
fs.renameSync(temporary, setup.manifestPath);
|
|
222
305
|
}
|
|
223
306
|
finally {
|
|
@@ -58,9 +58,9 @@ function collectTasks(repoRoot, state) {
|
|
|
58
58
|
const dir = path.join(repoRoot, '.agents', 'workspace', state);
|
|
59
59
|
if (!fs.existsSync(dir))
|
|
60
60
|
return [];
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
const shortIdByTaskId = state === 'active' || state === 'completed'
|
|
62
|
+
? loadShortIdByTaskId(repoRoot)
|
|
63
|
+
: new Map();
|
|
64
64
|
const rows = [];
|
|
65
65
|
for (const entry of fs.readdirSync(dir).sort()) {
|
|
66
66
|
if (!TASK_ID_RE.test(entry))
|