@fitlab-ai/agent-infra 0.9.5 → 0.9.7
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 +8 -3
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +171 -20
- package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +162 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +244 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -2
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +370 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +68 -8
- package/dist/lib/agent-clients/adapters/codex.js +1 -1
- package/dist/lib/defaults.json +10 -2
- package/dist/lib/git/workflow.js +30 -4
- package/dist/lib/internal/codex-lifecycle.js +169 -3
- package/dist/lib/internal/codex-sandbox-controller.js +106 -0
- package/dist/lib/internal/release-workflow.js +108 -11
- package/dist/lib/internal/sandbox-control.js +43 -6
- package/dist/lib/internal/task-create.js +12 -4
- package/dist/lib/internal/task-orchestration.js +40 -7
- package/dist/lib/platform/context.js +6 -4
- package/dist/lib/platform/github-client.js +55 -2
- package/dist/lib/platform/issue-comments.js +2 -1
- 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/release/workflow.js +29 -21
- package/dist/lib/sandbox/commands/create.js +111 -60
- package/dist/lib/sandbox/commands/prune.js +40 -16
- package/dist/lib/sandbox/commands/rm.js +372 -94
- package/dist/lib/sandbox/commands/start.js +4 -3
- package/dist/lib/sandbox/control/client.js +138 -22
- package/dist/lib/sandbox/control/container-identity.js +64 -0
- package/dist/lib/sandbox/control/executor.js +142 -0
- package/dist/lib/sandbox/control/lifecycle.js +557 -0
- package/dist/lib/sandbox/control/protocol.js +69 -26
- package/dist/lib/sandbox/control/server.js +395 -99
- package/dist/lib/sandbox/control/state.js +191 -0
- package/dist/lib/sandbox/control/timing.js +8 -0
- package/dist/lib/sandbox/index.js +2 -2
- package/dist/lib/sandbox/managed-fs.js +14 -2
- package/dist/lib/sandbox/mounts.js +22 -19
- package/dist/lib/sandbox/recovery.js +284 -50
- package/dist/lib/sandbox/workspace-view.js +90 -10
- package/dist/lib/sandbox/worktree-safety.js +196 -0
- package/dist/lib/server/daemon.js +1 -1
- package/dist/lib/server/process-control.js +17 -1
- package/dist/lib/server/process-state.js +57 -20
- package/dist/lib/task/artifact-lifecycle.js +87 -15
- package/dist/lib/task/artifact-receipts.js +128 -0
- package/dist/lib/task/codex-orchestration.js +236 -0
- package/dist/lib/task/commands/status.js +3 -0
- package/dist/lib/task/commands/validate.js +289 -0
- package/dist/lib/task/commit-finalization.js +36 -9
- package/dist/lib/task/create.js +10 -2
- package/dist/lib/task/delegation-receipts.js +109 -2
- package/dist/lib/task/events.js +149 -6
- package/dist/lib/task/finalization-artifacts.js +2 -1
- package/dist/lib/task/implementation-inputs.js +1 -7
- package/dist/lib/task/index.js +9 -0
- package/dist/lib/task/ledger.js +13 -1
- package/dist/lib/task/orchestration.js +365 -15
- 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 +78 -14
- package/dist/lib/task/verification.js +1 -0
- package/dist/lifecycle-build-manifest.json +6 -0
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +202 -18
- package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +227 -0
- package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +321 -0
- package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -3
- package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +26 -0
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +453 -0
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +83 -6
- package/lib/agent-clients/adapters/codex.ts +1 -1
- package/lib/defaults.json +10 -2
- package/lib/git/workflow.ts +25 -3
- package/lib/internal/codex-lifecycle.ts +175 -4
- package/lib/internal/codex-sandbox-controller.ts +109 -0
- package/lib/internal/release-workflow.ts +107 -11
- package/lib/internal/sandbox-control.ts +38 -6
- package/lib/internal/task-create.ts +16 -4
- package/lib/internal/task-orchestration.ts +34 -7
- package/lib/platform/context.ts +8 -5
- package/lib/platform/github-client.ts +61 -3
- package/lib/platform/issue-comments.ts +2 -1
- 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/release/workflow.ts +69 -20
- package/lib/sandbox/commands/create.ts +70 -25
- package/lib/sandbox/commands/prune.ts +53 -15
- package/lib/sandbox/commands/rm.ts +423 -108
- package/lib/sandbox/commands/start.ts +4 -3
- package/lib/sandbox/control/client.ts +152 -28
- package/lib/sandbox/control/container-identity.ts +93 -0
- package/lib/sandbox/control/executor.ts +167 -0
- package/lib/sandbox/control/lifecycle.ts +573 -0
- package/lib/sandbox/control/protocol.ts +121 -68
- package/lib/sandbox/control/server.ts +398 -109
- package/lib/sandbox/control/state.ts +213 -0
- package/lib/sandbox/control/timing.ts +12 -0
- package/lib/sandbox/index.ts +2 -2
- package/lib/sandbox/managed-fs.ts +17 -2
- package/lib/sandbox/mounts.ts +16 -8
- package/lib/sandbox/recovery.ts +335 -65
- package/lib/sandbox/workspace-view.ts +102 -11
- package/lib/sandbox/worktree-safety.ts +234 -0
- package/lib/server/daemon.ts +1 -1
- package/lib/server/process-control.ts +24 -3
- package/lib/server/process-state.ts +82 -27
- package/lib/task/artifact-lifecycle.ts +78 -14
- package/lib/task/artifact-receipts.ts +161 -0
- package/lib/task/codex-orchestration.ts +319 -0
- package/lib/task/commands/status.ts +2 -0
- package/lib/task/commands/validate.ts +282 -0
- package/lib/task/commit-finalization.ts +52 -9
- package/lib/task/create.ts +11 -2
- package/lib/task/delegation-receipts.ts +208 -7
- package/lib/task/events.ts +131 -5
- package/lib/task/finalization-artifacts.ts +2 -1
- package/lib/task/implementation-inputs.ts +1 -5
- package/lib/task/index.ts +8 -0
- package/lib/task/ledger.ts +16 -1
- package/lib/task/orchestration.ts +552 -18
- package/lib/task/sections.ts +6 -2
- package/lib/task/task-execution-lock.ts +11 -9
- package/lib/task/verification-engine.ts +93 -24
- package/lib/task/verification.ts +2 -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 +72 -12
- package/templates/.agents/rules/lifecycle-orchestration.en.md +8 -2
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +8 -2
- package/templates/.agents/rules/no-mid-flow-questions.en.md +1 -0
- package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +1 -0
- 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/post-release/SKILL.en.md +32 -6
- package/templates/.agents/skills/post-release/SKILL.zh-CN.md +32 -6
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +42 -0
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +42 -0
- package/templates/.agents/skills/run-manual-validation/config/verify.en.json +25 -0
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +25 -0
- 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 +69 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +69 -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 -3
- package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +5 -3
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +75 -3
- package/templates/.claude/commands/run-manual-validation.en.md +9 -0
- package/templates/.claude/commands/run-manual-validation.zh-CN.md +9 -0
- package/templates/.codex/hooks.json +8 -8
- package/templates/.opencode/commands/run-manual-validation.en.md +11 -0
- package/templates/.opencode/commands/run-manual-validation.zh-CN.md +11 -0
- package/templates/.agents/workspace/README.en.md +0 -26
- package/templates/.agents/workspace/README.zh-CN.md +0 -26
package/lib/release/workflow.ts
CHANGED
|
@@ -1,32 +1,81 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
type ReleasePhase = 'unprepared' | 'prepared' | 'partially-published' | 'published' | 'post-pending' | 'post-prepared' | 'complete';
|
|
4
|
+
type PostReleaseFacts = {
|
|
5
|
+
commit: string | null;
|
|
6
|
+
isHead: boolean;
|
|
7
|
+
published: boolean;
|
|
8
|
+
branch: string;
|
|
9
|
+
upstream: string | null;
|
|
10
|
+
remoteHead: string | null;
|
|
11
|
+
newVersion: string | null;
|
|
12
|
+
changedPaths: string[];
|
|
13
|
+
demoInputSha256: string | null;
|
|
14
|
+
worktree: string[];
|
|
15
|
+
staged: string[];
|
|
16
|
+
};
|
|
17
|
+
type ReleaseFacts = {
|
|
18
|
+
localTag: boolean;
|
|
19
|
+
localTagAncestor?: boolean;
|
|
20
|
+
localTagConflict?: boolean;
|
|
21
|
+
remoteBranch: boolean;
|
|
22
|
+
remoteTag: boolean;
|
|
23
|
+
githubRelease: boolean | null;
|
|
24
|
+
npm: boolean | null;
|
|
25
|
+
homebrew: boolean | null;
|
|
26
|
+
smoke: 'success' | 'pending' | 'failed' | null;
|
|
27
|
+
post: PostReleaseFacts;
|
|
28
|
+
};
|
|
29
|
+
type PostConfirmation = {
|
|
30
|
+
version: string;
|
|
31
|
+
commit: string;
|
|
32
|
+
branch: string;
|
|
33
|
+
upstream: string | null;
|
|
34
|
+
remoteHead: string | null;
|
|
35
|
+
newVersion: string | null;
|
|
36
|
+
changedPaths: string[];
|
|
37
|
+
demoInputSha256: string | null;
|
|
38
|
+
worktree: string[];
|
|
39
|
+
staged: string[];
|
|
40
|
+
sha256: string;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
function byteSort(values: readonly string[]): string[] {
|
|
44
|
+
return [...values].sort((left, right) => Buffer.from(left).compare(Buffer.from(right)));
|
|
45
|
+
}
|
|
3
46
|
|
|
4
47
|
function deriveReleasePhase(facts: ReleaseFacts): ReleasePhase {
|
|
5
48
|
if (!facts.localTag && !facts.localTagAncestor) return 'unprepared';
|
|
49
|
+
if (facts.post.commit) return facts.post.published ? 'complete' : 'post-prepared';
|
|
6
50
|
if (!facts.remoteBranch && !facts.remoteTag) return 'prepared';
|
|
7
51
|
if (!facts.remoteBranch || !facts.remoteTag) return 'partially-published';
|
|
8
52
|
if (facts.githubRelease !== true || facts.npm !== true || facts.homebrew !== true) return 'published';
|
|
9
|
-
|
|
10
|
-
return 'complete';
|
|
53
|
+
return 'post-pending';
|
|
11
54
|
}
|
|
12
55
|
|
|
13
|
-
function
|
|
14
|
-
|
|
56
|
+
function createPostConfirmation(version: string, post: PostReleaseFacts): PostConfirmation | null {
|
|
57
|
+
if (!post.commit || !post.isHead || post.worktree.length || post.staged.length) return null;
|
|
58
|
+
const canonical = {
|
|
59
|
+
version,
|
|
60
|
+
commit: post.commit,
|
|
61
|
+
branch: post.branch,
|
|
62
|
+
upstream: post.upstream,
|
|
63
|
+
remoteHead: post.remoteHead,
|
|
64
|
+
newVersion: post.newVersion,
|
|
65
|
+
changedPaths: byteSort(post.changedPaths),
|
|
66
|
+
demoInputSha256: post.demoInputSha256,
|
|
67
|
+
worktree: byteSort(post.worktree),
|
|
68
|
+
staged: byteSort(post.staged)
|
|
69
|
+
};
|
|
70
|
+
const sha256 = `sha256:${crypto.createHash('sha256').update(JSON.stringify(canonical), 'utf8').digest('hex')}`;
|
|
71
|
+
return { ...canonical, sha256 };
|
|
15
72
|
}
|
|
16
73
|
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const allowed = input.action === 'prepare' ? before.phase === 'unprepared'
|
|
22
|
-
: input.action === 'publish' ? before.facts.localTag && ['prepared', 'partially-published'].includes(before.phase)
|
|
23
|
-
: ['published', 'post-pending'].includes(before.phase);
|
|
24
|
-
if (!allowed) return { status: before.phase === 'complete' ? 'no-op' as const : 'failed' as const, changed: false, before, snapshot: before, error: before.phase === 'complete' ? null : { code: 'RELEASE_PHASE_INVALID', message: `Cannot ${input.action} from ${before.phase}` } };
|
|
25
|
-
await input[input.action]?.();
|
|
26
|
-
const snapshot = releaseSnapshot(input.version, await input.inspect());
|
|
27
|
-
const progressed = snapshot.phase !== before.phase;
|
|
28
|
-
return { status: progressed ? 'applied' as const : 'blocked' as const, changed: progressed, before, snapshot, error: progressed ? null : { code: 'RELEASE_PROGRESS_PENDING', message: `${input.action} has not reached the next observable phase` } };
|
|
74
|
+
function releaseSnapshot(version: string, facts: ReleaseFacts) {
|
|
75
|
+
const snapshot = { version, tag: `v${version.replace(/^v/, '')}`, phase: deriveReleasePhase(facts), facts };
|
|
76
|
+
const postConfirmation = createPostConfirmation(version, facts.post);
|
|
77
|
+
return postConfirmation ? { ...snapshot, postConfirmation } : snapshot;
|
|
29
78
|
}
|
|
30
79
|
|
|
31
|
-
export { deriveReleasePhase, releaseSnapshot
|
|
32
|
-
export type { ReleaseFacts, ReleasePhase };
|
|
80
|
+
export { createPostConfirmation, deriveReleasePhase, releaseSnapshot };
|
|
81
|
+
export type { PostConfirmation, PostReleaseFacts, ReleaseFacts, ReleasePhase };
|
|
@@ -2,10 +2,9 @@ import fs from 'node:fs';
|
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { createHash } from 'node:crypto';
|
|
5
|
-
import { execFileSync
|
|
5
|
+
import { execFileSync } from 'node:child_process';
|
|
6
6
|
import type { ExecFileSyncOptions, StdioOptions } from 'node:child_process';
|
|
7
7
|
import { parseArgs } from 'node:util';
|
|
8
|
-
import { fileURLToPath } from 'node:url';
|
|
9
8
|
import * as p from '@clack/prompts';
|
|
10
9
|
import pc from 'picocolors';
|
|
11
10
|
import { listAgentClientAdapters } from '../../agent-clients/registry.ts';
|
|
@@ -67,14 +66,17 @@ import type { SandboxTool, TmpfsSeedEntry } from '../tools.ts';
|
|
|
67
66
|
import {
|
|
68
67
|
assertFreshSandboxReady,
|
|
69
68
|
hydrateTmpfsSeedEntries,
|
|
70
|
-
prepareTmpfsMounts
|
|
69
|
+
prepareTmpfsMounts,
|
|
70
|
+
startSandboxControlBroker
|
|
71
71
|
} from '../recovery.ts';
|
|
72
72
|
import { hostJoin, toEnginePath, volumeArg } from '../engines/wsl2-paths.ts';
|
|
73
73
|
import { sandboxCoreBindMounts } from '../mounts.ts';
|
|
74
74
|
import {
|
|
75
75
|
assertSandboxTaskSource,
|
|
76
|
+
finalizeSandboxControlManifest,
|
|
76
77
|
materializeSandboxControl,
|
|
77
|
-
materializeSandboxWorkspaceView
|
|
78
|
+
materializeSandboxWorkspaceView,
|
|
79
|
+
prepareSandboxWorkspaceMountTarget,
|
|
78
80
|
} from '../workspace-view.ts';
|
|
79
81
|
import { clipboardHostDir, CONTAINER_CLIPBOARD_MOUNT } from '../clipboard/paths.ts';
|
|
80
82
|
import { validateSelinuxDisableEnv } from '../engines/selinux.ts';
|
|
@@ -157,12 +159,6 @@ type HostShellConfig = {
|
|
|
157
159
|
mounts: Array<{ hostPath: string; containerPath: string }>;
|
|
158
160
|
};
|
|
159
161
|
|
|
160
|
-
function internalCliPath(): string {
|
|
161
|
-
const directory = path.dirname(fileURLToPath(import.meta.url));
|
|
162
|
-
const extension = path.extname(fileURLToPath(import.meta.url));
|
|
163
|
-
return path.resolve(directory, '..', '..', '..', 'bin', `internal-cli${extension}`);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
162
|
function resolveToolDirs(config: Pick<SandboxCreateConfig, 'project'>, tools: SandboxTool[], branch: string): ResolvedTool[] {
|
|
167
163
|
return tools.map((tool) => {
|
|
168
164
|
const candidates = toolConfigDirCandidates(tool, config.project, branch);
|
|
@@ -1254,9 +1250,13 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1254
1250
|
const requestedDescription = target.workspace.mode === 'task-bound'
|
|
1255
1251
|
? `task-bound:${target.workspace.taskId}`
|
|
1256
1252
|
: target.workspace.mode;
|
|
1253
|
+
const recoveryTarget = target.workspace.mode === 'task-bound'
|
|
1254
|
+
? target.workspace.taskId
|
|
1255
|
+
: branch;
|
|
1257
1256
|
throw new Error(
|
|
1258
1257
|
`SANDBOX_WORKSPACE_IDENTITY_CONFLICT: container '${name}' is ${existingDescription}, `
|
|
1259
|
-
+ `but this request is ${requestedDescription}.
|
|
1258
|
+
+ `but this request is ${requestedDescription}. `
|
|
1259
|
+
+ `Run 'ai sandbox start --recreate ${recoveryTarget}' on the host, then retry.`
|
|
1260
1260
|
);
|
|
1261
1261
|
}
|
|
1262
1262
|
}
|
|
@@ -1342,6 +1342,7 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1342
1342
|
container,
|
|
1343
1343
|
identity: target.workspace
|
|
1344
1344
|
});
|
|
1345
|
+
prepareSandboxWorkspaceMountTarget(worktree);
|
|
1345
1346
|
const control = materializeSandboxControl({
|
|
1346
1347
|
base: effectiveConfig.controlBase,
|
|
1347
1348
|
repoRoot: effectiveConfig.repoRoot,
|
|
@@ -1349,7 +1350,8 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1349
1350
|
project: effectiveConfig.project,
|
|
1350
1351
|
container,
|
|
1351
1352
|
branch,
|
|
1352
|
-
identity: target.workspace
|
|
1353
|
+
identity: target.workspace,
|
|
1354
|
+
engine
|
|
1353
1355
|
});
|
|
1354
1356
|
hostShellConfig = prepareHostShellConfig({
|
|
1355
1357
|
home: effectiveConfig.home,
|
|
@@ -1362,6 +1364,7 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1362
1364
|
shellConfigHostDir: hostShellConfig.hostDir,
|
|
1363
1365
|
workspaceViewRoot: workspaceView.root,
|
|
1364
1366
|
controlDir: control.channelDir,
|
|
1367
|
+
controlStatusDir: control.statusDir,
|
|
1365
1368
|
...(target.workspace.mode === 'task-bound'
|
|
1366
1369
|
? {
|
|
1367
1370
|
taskSource: assertSandboxTaskSource(
|
|
@@ -1421,9 +1424,12 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1421
1424
|
const hostTz = detectHostTimezone();
|
|
1422
1425
|
const tzFlags = hostTz ? ['-e', `TZ=${hostTz}`] : [];
|
|
1423
1426
|
|
|
1424
|
-
|
|
1427
|
+
let createdContainerRef: string | null = null;
|
|
1428
|
+
try {
|
|
1429
|
+
const dockerRunId = runEngineTaskCommand(engine, 'docker', [
|
|
1425
1430
|
'run',
|
|
1426
1431
|
'-d',
|
|
1432
|
+
'--init',
|
|
1427
1433
|
'--name',
|
|
1428
1434
|
container,
|
|
1429
1435
|
'--hostname',
|
|
@@ -1445,7 +1451,11 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1445
1451
|
'-e',
|
|
1446
1452
|
`AGENT_INFRA_CONTROL_TOKEN=${control.token}`,
|
|
1447
1453
|
'-e',
|
|
1454
|
+
`AGENT_INFRA_CONTROL_GENERATION=${control.generation}`,
|
|
1455
|
+
'-e',
|
|
1448
1456
|
'AGENT_INFRA_CONTROL_DIR=/run/agent-infra/control',
|
|
1457
|
+
'-e',
|
|
1458
|
+
'AGENT_INFRA_CONTROL_STATUS_DIR=/run/agent-infra/control-status',
|
|
1449
1459
|
'--label',
|
|
1450
1460
|
`${sandboxRuntimeCapabilityLabel(effectiveConfig)}=${capabilityPlan.runtimeSignature}`,
|
|
1451
1461
|
...coreVolumes,
|
|
@@ -1466,18 +1476,53 @@ export async function create(args: string[]): Promise<void> {
|
|
|
1466
1476
|
'-w',
|
|
1467
1477
|
'/workspace',
|
|
1468
1478
|
effectiveConfig.imageName
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1479
|
+
]);
|
|
1480
|
+
createdContainerRef = dockerRunId.trim();
|
|
1481
|
+
const containerId = runEngineTaskCommand(engine, 'docker', [
|
|
1482
|
+
'inspect', '--format', '{{.Id}}', dockerRunId.trim()
|
|
1483
|
+
]).trim();
|
|
1484
|
+
if (!containerId) throw new Error('SANDBOX_CONTROL_CONTAINER_ID_INVALID');
|
|
1485
|
+
const rawContainerLabels = runEngineTaskCommand(engine, 'docker', [
|
|
1486
|
+
'inspect', '--format', '{{json .Config.Labels}}', containerId
|
|
1487
|
+
]).trim();
|
|
1488
|
+
let inspectedLabels: unknown;
|
|
1489
|
+
try {
|
|
1490
|
+
inspectedLabels = JSON.parse(rawContainerLabels);
|
|
1491
|
+
} catch {
|
|
1492
|
+
throw new Error('SANDBOX_CONTROL_CONTAINER_LABELS_INVALID');
|
|
1493
|
+
}
|
|
1494
|
+
if (!inspectedLabels || typeof inspectedLabels !== 'object' || Array.isArray(inspectedLabels)) {
|
|
1495
|
+
throw new Error('SANDBOX_CONTROL_CONTAINER_LABELS_INVALID');
|
|
1496
|
+
}
|
|
1497
|
+
const expectedLabels: Record<string, string> = {
|
|
1498
|
+
[sandboxLabel(effectiveConfig)]: '',
|
|
1499
|
+
[sandboxBranchLabel(effectiveConfig)]: branch,
|
|
1500
|
+
[sandboxWorkspaceModeLabel(effectiveConfig)]: target.workspace.mode,
|
|
1501
|
+
[sandboxRuntimeCapabilityLabel(effectiveConfig)]: capabilityPlan.runtimeSignature,
|
|
1502
|
+
...(target.workspace.mode === 'task-bound'
|
|
1503
|
+
? { [sandboxTaskIdLabel(effectiveConfig)]: target.workspace.taskId }
|
|
1504
|
+
: {})
|
|
1505
|
+
};
|
|
1506
|
+
const labels = inspectedLabels as Record<string, unknown>;
|
|
1507
|
+
for (const [key, expected] of Object.entries(expectedLabels)) {
|
|
1508
|
+
if (labels[key] !== expected) throw new Error('SANDBOX_CONTROL_CONTAINER_IDENTITY_MISMATCH');
|
|
1509
|
+
}
|
|
1510
|
+
finalizeSandboxControlManifest(control, {
|
|
1511
|
+
engine,
|
|
1512
|
+
id: containerId,
|
|
1513
|
+
labels: expectedLabels
|
|
1514
|
+
});
|
|
1515
|
+
await startSandboxControlBroker(effectiveConfig.repoRoot, control.manifestPath);
|
|
1516
|
+
createdTmpfsSeedPlan = tmpfsSeedPlan;
|
|
1517
|
+
} catch (error) {
|
|
1518
|
+
if (createdContainerRef) {
|
|
1519
|
+
runSafeEngine(engine, 'docker', ['stop', createdContainerRef]);
|
|
1520
|
+
runSafeEngine(engine, 'docker', ['rm', createdContainerRef]);
|
|
1521
|
+
}
|
|
1522
|
+
removeDirRecursive(control.root);
|
|
1523
|
+
removeDirRecursive(workspaceView.root);
|
|
1524
|
+
throw error;
|
|
1525
|
+
}
|
|
1481
1526
|
} finally {
|
|
1482
1527
|
envFile.cleanup();
|
|
1483
1528
|
}
|
|
@@ -6,16 +6,27 @@ import pc from 'picocolors';
|
|
|
6
6
|
import { loadConfig } from '../config.ts';
|
|
7
7
|
import type { SandboxConfig } from '../config.ts';
|
|
8
8
|
import { safeNameCandidates, sandboxBranchLabel, sandboxLabel } from '../constants.ts';
|
|
9
|
-
import { detectEngine } from '../engine.ts';
|
|
9
|
+
import { detectEngine, ENGINES } from '../engine.ts';
|
|
10
10
|
import { hostJoin } from '../engines/wsl2-paths.ts';
|
|
11
11
|
import { removeManagedDir, removeWorktreeDir } from '../managed-fs.ts';
|
|
12
12
|
import { parseLabels } from './ls.ts';
|
|
13
13
|
import { runEngine, runSafe } from '../shell.ts';
|
|
14
14
|
import { createSandboxCapabilityPlan } from '../agent-client-reconciler.ts';
|
|
15
15
|
import type { SandboxTool } from '../tools.ts';
|
|
16
|
+
import { createCleanPermit, formatWorktreeSnapshot, inspectWorktrees } from '../worktree-safety.ts';
|
|
17
|
+
import type { WorktreeRemovalPermit } from '../worktree-safety.ts';
|
|
16
18
|
|
|
17
19
|
const USAGE = `Usage: ai sandbox prune [--dry-run]`;
|
|
18
20
|
|
|
21
|
+
type PruneDependencies = {
|
|
22
|
+
config?: SandboxConfig;
|
|
23
|
+
tools?: SandboxTool[];
|
|
24
|
+
engine?: string;
|
|
25
|
+
labelsOutput?: string;
|
|
26
|
+
confirm?: typeof p.confirm;
|
|
27
|
+
isCancel?: typeof p.isCancel;
|
|
28
|
+
};
|
|
29
|
+
|
|
19
30
|
type OrphanKind = 'shell' | 'worktree' | 'share' | 'tool';
|
|
20
31
|
|
|
21
32
|
export type OrphanGroup = {
|
|
@@ -107,12 +118,19 @@ export function collectOrphanGroups(
|
|
|
107
118
|
return groups;
|
|
108
119
|
}
|
|
109
120
|
|
|
110
|
-
export function removeOrphanGroups(
|
|
121
|
+
export function removeOrphanGroups(
|
|
122
|
+
config: SandboxConfig,
|
|
123
|
+
groups: OrphanGroup[],
|
|
124
|
+
permits: ReadonlyMap<string, WorktreeRemovalPermit>,
|
|
125
|
+
allowRegisteredPathFallback = false
|
|
126
|
+
): boolean {
|
|
111
127
|
let removedWorktrees = false;
|
|
112
128
|
for (const group of groups) {
|
|
113
129
|
for (const dir of group.dirs) {
|
|
114
130
|
if (group.kind === 'worktree') {
|
|
115
|
-
|
|
131
|
+
const permit = permits.get(path.resolve(dir));
|
|
132
|
+
if (!permit) throw new Error(`Missing worktree removal permit: ${dir}`);
|
|
133
|
+
removeWorktreeDir(config.repoRoot, group.base, dir, permit, { allowRegisteredPathFallback });
|
|
116
134
|
removedWorktrees = true;
|
|
117
135
|
} else {
|
|
118
136
|
removeManagedDir(group.base, dir);
|
|
@@ -142,7 +160,7 @@ function writeGroups(groups: OrphanGroup[]): void {
|
|
|
142
160
|
}
|
|
143
161
|
}
|
|
144
162
|
|
|
145
|
-
export async function prune(args: string[]): Promise<void> {
|
|
163
|
+
export async function prune(args: string[], dependencies: PruneDependencies = {}): Promise<void> {
|
|
146
164
|
const { values } = parseArgs({
|
|
147
165
|
args,
|
|
148
166
|
strict: true,
|
|
@@ -157,9 +175,9 @@ export async function prune(args: string[]): Promise<void> {
|
|
|
157
175
|
return;
|
|
158
176
|
}
|
|
159
177
|
|
|
160
|
-
const config = loadConfig();
|
|
161
|
-
const tools = [...createSandboxCapabilityPlan(config).cleanupInventory];
|
|
162
|
-
const engine = detectEngine(config);
|
|
178
|
+
const config = dependencies.config ?? loadConfig();
|
|
179
|
+
const tools = dependencies.tools ?? [...createSandboxCapabilityPlan(config).cleanupInventory];
|
|
180
|
+
const engine = dependencies.engine ?? detectEngine(config);
|
|
163
181
|
const psArgs = [
|
|
164
182
|
'ps',
|
|
165
183
|
'-a',
|
|
@@ -168,14 +186,25 @@ export async function prune(args: string[]): Promise<void> {
|
|
|
168
186
|
'--format',
|
|
169
187
|
'{{.Labels}}'
|
|
170
188
|
];
|
|
171
|
-
let labelsOutput
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
189
|
+
let labelsOutput = dependencies.labelsOutput;
|
|
190
|
+
if (labelsOutput === undefined) {
|
|
191
|
+
try {
|
|
192
|
+
labelsOutput = runEngine(engine, 'docker', psArgs);
|
|
193
|
+
} catch {
|
|
194
|
+
throw new Error('Unable to determine active sandbox branches: docker ps failed');
|
|
195
|
+
}
|
|
176
196
|
}
|
|
197
|
+
const confirm = dependencies.confirm ?? p.confirm;
|
|
198
|
+
const isCancel = dependencies.isCancel ?? p.isCancel;
|
|
177
199
|
const groups = collectOrphanGroups(config, tools, activeBranchesFromLabels(config, labelsOutput));
|
|
178
200
|
const count = orphanCount(groups);
|
|
201
|
+
const worktrees = groups.filter((group) => group.kind === 'worktree').flatMap((group) => group.dirs);
|
|
202
|
+
const inspections = inspectWorktrees(worktrees);
|
|
203
|
+
const blockers = inspections.filter((inspection) => inspection.status !== 'clean');
|
|
204
|
+
const permits = new Map<string, WorktreeRemovalPermit>();
|
|
205
|
+
for (const inspection of inspections) {
|
|
206
|
+
if (inspection.status === 'clean') permits.set(inspection.snapshot.worktree, createCleanPermit(inspection.snapshot));
|
|
207
|
+
}
|
|
179
208
|
|
|
180
209
|
p.intro(pc.cyan(`Pruning orphaned sandbox state for ${config.project}`));
|
|
181
210
|
|
|
@@ -186,23 +215,32 @@ export async function prune(args: string[]): Promise<void> {
|
|
|
186
215
|
}
|
|
187
216
|
|
|
188
217
|
writeGroups(groups);
|
|
218
|
+
for (const blocker of blockers) {
|
|
219
|
+
p.log.error(blocker.status === 'failed'
|
|
220
|
+
? `${JSON.stringify(blocker.worktree)}: ${blocker.message}`
|
|
221
|
+
: formatWorktreeSnapshot(blocker.snapshot));
|
|
222
|
+
}
|
|
189
223
|
|
|
190
224
|
if (values['dry-run']) {
|
|
191
225
|
p.outro(pc.green('Dry run complete'));
|
|
192
226
|
return;
|
|
193
227
|
}
|
|
194
228
|
|
|
195
|
-
|
|
229
|
+
if (blockers.length > 0) {
|
|
230
|
+
throw new Error('Refusing to prune because worktree preflight found blocker(s); no orphan state was deleted');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const shouldRemove = await confirm({
|
|
196
234
|
message: `Remove ${count} orphaned sandbox state dirs?`,
|
|
197
235
|
initialValue: true
|
|
198
236
|
});
|
|
199
237
|
|
|
200
|
-
if (
|
|
238
|
+
if (isCancel(shouldRemove) || !shouldRemove) {
|
|
201
239
|
p.outro('Cancelled');
|
|
202
240
|
return;
|
|
203
241
|
}
|
|
204
242
|
|
|
205
|
-
const removedWorktrees = removeOrphanGroups(config, groups);
|
|
243
|
+
const removedWorktrees = removeOrphanGroups(config, groups, permits, engine === ENGINES.WSL2);
|
|
206
244
|
if (removedWorktrees) {
|
|
207
245
|
runSafe('git', ['-C', config.repoRoot, 'worktree', 'prune']);
|
|
208
246
|
}
|