@fitlab-ai/agent-infra 0.11.14 → 0.12.0
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/bin/cli.ts +1 -1
- package/dist/bin/cli.js +1 -1
- package/dist/bin/internal-cli.js +6 -1
- package/dist/lib/agent-clients/adapters/codex-lifecycle/recovery.js +6 -6
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +8 -18
- package/dist/lib/agent-clients/adapters/codex-orchestration.js +1 -1
- package/dist/lib/decide.js +2 -7
- package/dist/lib/internal/cli-route-inventory.js +3 -0
- package/dist/lib/internal/manual-validation.js +8 -16
- package/dist/lib/internal/platform-comment.js +3 -2
- package/dist/lib/internal/task-activity.js +1 -12
- package/dist/lib/internal/task-event.js +1 -12
- package/dist/lib/internal/task-finalization.js +2 -2
- package/dist/lib/internal/task-ledger.js +1 -12
- package/dist/lib/internal/task-operation-registry.js +2 -1
- package/dist/lib/internal/task-qualification.js +6 -15
- package/dist/lib/internal/task-review.js +3 -16
- package/dist/lib/internal/task-short-id.js +2 -1
- package/dist/lib/internal/task-verify.js +1 -11
- package/dist/lib/internal/task-warning.js +1 -12
- package/dist/lib/internal/task-workspace.js +43 -0
- package/dist/lib/merge.js +4 -3
- package/dist/lib/platform/pr-change-report.js +10 -3
- package/dist/lib/platform/pr-summary.js +6 -15
- package/dist/lib/platform/pull-requests.js +3 -15
- package/dist/lib/pr-review/evidence-grading.js +3 -2
- package/dist/lib/process-data/index.js +18 -6
- package/dist/lib/process-data/sources.js +6 -0
- package/dist/lib/run/index.js +3 -2
- package/dist/lib/sandbox/commands/create.js +5 -1
- package/dist/lib/sandbox/commands/enter.js +1 -1
- package/dist/lib/sandbox/commands/list-running.js +2 -1
- package/dist/lib/sandbox/control/client.js +8 -13
- package/dist/lib/sandbox/control/completed-reentry.js +2 -1
- package/dist/lib/sandbox/control/lifecycle.js +2 -21
- package/dist/lib/sandbox/control/task-view.js +4 -2
- package/dist/lib/sandbox/mounts.js +4 -4
- package/dist/lib/sandbox/recovery.js +26 -16
- package/dist/lib/sandbox/removal.js +3 -10
- package/dist/lib/sandbox/task-resolver.js +9 -6
- package/dist/lib/sandbox/workspace-identity.js +36 -20
- package/dist/lib/sandbox/workspace-view.js +29 -22
- package/dist/lib/task/activity-intent.js +2 -12
- package/dist/lib/task/artifact-lifecycle.js +17 -11
- package/dist/lib/task/artifact-operations.js +41 -49
- package/dist/lib/task/capabilities.js +2 -2
- package/dist/lib/task/commands/ls.js +4 -3
- package/dist/lib/task/commands/status.js +1 -1
- package/dist/lib/task/commit-operation.js +6 -6
- package/dist/lib/task/control-authority.js +32 -32
- package/dist/lib/task/create-service.js +2 -4
- package/dist/lib/task/create.js +12 -65
- package/dist/lib/task/delegation-receipts.js +1 -1
- package/dist/lib/task/delivery.js +2 -8
- package/dist/lib/task/events.js +5 -31
- package/dist/lib/task/finalization.js +17 -22
- package/dist/lib/task/lifecycle.js +38 -49
- package/dist/lib/task/local-artifact-finalization.js +1 -2
- package/dist/lib/task/orchestration.js +24 -57
- package/dist/lib/task/platform-operation-recovery.js +1 -2
- package/dist/lib/task/resolve-ref.js +17 -12
- package/dist/lib/task/review-finalization.js +2 -35
- package/dist/lib/task/review-identity.js +3 -1
- package/dist/lib/task/short-id.js +12 -10
- package/dist/lib/task/verification-engine.js +19 -58
- package/dist/lib/task/workspace-root.js +59 -0
- package/dist/lib/task/workspace-snapshot.js +4 -3
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapters/codex-lifecycle/recovery.ts +6 -6
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +8 -34
- package/lib/agent-clients/adapters/codex-orchestration.ts +1 -1
- package/lib/decide.ts +2 -7
- package/lib/internal/cli-route-inventory.ts +2 -0
- package/lib/internal/manual-validation.ts +8 -19
- package/lib/internal/platform-comment.ts +3 -2
- package/lib/internal/task-activity.ts +1 -10
- package/lib/internal/task-event.ts +1 -10
- package/lib/internal/task-finalization.ts +2 -2
- package/lib/internal/task-ledger.ts +1 -10
- package/lib/internal/task-operation-registry.ts +2 -1
- package/lib/internal/task-qualification.ts +5 -11
- package/lib/internal/task-review.ts +3 -14
- package/lib/internal/task-short-id.ts +2 -1
- package/lib/internal/task-verify.ts +1 -9
- package/lib/internal/task-warning.ts +1 -10
- package/lib/internal/task-workspace.ts +39 -0
- package/lib/merge.ts +4 -3
- package/lib/platform/pr-change-report.ts +9 -3
- package/lib/platform/pr-summary.ts +7 -16
- package/lib/platform/pull-requests.ts +3 -24
- package/lib/pr-review/evidence-grading.ts +3 -2
- package/lib/process-data/index.ts +17 -5
- package/lib/process-data/sources.ts +6 -0
- package/lib/run/index.ts +3 -2
- package/lib/sandbox/commands/create.ts +5 -1
- package/lib/sandbox/commands/enter.ts +1 -1
- package/lib/sandbox/commands/list-running.ts +2 -1
- package/lib/sandbox/control/client.ts +7 -12
- package/lib/sandbox/control/completed-reentry.ts +2 -1
- package/lib/sandbox/control/lifecycle.ts +4 -21
- package/lib/sandbox/control/task-view.ts +4 -2
- package/lib/sandbox/mounts.ts +4 -4
- package/lib/sandbox/recovery.ts +26 -18
- package/lib/sandbox/removal.ts +3 -26
- package/lib/sandbox/task-resolver.ts +9 -6
- package/lib/sandbox/workspace-identity.ts +42 -18
- package/lib/sandbox/workspace-view.ts +28 -20
- package/lib/task/activity-intent.ts +3 -17
- package/lib/task/artifact-lifecycle.ts +39 -11
- package/lib/task/artifact-operations.ts +28 -35
- package/lib/task/capabilities.ts +2 -2
- package/lib/task/commands/ls.ts +4 -3
- package/lib/task/commands/status.ts +1 -1
- package/lib/task/commit-operation.ts +5 -6
- package/lib/task/control-authority.ts +29 -35
- package/lib/task/create-service.ts +2 -4
- package/lib/task/create.ts +10 -51
- package/lib/task/delegation-receipts.ts +1 -1
- package/lib/task/delivery.ts +2 -10
- package/lib/task/events.ts +5 -32
- package/lib/task/finalization.ts +16 -24
- package/lib/task/lifecycle.ts +40 -51
- package/lib/task/local-artifact-finalization.ts +1 -4
- package/lib/task/orchestration.ts +31 -80
- package/lib/task/platform-operation-recovery.ts +1 -2
- package/lib/task/resolve-ref.ts +18 -12
- package/lib/task/review-finalization.ts +2 -52
- package/lib/task/review-identity.ts +3 -1
- package/lib/task/short-id.ts +15 -10
- package/lib/task/verification-engine.ts +20 -62
- package/lib/task/workspace-root.ts +78 -0
- package/lib/task/workspace-snapshot.ts +4 -3
- package/package.json +1 -1
- package/templates/.agents/QUICKSTART.en.md +19 -8
- package/templates/.agents/QUICKSTART.zh-CN.md +19 -8
- package/templates/.agents/README.en.md +10 -15
- package/templates/.agents/README.zh-CN.md +9 -13
- package/templates/.agents/rules/issue-sync.en.md +1 -1
- package/templates/.agents/rules/issue-sync.zh-CN.md +1 -1
- package/templates/.agents/rules/lifecycle-orchestration.en.md +1 -1
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +1 -1
- package/templates/.agents/rules/next-step-output.en.md +2 -2
- package/templates/.agents/rules/next-step-output.zh-CN.md +2 -2
- package/templates/.agents/rules/pr-sync.en.md +1 -1
- package/templates/.agents/rules/pr-sync.zh-CN.md +1 -1
- package/templates/.agents/rules/release-commands.en.md +3 -0
- package/templates/.agents/rules/release-commands.zh-CN.md +3 -0
- package/templates/.agents/rules/task-management.en.md +0 -2
- package/templates/.agents/rules/task-management.zh-CN.md +0 -2
- package/templates/.agents/rules/task-short-id.en.md +2 -2
- package/templates/.agents/rules/task-short-id.zh-CN.md +2 -2
- package/templates/.agents/scripts/task-short-id.js +2 -8
- package/templates/.agents/skills/analyze-task/SKILL.en.md +6 -6
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +6 -6
- package/templates/.agents/skills/analyze-task/config/verify.en.json +0 -1
- package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +0 -1
- package/templates/.agents/skills/archive-tasks/SKILL.en.md +5 -5
- package/templates/.agents/skills/archive-tasks/SKILL.zh-CN.md +5 -5
- package/templates/.agents/skills/archive-tasks/scripts/archive-tasks.sh +10 -2
- package/templates/.agents/skills/block-task/SKILL.en.md +4 -4
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +5 -5
- package/templates/.agents/skills/block-task/config/verify.json +0 -1
- package/templates/.agents/skills/cancel-task/SKILL.en.md +6 -6
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +6 -6
- package/templates/.agents/skills/cancel-task/config/verify.json +0 -1
- package/templates/.agents/skills/code-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/code-task/config/verify.en.json +0 -1
- package/templates/.agents/skills/code-task/config/verify.zh-CN.json +0 -1
- package/templates/.agents/skills/code-task/reference/output-template.en.md +1 -1
- package/templates/.agents/skills/code-task/reference/output-template.zh-CN.md +1 -1
- package/templates/.agents/skills/commit/SKILL.en.md +1 -1
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/commit/config/verify.json +0 -1
- package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +2 -2
- package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +2 -2
- package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +1 -1
- package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +1 -1
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +2 -2
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +0 -1
- package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +0 -1
- package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +1 -1
- package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +1 -1
- package/templates/.agents/skills/complete-task/SKILL.en.md +17 -9
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +18 -10
- package/templates/.agents/skills/complete-task/config/verify.en.json +0 -1
- package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +0 -1
- package/templates/.agents/skills/create-pr/SKILL.en.md +3 -3
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/create-pr/config/verify.json +0 -1
- package/templates/.agents/skills/create-pr/reference/change-report.en.md +13 -3
- package/templates/.agents/skills/create-pr/reference/change-report.zh-CN.md +13 -3
- package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +1 -1
- package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +1 -1
- package/templates/.agents/skills/create-release-note/SKILL.en.md +2 -2
- package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/create-task/SKILL.en.md +4 -4
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/create-task/config/verify.json +0 -1
- package/templates/.agents/skills/import-codescan/SKILL.en.md +1 -1
- package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/import-codescan/config/verify.json +0 -1
- package/templates/.agents/skills/import-dependabot/SKILL.en.md +3 -3
- package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/import-dependabot/config/verify.json +0 -1
- package/templates/.agents/skills/import-issue/SKILL.en.md +5 -5
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +5 -5
- package/templates/.agents/skills/import-issue/config/verify.json +0 -1
- package/templates/.agents/skills/plan-task/SKILL.en.md +5 -5
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +5 -5
- package/templates/.agents/skills/plan-task/config/verify.en.json +0 -1
- package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +0 -1
- package/templates/.agents/skills/restore-task/SKILL.en.md +2 -2
- package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/restore-task/config/verify.json +0 -1
- package/templates/.agents/skills/review-analysis/SKILL.en.md +1 -1
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/review-analysis/config/verify.en.json +0 -1
- package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +0 -1
- package/templates/.agents/skills/review-analysis/reference/output-templates.en.md +5 -5
- package/templates/.agents/skills/review-analysis/reference/output-templates.zh-CN.md +5 -5
- package/templates/.agents/skills/review-code/SKILL.en.md +2 -2
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/review-code/reference/output-templates.en.md +4 -4
- package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +4 -4
- package/templates/.agents/skills/review-plan/SKILL.en.md +1 -1
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/review-plan/config/verify.en.json +0 -1
- package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +0 -1
- package/templates/.agents/skills/review-plan/reference/output-templates.en.md +5 -5
- package/templates/.agents/skills/review-plan/reference/output-templates.zh-CN.md +5 -5
- package/templates/.agents/skills/review-pr/SKILL.en.md +2 -2
- package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/review-pr/config/verify.en.json +0 -1
- package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +0 -1
- package/templates/.agents/skills/review-pr/reference/output-guidance.en.md +1 -1
- package/templates/.agents/skills/review-pr/reference/output-guidance.zh-CN.md +1 -1
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +5 -5
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +5 -5
- package/templates/.agents/skills/run-manual-validation/config/verify.en.json +0 -1
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +0 -1
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +13 -2
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +13 -2
- package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -0
- package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/test/SKILL.en.md +0 -3
- package/templates/.agents/skills/test/SKILL.zh-CN.md +0 -3
- package/templates/.agents/skills/test-integration/SKILL.en.md +0 -3
- package/templates/.agents/skills/test-integration/SKILL.zh-CN.md +0 -3
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +13 -6
- package/templates/.agents/skills/upgrade-dependency/SKILL.en.md +0 -3
- package/templates/.agents/skills/upgrade-dependency/SKILL.zh-CN.md +0 -3
- package/templates/.agents/skills/watch-pr/SKILL.en.md +21 -4
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +22 -5
- package/templates/.agents/skills/watch-pr/config/verify.json +0 -1
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +14 -0
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +15 -1
- package/templates/.agents/templates/handoff.en.md +1 -1
- package/templates/.agents/templates/handoff.zh-CN.md +1 -1
- package/templates/.agents/workflows/bug-fix.en.yaml +1 -1
- package/templates/.agents/workflows/feature-development.en.yaml +1 -1
- package/templates/.agents/workflows/refactoring.en.yaml +1 -1
- package/dist/lib/runtime/agent-runtime.js +0 -33
- package/dist/lib/task/runtime-paths.js +0 -10
- package/dist/lib/task/task-execution-lock.js +0 -250
- package/lib/runtime/agent-runtime.ts +0 -41
- package/lib/task/runtime-paths.ts +0 -12
- package/lib/task/task-execution-lock.ts +0 -320
|
@@ -5,6 +5,7 @@ import { runSafeEngine, runVerboseEngine } from '../shell.ts';
|
|
|
5
5
|
import { resolveTaskBranch } from '../task-resolver.ts';
|
|
6
6
|
import { isRemovedHashShortIdInput } from '../../task/short-id.ts';
|
|
7
7
|
import { parseSandboxWorkspaceIdentity } from '../workspace-identity.ts';
|
|
8
|
+
import { resolveProjectTaskWorkspaceRoot } from '../../task/workspace-root.ts';
|
|
8
9
|
|
|
9
10
|
export type SandboxRow = {
|
|
10
11
|
name: string;
|
|
@@ -143,7 +144,7 @@ function tryResolveFromRegistry(arg: string, repoRoot: string): RegistryLookup {
|
|
|
143
144
|
);
|
|
144
145
|
}
|
|
145
146
|
for (const sub of ['active', 'completed', 'blocked', 'archive']) {
|
|
146
|
-
const taskMdPath = path.join(repoRoot,
|
|
147
|
+
const taskMdPath = path.join(resolveProjectTaskWorkspaceRoot(repoRoot), sub, taskId, 'task.md');
|
|
147
148
|
if (!fs.existsSync(taskMdPath)) continue;
|
|
148
149
|
const content = fs.readFileSync(taskMdPath, 'utf8');
|
|
149
150
|
const fm = content.match(/^branch:\s*(.+)$/m);
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { resolveProjectTaskWorkspaceRoot } from '../../task/workspace-root.ts';
|
|
5
|
+
import { detectRepoRoot } from '../../task/resolve-ref.ts';
|
|
4
6
|
import {
|
|
5
7
|
SANDBOX_CONTROL_MAX_BYTES,
|
|
6
8
|
SANDBOX_CONTROL_STATUS_STALE_MS,
|
|
@@ -158,18 +160,11 @@ type CanonicalRequestTask =
|
|
|
158
160
|
|
|
159
161
|
// Use the current short-id contract for the first visible active registry.
|
|
160
162
|
export function resolveVisibleActiveShortId(ref: string): string | null {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
return resolved.ok ? resolved.taskId : null;
|
|
167
|
-
}
|
|
168
|
-
const parent = path.dirname(dir);
|
|
169
|
-
if (parent === dir) break;
|
|
170
|
-
dir = parent;
|
|
171
|
-
}
|
|
172
|
-
return null;
|
|
163
|
+
const repoRoot = detectRepoRoot();
|
|
164
|
+
const registryPath = path.join(resolveProjectTaskWorkspaceRoot(repoRoot), 'active', '.short-ids.json');
|
|
165
|
+
if (!fs.existsSync(registryPath)) return null;
|
|
166
|
+
const resolved = resolveShortIdReadOnly(ref, repoRoot, { shortIdLength: configuredShortIdLength(repoRoot) });
|
|
167
|
+
return resolved.ok ? resolved.taskId : null;
|
|
173
168
|
}
|
|
174
169
|
|
|
175
170
|
// A request's leading argument is only a task reference for some families and
|
|
@@ -4,6 +4,7 @@ import { resolveTaskWorkspace } from '../task-resolver.ts';
|
|
|
4
4
|
import { assertGitWorktreeBinding } from '../../git/worktree-identity.ts';
|
|
5
5
|
import { parseTypedTaskFrontmatter } from '../../task/frontmatter.ts';
|
|
6
6
|
import { readTaskFinalizationReceipt } from '../../task/finalization.ts';
|
|
7
|
+
import { resolveProjectTaskWorkspaceRoot } from '../../task/workspace-root.ts';
|
|
7
8
|
import { inspectSandboxControlContainer } from './container-identity.ts';
|
|
8
9
|
import type { SandboxControlManifest } from './protocol.ts';
|
|
9
10
|
import { atomicWriteJson, readSandboxControlStatus } from './state.ts';
|
|
@@ -53,7 +54,7 @@ export async function prepareCompletedReentry(
|
|
|
53
54
|
|| previous.receipt.generation !== view.receipt.generation
|
|
54
55
|
|| previous.receipt.requestId !== view.receipt.requestId
|
|
55
56
|
|| previous.receipt.revision > view.receipt.revision) throw invalid();
|
|
56
|
-
const completedRoot = fs.realpathSync.native(path.join(manifest.repoRoot, '
|
|
57
|
+
const completedRoot = fs.realpathSync.native(path.join(resolveProjectTaskWorkspaceRoot(manifest.repoRoot), 'completed'));
|
|
57
58
|
const source = fs.realpathSync.native(path.join(completedRoot, manifest.taskId));
|
|
58
59
|
if (source !== path.join(completedRoot, manifest.taskId)) throw invalid();
|
|
59
60
|
const stat = fs.statSync(source, { bigint: true });
|
|
@@ -661,28 +661,11 @@ function readTaskCompletionManifest(
|
|
|
661
661
|
return manifest;
|
|
662
662
|
}
|
|
663
663
|
|
|
664
|
-
export
|
|
664
|
+
export function validateSandboxControlTaskIdentity(
|
|
665
665
|
root: string,
|
|
666
|
-
binding: Readonly<{ taskId: string; generation: string; controlRootId: string; statusDir: string }
|
|
667
|
-
|
|
668
|
-
)
|
|
669
|
-
const resolvedRoot = path.resolve(root);
|
|
670
|
-
const manifest = readTaskCompletionManifest(resolvedRoot, binding);
|
|
671
|
-
markSandboxControlRootQuiescing(resolvedRoot);
|
|
672
|
-
const deadline = Date.now() + timeoutMs;
|
|
673
|
-
const startupPath = path.join(resolvedRoot, BROKER_STARTING_FILE);
|
|
674
|
-
const brokerPath = path.join(resolvedRoot, 'broker.json');
|
|
675
|
-
while (true) {
|
|
676
|
-
const startup = readStartupOwner(startupPath);
|
|
677
|
-
if (fs.existsSync(startupPath) && !startup) throw new Error('SANDBOX_CONTROL_BROKER_START_TRANSITION');
|
|
678
|
-
const broker = readBrokerOwner(brokerPath);
|
|
679
|
-
if (broker && (broker.token !== manifest.token || broker.generation !== manifest.generation)) {
|
|
680
|
-
throw new Error('SANDBOX_CONTROL_OWNER_MISMATCH');
|
|
681
|
-
}
|
|
682
|
-
if (!startup && !broker) return;
|
|
683
|
-
if (Date.now() >= deadline) throw new Error('SANDBOX_CONTROL_BROKER_DRAIN_TIMEOUT');
|
|
684
|
-
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
685
|
-
}
|
|
666
|
+
binding: Readonly<{ taskId: string; generation: string; controlRootId: string; statusDir: string }>
|
|
667
|
+
): void {
|
|
668
|
+
readTaskCompletionManifest(path.resolve(root), binding);
|
|
686
669
|
}
|
|
687
670
|
|
|
688
671
|
function recordSandboxRemovalPending(
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { resolveProjectTaskWorkspaceRoot } from '../../task/workspace-root.ts';
|
|
3
4
|
|
|
4
5
|
export const SANDBOX_TASK_VIEW_STATES = ['not-applicable', 'current', 'finalized-stale', 'unknown'] as const;
|
|
5
6
|
export const SANDBOX_TASK_VIEW_SOURCES = ['active', 'completed', 'unknown'] as const;
|
|
@@ -119,8 +120,9 @@ export function taskViewForManifest(params: Readonly<{
|
|
|
119
120
|
if (params.mode !== 'task-bound' || !params.taskId) {
|
|
120
121
|
return { state: 'not-applicable', taskId: null, observedSource: null, receipt: null, reasonCode: null };
|
|
121
122
|
}
|
|
122
|
-
const
|
|
123
|
-
const
|
|
123
|
+
const taskRoot = resolveProjectTaskWorkspaceRoot(params.repoRoot);
|
|
124
|
+
const active = path.join(taskRoot, 'active', params.taskId, 'task.md');
|
|
125
|
+
const completed = path.join(taskRoot, 'completed', params.taskId, 'task.md');
|
|
124
126
|
const activeExists = fs.existsSync(active);
|
|
125
127
|
const completedExists = fs.existsSync(completed);
|
|
126
128
|
if (activeExists === completedExists) {
|
package/lib/sandbox/mounts.ts
CHANGED
|
@@ -24,12 +24,12 @@ function workspaceStateMounts(workspaceViewRoot: string, taskBound: boolean): Sa
|
|
|
24
24
|
.map(({ state, hostPath }) => taskBound && state === 'active'
|
|
25
25
|
? {
|
|
26
26
|
hostPaths: [path.join(hostPath, '.short-ids.json')],
|
|
27
|
-
containerPath: '/workspace
|
|
27
|
+
containerPath: '/task-workspace/active/.short-ids.json',
|
|
28
28
|
readOnly: true
|
|
29
29
|
}
|
|
30
30
|
: {
|
|
31
31
|
hostPaths: [hostPath],
|
|
32
|
-
containerPath: path.posix.join('/workspace
|
|
32
|
+
containerPath: path.posix.join('/task-workspace', state),
|
|
33
33
|
readOnly: true
|
|
34
34
|
}
|
|
35
35
|
);
|
|
@@ -60,12 +60,12 @@ function taskWorkspaceMounts(taskSources: string[], taskBound: boolean, override
|
|
|
60
60
|
const taskId = overrides.taskId!;
|
|
61
61
|
const mounts: SandboxBindMountDeclaration[] = [{
|
|
62
62
|
hostPaths: taskSources,
|
|
63
|
-
containerPath: path.posix.join('/workspace
|
|
63
|
+
containerPath: path.posix.join('/task-workspace/active', taskId),
|
|
64
64
|
readOnly: false
|
|
65
65
|
}];
|
|
66
66
|
if (overrides.completedTaskSource) mounts.push({
|
|
67
67
|
hostPaths: [overrides.completedTaskSource],
|
|
68
|
-
containerPath: path.posix.join('/workspace
|
|
68
|
+
containerPath: path.posix.join('/task-workspace/completed', taskId),
|
|
69
69
|
readOnly: false
|
|
70
70
|
});
|
|
71
71
|
return mounts;
|
package/lib/sandbox/recovery.ts
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
sandboxWorkspaceViewPaths
|
|
31
31
|
} from './workspace-view.ts';
|
|
32
32
|
import { resolveTaskWorkspace } from './task-resolver.ts';
|
|
33
|
+
import { resolveProjectTaskWorkspaceRoot } from '../task/workspace-root.ts';
|
|
33
34
|
import {
|
|
34
35
|
parseSandboxWorkspaceIdentity,
|
|
35
36
|
sameSandboxWorkspaceIdentity,
|
|
@@ -450,11 +451,13 @@ export function classifySandboxRecovery(snapshot: SandboxRecoverySnapshot): Sand
|
|
|
450
451
|
});
|
|
451
452
|
}
|
|
452
453
|
if (snapshot.workspaceTopology === 'legacy-parent') {
|
|
454
|
+
const legacyRoot = snapshot.mounts.some((mount) => mount.path === '/task-workspace')
|
|
455
|
+
? '/task-workspace' : '/workspace/.agents/workspace';
|
|
453
456
|
findings.push({
|
|
454
457
|
repairKind: 'hard-failure',
|
|
455
458
|
code: 'SANDBOX_WORKSPACE_TOPOLOGY_MISMATCH',
|
|
456
459
|
message: 'Sandbox uses the legacy workspace parent-mount topology; recreate the container explicitly to migrate it.',
|
|
457
|
-
path:
|
|
460
|
+
path: legacyRoot
|
|
458
461
|
});
|
|
459
462
|
}
|
|
460
463
|
if (snapshot.taskView && !snapshot.taskView.readable) {
|
|
@@ -476,7 +479,8 @@ export function classifySandboxRecovery(snapshot: SandboxRecoverySnapshot): Sand
|
|
|
476
479
|
for (const mount of snapshot.mounts) {
|
|
477
480
|
if (
|
|
478
481
|
snapshot.workspaceTopology === 'legacy-parent'
|
|
479
|
-
&& mount.path.startsWith('/workspace/.agents/workspace')
|
|
482
|
+
&& (mount.path.startsWith('/workspace/.agents/workspace')
|
|
483
|
+
|| mount.path.startsWith('/task-workspace'))
|
|
480
484
|
) continue;
|
|
481
485
|
if (
|
|
482
486
|
mount.actualType !== mount.expectedType
|
|
@@ -571,13 +575,14 @@ function inspectContainer(
|
|
|
571
575
|
}
|
|
572
576
|
|
|
573
577
|
function workspaceTopology(mountsByDestination: ReadonlyMap<string, DockerMount>): SandboxRecoverySnapshot['workspaceTopology'] {
|
|
574
|
-
if (mountsByDestination.has('/workspace/.agents/workspace')
|
|
578
|
+
if (mountsByDestination.has('/workspace/.agents/workspace')
|
|
579
|
+
|| mountsByDestination.has('/task-workspace')) return 'legacy-parent';
|
|
575
580
|
const perState = [
|
|
576
|
-
'/workspace
|
|
577
|
-
'/workspace
|
|
578
|
-
'/workspace
|
|
579
|
-
'/workspace
|
|
580
|
-
'/workspace
|
|
581
|
+
'/task-workspace/active',
|
|
582
|
+
'/task-workspace/active/.short-ids.json',
|
|
583
|
+
'/task-workspace/completed',
|
|
584
|
+
'/task-workspace/blocked',
|
|
585
|
+
'/task-workspace/archive'
|
|
581
586
|
];
|
|
582
587
|
return perState.some((destination) => mountsByDestination.has(destination))
|
|
583
588
|
? 'per-state'
|
|
@@ -618,8 +623,9 @@ function recoveryTaskSources(repoRoot: string, taskId: string): {
|
|
|
618
623
|
mountPaths: string[];
|
|
619
624
|
accessiblePaths: string[];
|
|
620
625
|
} {
|
|
621
|
-
const
|
|
622
|
-
const
|
|
626
|
+
const taskRoot = resolveProjectTaskWorkspaceRoot(repoRoot);
|
|
627
|
+
const activePath = path.join(taskRoot, 'active', taskId);
|
|
628
|
+
const completedPath = path.join(taskRoot, 'completed', taskId);
|
|
623
629
|
if (fs.existsSync(activePath) && !fs.existsSync(completedPath)) {
|
|
624
630
|
const source = assertSandboxTaskSource(repoRoot, taskId);
|
|
625
631
|
return { mountPaths: [source], accessiblePaths: [source] };
|
|
@@ -635,7 +641,7 @@ function recoveryTaskSources(repoRoot: string, taskId: string): {
|
|
|
635
641
|
}
|
|
636
642
|
|
|
637
643
|
const completedRoot = fs.realpathSync.native(
|
|
638
|
-
path.join(
|
|
644
|
+
path.join(taskRoot, 'completed')
|
|
639
645
|
);
|
|
640
646
|
const completedSource = fs.realpathSync.native(path.dirname(task.taskMd));
|
|
641
647
|
if (path.dirname(completedSource) !== completedRoot) {
|
|
@@ -643,9 +649,7 @@ function recoveryTaskSources(repoRoot: string, taskId: string): {
|
|
|
643
649
|
}
|
|
644
650
|
|
|
645
651
|
const historicalActivePath = path.join(
|
|
646
|
-
|
|
647
|
-
'.agents',
|
|
648
|
-
'workspace',
|
|
652
|
+
taskRoot,
|
|
649
653
|
'active',
|
|
650
654
|
taskId
|
|
651
655
|
);
|
|
@@ -670,7 +674,7 @@ export function startCompletedSandboxContainer(params: {
|
|
|
670
674
|
}): void {
|
|
671
675
|
const taskSources = recoveryTaskSources(params.config.repoRoot, params.taskId);
|
|
672
676
|
const completedSource = taskSources.accessiblePaths[0]!;
|
|
673
|
-
const activeRoot = path.join(params.config.repoRoot, '
|
|
677
|
+
const activeRoot = path.join(resolveProjectTaskWorkspaceRoot(params.config.repoRoot), 'active');
|
|
674
678
|
const historicalSource = path.join(activeRoot, params.taskId);
|
|
675
679
|
fs.mkdirSync(activeRoot, { recursive: true });
|
|
676
680
|
let replacedEmptyRoot = false;
|
|
@@ -791,11 +795,11 @@ function expectedMounts(params: {
|
|
|
791
795
|
expectedType: 'bind' as const,
|
|
792
796
|
hostPaths: mount.hostPaths,
|
|
793
797
|
...(taskSources !== null && taskId !== null
|
|
794
|
-
&& mount.containerPath === `/workspace
|
|
798
|
+
&& mount.containerPath === `/task-workspace/active/${taskId}`
|
|
795
799
|
? { sourceAccessiblePaths: taskSources!.accessiblePaths }
|
|
796
800
|
: {}),
|
|
797
801
|
...(completedTaskSource !== null && taskId !== null
|
|
798
|
-
&& mount.containerPath === `/workspace
|
|
802
|
+
&& mount.containerPath === `/task-workspace/completed/${taskId}`
|
|
799
803
|
? { sourceAccessiblePaths: [completedTaskSource] }
|
|
800
804
|
: {}),
|
|
801
805
|
expectedRW: !mount.readOnly
|
|
@@ -929,7 +933,7 @@ export function collectSandboxRecoverySnapshot(params: {
|
|
|
929
933
|
const tools = [...capabilityPlan.tools];
|
|
930
934
|
const topology = workspaceTopology(mountsByDestination);
|
|
931
935
|
const taskViewPath = workspace.mode === 'task-bound'
|
|
932
|
-
? `/workspace
|
|
936
|
+
? `/task-workspace/active/${workspace.taskId}/task.md`
|
|
933
937
|
: null;
|
|
934
938
|
const seeds = declaredTmpfsSeedEntries(tools).map((seed) => {
|
|
935
939
|
const mounted = mountsByDestination.get(seed.stagingPath)?.Type === 'bind';
|
|
@@ -1215,6 +1219,10 @@ function canRetryFreshReadiness(findings: SandboxRecoveryFinding[]): boolean {
|
|
|
1215
1219
|
&& finding.path !== undefined
|
|
1216
1220
|
&& (
|
|
1217
1221
|
finding.path.startsWith('/workspace/.agents/workspace')
|
|
1222
|
+
|| finding.path.startsWith('/task-workspace/active')
|
|
1223
|
+
|| finding.path.startsWith('/task-workspace/blocked')
|
|
1224
|
+
|| finding.path.startsWith('/task-workspace/completed')
|
|
1225
|
+
|| finding.path.startsWith('/task-workspace/archive')
|
|
1218
1226
|
|| finding.path === '/home/devuser/.host-shell-config'
|
|
1219
1227
|
|| finding.path === '/home/devuser/.bash_aliases'
|
|
1220
1228
|
|| finding.path === '/run/agent-infra/runtime'
|
package/lib/sandbox/removal.ts
CHANGED
|
@@ -48,7 +48,6 @@ import { toolConfigDirCandidates, toolProjectDirCandidates } from './tools.ts';
|
|
|
48
48
|
import type { SandboxTool } from './tools.ts';
|
|
49
49
|
import { getProcessStartTime } from '../server/process-state.ts';
|
|
50
50
|
import { releaseStaleShortIdRegistry } from '../task/short-id.ts';
|
|
51
|
-
import { withRepositoryMutationLock } from '../task/task-execution-lock.ts';
|
|
52
51
|
import { fetchSandboxRows, type SandboxRow } from './commands/list-running.ts';
|
|
53
52
|
import { createSandboxControlBindingEvidence } from './task-cleanup.ts';
|
|
54
53
|
import {
|
|
@@ -933,7 +932,7 @@ async function authorizeWorktrees(
|
|
|
933
932
|
return permits;
|
|
934
933
|
}
|
|
935
934
|
|
|
936
|
-
async function
|
|
935
|
+
async function runRmOne(
|
|
937
936
|
config: SandboxConfig,
|
|
938
937
|
tools: SandboxTool[],
|
|
939
938
|
branch: string,
|
|
@@ -953,10 +952,7 @@ async function rmOne(
|
|
|
953
952
|
branch: string,
|
|
954
953
|
options: RmOneOptions = {}
|
|
955
954
|
): Promise<void> {
|
|
956
|
-
await
|
|
957
|
-
config.repoRoot,
|
|
958
|
-
() => runRmOneUnderRepositoryLock(config, tools, branch, options)
|
|
959
|
-
);
|
|
955
|
+
await runRmOne(config, tools, branch, options);
|
|
960
956
|
}
|
|
961
957
|
|
|
962
958
|
async function removeUncheckedSandbox(
|
|
@@ -1060,14 +1056,6 @@ async function rmPurge(
|
|
|
1060
1056
|
config: SandboxConfig,
|
|
1061
1057
|
tools: SandboxTool[],
|
|
1062
1058
|
prompt: PromptDependencies = {}
|
|
1063
|
-
): Promise<void> {
|
|
1064
|
-
return withRepositoryMutationLock(config.repoRoot, () => rmPurgeCore(config, tools, prompt));
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
async function rmPurgeCore(
|
|
1068
|
-
config: SandboxConfig,
|
|
1069
|
-
tools: SandboxTool[],
|
|
1070
|
-
prompt: PromptDependencies = {}
|
|
1071
1059
|
): Promise<void> {
|
|
1072
1060
|
const engine = detectEngine(config);
|
|
1073
1061
|
const confirm = prompt.confirm ?? p.confirm;
|
|
@@ -1142,17 +1130,6 @@ async function rmUnbound(
|
|
|
1142
1130
|
config: SandboxConfig,
|
|
1143
1131
|
tools: SandboxTool[],
|
|
1144
1132
|
options: { dryRun: boolean; assumeYes: boolean }
|
|
1145
|
-
): Promise<void> {
|
|
1146
|
-
return withRepositoryMutationLock(
|
|
1147
|
-
config.repoRoot,
|
|
1148
|
-
() => rmUnboundCore(config, tools, options)
|
|
1149
|
-
);
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
|
-
async function rmUnboundCore(
|
|
1153
|
-
config: SandboxConfig,
|
|
1154
|
-
tools: SandboxTool[],
|
|
1155
|
-
options: { dryRun: boolean; assumeYes: boolean }
|
|
1156
1133
|
): Promise<void> {
|
|
1157
1134
|
const engine = detectEngine(config);
|
|
1158
1135
|
const listed = fetchSandboxRows(
|
|
@@ -1215,7 +1192,7 @@ async function rmUnboundCore(
|
|
|
1215
1192
|
return;
|
|
1216
1193
|
}
|
|
1217
1194
|
for (const { row, cleanupTarget, target } of targets) {
|
|
1218
|
-
await
|
|
1195
|
+
await runRmOne(config, tools, cleanupTarget.branch, {
|
|
1219
1196
|
assumeYes: options.assumeYes,
|
|
1220
1197
|
target,
|
|
1221
1198
|
cleanupTarget
|
|
@@ -2,6 +2,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { isRemovedHashShortIdInput } from '../task/short-id.ts';
|
|
5
|
+
import { resolveProjectTaskWorkspaceRoot } from '../task/workspace-root.ts';
|
|
5
6
|
|
|
6
7
|
const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
|
|
7
8
|
const SHORT_ID_RE = /^\d+$/;
|
|
@@ -35,10 +36,11 @@ function stripQuotes(value: string): string {
|
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
function readTaskContent(repoRoot: string, taskId: string): { content: string; state: TaskWorkspaceState; taskMd: string } {
|
|
39
|
+
const taskRoot = resolveProjectTaskWorkspaceRoot(repoRoot);
|
|
38
40
|
for (const state of TASK_WORKSPACE_STATES) {
|
|
39
41
|
const taskPath = state === 'archive'
|
|
40
|
-
? path.join(
|
|
41
|
-
: path.join(
|
|
42
|
+
? path.join(taskRoot, state, ...findArchiveTaskParts(repoRoot, taskId), 'local', 'task.md')
|
|
43
|
+
: path.join(taskRoot, state, taskId, 'task.md');
|
|
42
44
|
if (fs.existsSync(taskPath)) {
|
|
43
45
|
return { content: fs.readFileSync(taskPath, 'utf8'), state, taskMd: taskPath };
|
|
44
46
|
}
|
|
@@ -47,8 +49,9 @@ function readTaskContent(repoRoot: string, taskId: string): { content: string; s
|
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
function taskWorkspaceCandidates(repoRoot: string, taskId: string): TaskWorkspace[] {
|
|
52
|
+
const taskRoot = resolveProjectTaskWorkspaceRoot(repoRoot);
|
|
50
53
|
const hot = TASK_WORKSPACE_STATES.filter((state) => state !== 'archive').flatMap((state) => {
|
|
51
|
-
const taskMd = path.join(
|
|
54
|
+
const taskMd = path.join(taskRoot, state, taskId, 'task.md');
|
|
52
55
|
if (!fs.existsSync(taskMd)) return [];
|
|
53
56
|
const content = fs.readFileSync(taskMd, 'utf8');
|
|
54
57
|
return [{
|
|
@@ -60,7 +63,7 @@ function taskWorkspaceCandidates(repoRoot: string, taskId: string): TaskWorkspac
|
|
|
60
63
|
});
|
|
61
64
|
const parts = findArchiveTaskParts(repoRoot, taskId);
|
|
62
65
|
if (parts.length === 0) return hot;
|
|
63
|
-
const taskMd = path.join(
|
|
66
|
+
const taskMd = path.join(taskRoot, 'archive', ...parts, 'local', 'task.md');
|
|
64
67
|
if (!fs.existsSync(taskMd)) return hot;
|
|
65
68
|
const content = fs.readFileSync(taskMd, 'utf8');
|
|
66
69
|
return [...hot, {
|
|
@@ -72,7 +75,7 @@ function taskWorkspaceCandidates(repoRoot: string, taskId: string): TaskWorkspac
|
|
|
72
75
|
}
|
|
73
76
|
|
|
74
77
|
function findArchiveTaskParts(repoRoot: string, taskId: string): string[] {
|
|
75
|
-
const archiveRoot = path.join(repoRoot, '
|
|
78
|
+
const archiveRoot = path.join(resolveProjectTaskWorkspaceRoot(repoRoot), 'archive');
|
|
76
79
|
if (!fs.existsSync(archiveRoot)) return [];
|
|
77
80
|
for (const year of fs.readdirSync(archiveRoot).sort().reverse()) {
|
|
78
81
|
if (!/^\d{4}$/.test(year)) continue;
|
|
@@ -129,7 +132,7 @@ export function resolveTaskWorkspace(taskId: string, repoRoot: string): TaskWork
|
|
|
129
132
|
}
|
|
130
133
|
|
|
131
134
|
export function listTaskWorkspaces(repoRoot: string, state: TaskWorkspaceState): TaskWorkspace[] {
|
|
132
|
-
const stateRoot = path.join(repoRoot,
|
|
135
|
+
const stateRoot = path.join(resolveProjectTaskWorkspaceRoot(repoRoot), state);
|
|
133
136
|
if (state === 'archive') {
|
|
134
137
|
if (!fs.existsSync(stateRoot)) return [];
|
|
135
138
|
const rows: TaskWorkspace[] = [];
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { isRemovedHashShortIdInput } from '../task/short-id.ts';
|
|
4
|
-
import { resolveTaskRef } from '../task/resolve-ref.ts';
|
|
5
3
|
import {
|
|
4
|
+
configuredShortIdLength,
|
|
5
|
+
inspectShortIdRegistry,
|
|
6
|
+
isRemovedHashShortIdInput,
|
|
7
|
+
normalizeShortIdInput
|
|
8
|
+
} from '../task/short-id.ts';
|
|
9
|
+
import { resolveProjectTaskWorkspaceRoot } from '../task/workspace-root.ts';
|
|
10
|
+
import {
|
|
11
|
+
TASK_WORKSPACE_STATES,
|
|
12
|
+
listTaskWorkspaces,
|
|
6
13
|
resolveTaskWorkspace,
|
|
7
14
|
type TaskWorkspaceState
|
|
8
15
|
} from './task-resolver.ts';
|
|
@@ -51,7 +58,7 @@ function taskBranch(taskMd: string, taskId: string): string {
|
|
|
51
58
|
}
|
|
52
59
|
|
|
53
60
|
function activeRegistry(repoRoot: string): Record<string, string> {
|
|
54
|
-
const registryPath = path.join(repoRoot, '
|
|
61
|
+
const registryPath = path.join(resolveProjectTaskWorkspaceRoot(repoRoot), 'active', '.short-ids.json');
|
|
55
62
|
if (!fs.existsSync(registryPath)) return {};
|
|
56
63
|
const value = JSON.parse(fs.readFileSync(registryPath, 'utf8')) as unknown;
|
|
57
64
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
@@ -68,7 +75,7 @@ function activeRegistry(repoRoot: string): Record<string, string> {
|
|
|
68
75
|
}
|
|
69
76
|
|
|
70
77
|
function taskIdentity(repoRoot: string, taskId: string, registry: Record<string, string>): SandboxWorkspaceIdentity {
|
|
71
|
-
const taskMd = path.join(repoRoot, '
|
|
78
|
+
const taskMd = path.join(resolveProjectTaskWorkspaceRoot(repoRoot), 'active', taskId, 'task.md');
|
|
72
79
|
if (!fs.existsSync(taskMd)) throw new Error(`Active task not found: ${taskId}`);
|
|
73
80
|
const shortIds = Object.entries(registry).filter(([, id]) => id === taskId).map(([shortId]) => shortId);
|
|
74
81
|
if (shortIds.length !== 1) {
|
|
@@ -79,10 +86,29 @@ function taskIdentity(repoRoot: string, taskId: string, registry: Record<string,
|
|
|
79
86
|
|
|
80
87
|
function cleanupTaskId(requestedRef: string, repoRoot: string): string | null {
|
|
81
88
|
if (TASK_ID_RE.test(requestedRef)) return requestedRef;
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
if (
|
|
85
|
-
return
|
|
89
|
+
const shortIdLength = configuredShortIdLength(repoRoot);
|
|
90
|
+
const normalized = normalizeShortIdInput(requestedRef, { shortIdLength });
|
|
91
|
+
if (normalized.kind === 'error') throw new Error(normalized.message);
|
|
92
|
+
if (normalized.kind !== 'shortId') return null;
|
|
93
|
+
|
|
94
|
+
const registry = inspectShortIdRegistry(repoRoot, shortIdLength);
|
|
95
|
+
if (registry.status !== 'valid') throw new Error(registry.error.message);
|
|
96
|
+
const match = [...registry.shortIds].find(([, shortId]) => shortId === normalized.value);
|
|
97
|
+
if (!match) {
|
|
98
|
+
throw new Error(`Short id '${normalized.value}' not found in active task registry`);
|
|
99
|
+
}
|
|
100
|
+
return match[0];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function cleanupTaskIdByBranch(requestedRef: string, repoRoot: string): string | null {
|
|
104
|
+
const taskIds = new Set(TASK_WORKSPACE_STATES
|
|
105
|
+
.flatMap((state) => listTaskWorkspaces(repoRoot, state))
|
|
106
|
+
.filter((task) => task.branch === requestedRef)
|
|
107
|
+
.map((task) => task.taskId));
|
|
108
|
+
if (taskIds.size > 1) {
|
|
109
|
+
throw new Error(`SANDBOX_TASK_IDENTITY_AMBIGUOUS: branch '${requestedRef}' is bound to multiple tasks`);
|
|
110
|
+
}
|
|
111
|
+
return taskIds.values().next().value ?? null;
|
|
86
112
|
}
|
|
87
113
|
|
|
88
114
|
export function resolveSandboxTarget(requestedRef: string, repoRoot: string): SandboxTarget {
|
|
@@ -101,14 +127,14 @@ export function resolveSandboxTarget(requestedRef: string, repoRoot: string): Sa
|
|
|
101
127
|
const taskId = canonicalShortId ? registry[canonicalShortId] : undefined;
|
|
102
128
|
if (!taskId) throw new Error(`Short id '${requestedRef}' not found in active task registry`);
|
|
103
129
|
const workspace = taskIdentity(repoRoot, taskId, registry);
|
|
104
|
-
return { requestedRef, branch: taskBranch(path.join(repoRoot, '
|
|
130
|
+
return { requestedRef, branch: taskBranch(path.join(resolveProjectTaskWorkspaceRoot(repoRoot), 'active', taskId, 'task.md'), taskId), workspace };
|
|
105
131
|
}
|
|
106
132
|
if (TASK_ID_RE.test(requestedRef)) {
|
|
107
133
|
const workspace = taskIdentity(repoRoot, requestedRef, registry);
|
|
108
|
-
return { requestedRef, branch: taskBranch(path.join(repoRoot, '
|
|
134
|
+
return { requestedRef, branch: taskBranch(path.join(resolveProjectTaskWorkspaceRoot(repoRoot), 'active', requestedRef, 'task.md'), requestedRef), workspace };
|
|
109
135
|
}
|
|
110
136
|
|
|
111
|
-
const activeRoot = path.join(repoRoot, '
|
|
137
|
+
const activeRoot = path.join(resolveProjectTaskWorkspaceRoot(repoRoot), 'active');
|
|
112
138
|
const matches = fs.existsSync(activeRoot)
|
|
113
139
|
? fs.readdirSync(activeRoot, { withFileTypes: true }).flatMap((entry) => {
|
|
114
140
|
if (!entry.isDirectory() || !TASK_ID_RE.test(entry.name)) return [];
|
|
@@ -139,7 +165,8 @@ export function resolveSandboxCleanupTarget(
|
|
|
139
165
|
throw new Error(`Invalid task short id '${requestedRef}': task short ids must use bare digits`);
|
|
140
166
|
}
|
|
141
167
|
|
|
142
|
-
const taskId = cleanupTaskId(requestedRef, repoRoot)
|
|
168
|
+
const taskId = cleanupTaskId(requestedRef, repoRoot)
|
|
169
|
+
?? cleanupTaskIdByBranch(requestedRef, repoRoot);
|
|
143
170
|
if (taskId) {
|
|
144
171
|
let task: ReturnType<typeof resolveTaskWorkspace>;
|
|
145
172
|
try {
|
|
@@ -166,14 +193,11 @@ export function resolveSandboxCleanupTarget(
|
|
|
166
193
|
};
|
|
167
194
|
}
|
|
168
195
|
|
|
169
|
-
const target = resolveSandboxTarget(requestedRef, repoRoot);
|
|
170
196
|
return {
|
|
171
197
|
requestedRef,
|
|
172
|
-
branch:
|
|
173
|
-
workspace:
|
|
174
|
-
|
|
175
|
-
: { mode: 'branch-only' },
|
|
176
|
-
taskState: target.workspace.mode === 'task-bound' ? 'active' : 'branch-only'
|
|
198
|
+
branch: requestedRef,
|
|
199
|
+
workspace: { mode: 'branch-only' },
|
|
200
|
+
taskState: 'branch-only'
|
|
177
201
|
};
|
|
178
202
|
}
|
|
179
203
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createHash, randomBytes } from 'node:crypto';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
3
4
|
import path from 'node:path';
|
|
5
|
+
import { resolveProjectTaskWorkspaceRoot } from '../task/workspace-root.ts';
|
|
4
6
|
import type { SandboxWorkspaceIdentity, SandboxWorkspaceKey } from './workspace-identity.ts';
|
|
5
7
|
import { resolveTaskWorkspace } from './task-resolver.ts';
|
|
6
8
|
import type { SandboxControlManifest } from './control/protocol.ts';
|
|
@@ -147,7 +149,8 @@ function assertSafeMountTargetPath(
|
|
|
147
149
|
|
|
148
150
|
export function assertSandboxTaskSource(repoRoot: string, taskId: string): string {
|
|
149
151
|
if (!/^TASK-\d{8}-\d{6}$/.test(taskId)) throw new Error('SANDBOX_TASK_SOURCE_INVALID');
|
|
150
|
-
const
|
|
152
|
+
const workspaceRootPath = resolveProjectTaskWorkspaceRoot(repoRoot);
|
|
153
|
+
const activeSource = path.join(workspaceRootPath, 'active', taskId);
|
|
151
154
|
try {
|
|
152
155
|
const stat = fs.lstatSync(activeSource);
|
|
153
156
|
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
@@ -155,7 +158,7 @@ export function assertSandboxTaskSource(repoRoot: string, taskId: string): strin
|
|
|
155
158
|
}
|
|
156
159
|
const canonical = fs.realpathSync.native(activeSource);
|
|
157
160
|
const activeRoot = fs.realpathSync.native(path.dirname(activeSource));
|
|
158
|
-
const workspaceRoot = fs.realpathSync.native(
|
|
161
|
+
const workspaceRoot = fs.realpathSync.native(workspaceRootPath);
|
|
159
162
|
const relative = path.relative(workspaceRoot, canonical);
|
|
160
163
|
if (path.dirname(canonical) !== activeRoot || relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
161
164
|
throw new Error(`SANDBOX_TASK_SOURCE_INVALID: ${activeSource} escapes the task workspace`);
|
|
@@ -173,7 +176,7 @@ export function assertSandboxTaskSource(repoRoot: string, taskId: string): strin
|
|
|
173
176
|
throw new Error(`SANDBOX_TASK_SOURCE_INVALID: ${source} must be a real directory`);
|
|
174
177
|
}
|
|
175
178
|
const canonical = fs.realpathSync.native(source);
|
|
176
|
-
const workspaceRoot = fs.realpathSync.native(
|
|
179
|
+
const workspaceRoot = fs.realpathSync.native(workspaceRootPath);
|
|
177
180
|
const relative = path.relative(workspaceRoot, canonical);
|
|
178
181
|
if (path.basename(canonical) !== taskId || relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
179
182
|
throw new Error(`SANDBOX_TASK_SOURCE_INVALID: ${source} escapes the task workspace`);
|
|
@@ -181,30 +184,38 @@ export function assertSandboxTaskSource(repoRoot: string, taskId: string): strin
|
|
|
181
184
|
return canonical;
|
|
182
185
|
}
|
|
183
186
|
|
|
184
|
-
export function prepareSandboxWorkspaceMountTargets(
|
|
185
|
-
const
|
|
186
|
-
const
|
|
187
|
-
const workspaceRoot = path.resolve(
|
|
187
|
+
export function prepareSandboxWorkspaceMountTargets(repoRoot: string): void {
|
|
188
|
+
const resolvedHome = path.resolve(process.env.HOME || os.homedir());
|
|
189
|
+
const canonicalHome = fs.realpathSync.native(resolvedHome);
|
|
190
|
+
const workspaceRoot = path.resolve(resolveProjectTaskWorkspaceRoot(repoRoot));
|
|
191
|
+
const relativeRoot = path.relative(resolvedHome, workspaceRoot);
|
|
192
|
+
if (relativeRoot.startsWith('..') || path.isAbsolute(relativeRoot)) {
|
|
193
|
+
throw new Error(`Sandbox workspace mount target escapes the host home: ${workspaceRoot}`);
|
|
194
|
+
}
|
|
188
195
|
const statePaths = sandboxWorkspaceViewStatePaths(workspaceRoot);
|
|
189
196
|
const registryTarget = path.join(workspaceRoot, 'active', '.short-ids.json');
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
for (const target of [workspaceRoot, ...statePaths.map(({ hostPath }) => hostPath), registryTarget, ...(completedTaskTarget ? [completedTaskTarget] : [])]) {
|
|
193
|
-
assertSafeMountTargetPath(target, resolvedWorktreeRoot, canonicalWorktreeRoot);
|
|
197
|
+
for (const target of [workspaceRoot, ...statePaths.map(({ hostPath }) => hostPath), registryTarget]) {
|
|
198
|
+
assertSafeMountTargetPath(target, resolvedHome, canonicalHome);
|
|
194
199
|
}
|
|
195
200
|
fs.mkdirSync(workspaceRoot, { recursive: true, mode: 0o700 });
|
|
196
201
|
for (const { hostPath } of statePaths) {
|
|
197
202
|
fs.mkdirSync(hostPath, { recursive: true, mode: 0o700 });
|
|
198
203
|
fs.chmodSync(hostPath, 0o700);
|
|
199
204
|
}
|
|
200
|
-
if (completedTaskTarget) fs.mkdirSync(completedTaskTarget, { recursive: true, mode: 0o700 });
|
|
201
205
|
fs.chmodSync(workspaceRoot, 0o700);
|
|
202
|
-
fs.
|
|
206
|
+
const registryFd = fs.openSync(registryTarget, 'a', 0o600);
|
|
207
|
+
try {
|
|
208
|
+
if (fs.fstatSync(registryFd).size === 0) {
|
|
209
|
+
fs.writeFileSync(registryFd, `${JSON.stringify({ version: 1, ids: {} })}\n`);
|
|
210
|
+
}
|
|
211
|
+
} finally {
|
|
212
|
+
fs.closeSync(registryFd);
|
|
213
|
+
}
|
|
203
214
|
}
|
|
204
215
|
|
|
205
216
|
export function prepareSandboxCompletedTaskMountSource(repoRoot: string, taskId: string): string {
|
|
206
217
|
if (!/^TASK-\d{8}-\d{6}$/.test(taskId)) throw new Error('SANDBOX_TASK_SOURCE_INVALID');
|
|
207
|
-
const workspaceRoot =
|
|
218
|
+
const workspaceRoot = resolveProjectTaskWorkspaceRoot(repoRoot);
|
|
208
219
|
fs.mkdirSync(path.join(workspaceRoot, 'completed'), { recursive: true, mode: 0o700 });
|
|
209
220
|
const completedRoot = path.join(workspaceRoot, 'completed');
|
|
210
221
|
const completedStat = fs.lstatSync(completedRoot);
|
|
@@ -224,16 +235,13 @@ export function prepareSandboxCompletedTaskMountSource(repoRoot: string, taskId:
|
|
|
224
235
|
|
|
225
236
|
export function sandboxCompletedTaskMountSource(repoRoot: string, taskId: string): string {
|
|
226
237
|
if (!/^TASK-\d{8}-\d{6}$/.test(taskId)) throw new Error('SANDBOX_TASK_SOURCE_INVALID');
|
|
227
|
-
const source = path.resolve(repoRoot, '
|
|
238
|
+
const source = path.resolve(resolveProjectTaskWorkspaceRoot(repoRoot), 'completed', taskId);
|
|
228
239
|
const stat = fs.lstatSync(source);
|
|
229
240
|
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error('SANDBOX_TASK_SOURCE_INVALID');
|
|
230
|
-
const canonicalWorkspace = fs.realpathSync.native(
|
|
241
|
+
const canonicalWorkspace = fs.realpathSync.native(resolveProjectTaskWorkspaceRoot(repoRoot));
|
|
231
242
|
const canonicalSource = fs.realpathSync.native(source);
|
|
232
|
-
const canonicalRepoRoot = fs.realpathSync.native(repoRoot);
|
|
233
|
-
const workspaceRelative = path.relative(canonicalRepoRoot, canonicalWorkspace);
|
|
234
243
|
const relative = path.relative(canonicalWorkspace, canonicalSource);
|
|
235
|
-
if (
|
|
236
|
-
|| path.dirname(canonicalSource) !== fs.realpathSync.native(path.dirname(source))
|
|
244
|
+
if (path.dirname(canonicalSource) !== fs.realpathSync.native(path.dirname(source))
|
|
237
245
|
|| relative.startsWith('..') || path.isAbsolute(relative)) throw new Error('SANDBOX_TASK_SOURCE_INVALID');
|
|
238
246
|
return canonicalSource;
|
|
239
247
|
}
|