@fitlab-ai/agent-infra 0.9.13 → 0.9.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/bin/cli.ts +26 -9
- package/dist/bin/cli.js +173 -157
- package/dist/bin/internal-cli.js +42 -21
- package/dist/lib/agent-client.js +5 -0
- package/dist/lib/internal/agent-client.js +5 -2
- package/dist/lib/internal/cli-route-inventory.js +152 -0
- package/dist/lib/internal/codex-lifecycle.js +9 -6
- package/dist/lib/internal/codex-sandbox-controller.js +8 -2
- package/dist/lib/internal/git-workflow.js +10 -6
- package/dist/lib/internal/platform-checks.js +16 -4
- package/dist/lib/internal/platform-comment.js +16 -4
- package/dist/lib/internal/platform-context.js +4 -1
- package/dist/lib/internal/platform-issue.js +12 -4
- package/dist/lib/internal/platform-metadata.js +88 -0
- package/dist/lib/internal/platform-pr-review.js +16 -3
- package/dist/lib/internal/platform-pr.js +24 -10
- package/dist/lib/internal/platform-release-notes.js +6 -3
- package/dist/lib/internal/platform-security.js +93 -0
- package/dist/lib/internal/pr-review-grade.js +14 -3
- package/dist/lib/internal/release-workflow.js +19 -5
- package/dist/lib/internal/sandbox-control.js +22 -5
- package/dist/lib/internal/task-activity.js +13 -4
- package/dist/lib/internal/task-artifact.js +186 -27
- package/dist/lib/internal/task-context.js +4 -1
- package/dist/lib/internal/task-create.js +48 -15
- package/dist/lib/internal/task-delivery.js +4 -1
- package/dist/lib/internal/task-event.js +4 -1
- package/dist/lib/internal/task-finalization.js +4 -1
- package/dist/lib/internal/task-invalidation.js +4 -1
- package/dist/lib/internal/task-ledger.js +12 -1
- package/dist/lib/internal/task-lifecycle.js +4 -1
- package/dist/lib/internal/task-operation-registry.js +421 -0
- package/dist/lib/internal/task-orchestration.js +12 -0
- package/dist/lib/internal/task-override.js +10 -3
- package/dist/lib/internal/task-qualification.js +90 -0
- package/dist/lib/internal/task-review.js +4 -1
- package/dist/lib/internal/task-short-id.js +11 -1
- package/dist/lib/internal/task-snapshot.js +4 -1
- package/dist/lib/internal/task-validate.js +7 -2
- package/dist/lib/internal/task-verify.js +5 -1
- package/dist/lib/internal/task-warning.js +8 -1
- package/dist/lib/platform/comment-safety.js +226 -0
- package/dist/lib/platform/github-provider.js +233 -6
- package/dist/lib/platform/issue-comments.js +146 -34
- package/dist/lib/platform/pr-change-report.js +6 -12
- package/dist/lib/platform/pr-summary.js +28 -7
- package/dist/lib/platform/provider-contract.d.ts +67 -1
- package/dist/lib/platform/provider-contract.js +6 -2
- package/dist/lib/platform/provider-validation.js +76 -3
- package/dist/lib/platform/pull-requests.js +115 -170
- package/dist/lib/platform/repository-metadata.js +377 -0
- package/dist/lib/platform/security-alerts.js +155 -0
- package/dist/lib/platform/verification-sync.js +33 -26
- package/dist/lib/process-data/index.js +7 -0
- package/dist/lib/run/index.js +5 -0
- package/dist/lib/sandbox/commands/control-status.js +22 -0
- package/dist/lib/sandbox/commands/create.js +32 -6
- package/dist/lib/sandbox/commands/ls.js +5 -0
- package/dist/lib/sandbox/commands/rm.js +827 -120
- package/dist/lib/sandbox/commands/show.js +16 -0
- package/dist/lib/sandbox/control/client.js +83 -9
- package/dist/lib/sandbox/control/completed-reentry.js +85 -0
- package/dist/lib/sandbox/control/container-identity.js +28 -31
- package/dist/lib/sandbox/control/executor.js +149 -14
- package/dist/lib/sandbox/control/lifecycle.js +621 -127
- package/dist/lib/sandbox/control/native-file-lock.js +177 -0
- package/dist/lib/sandbox/control/server.js +305 -35
- package/dist/lib/sandbox/control/state.js +5 -3
- package/dist/lib/sandbox/control/task-view.js +183 -0
- package/dist/lib/sandbox/engines/authority.js +289 -0
- package/dist/lib/sandbox/engines/docker-exact-discovery.js +97 -0
- package/dist/lib/sandbox/index.js +5 -0
- package/dist/lib/sandbox/recovery.js +21 -1
- package/dist/lib/sandbox/workspace-view.js +2 -1
- package/dist/lib/sandbox/worktree-safety.js +4 -1
- package/dist/lib/server/index.js +8 -0
- package/dist/lib/task/artifact-lifecycle.js +42 -13
- package/dist/lib/task/artifact-operations.js +520 -0
- package/dist/lib/task/artifact-repair-intent.js +59 -0
- package/dist/lib/task/artifact-schema.js +135 -0
- package/dist/lib/task/capabilities.js +72 -1
- package/dist/lib/task/control-authority.js +4 -0
- package/dist/lib/task/create-service.js +103 -4
- package/dist/lib/task/create.js +25 -0
- package/dist/lib/task/events.js +186 -67
- package/dist/lib/task/index.js +11 -3
- package/dist/lib/task/local-artifact-finalization.js +75 -150
- package/dist/lib/task/orchestration.js +192 -52
- package/dist/lib/task/qualification-audit.js +490 -0
- package/dist/lib/task/qualification-confirmation.js +109 -0
- package/dist/lib/task/qualification-intents.js +148 -0
- package/dist/lib/task/review-artifacts.js +22 -23
- package/dist/lib/task/review-finalization.js +121 -4
- package/dist/lib/task/sections.js +6 -4
- package/dist/lib/task/verification-config.js +11 -0
- package/dist/lib/task/verification-engine.js +54 -4
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-client.ts +9 -0
- package/lib/internal/agent-client.ts +4 -2
- package/lib/internal/cli-route-inventory.ts +140 -0
- package/lib/internal/codex-lifecycle.ts +8 -6
- package/lib/internal/codex-sandbox-controller.ts +7 -2
- package/lib/internal/git-workflow.ts +9 -6
- package/lib/internal/platform-checks.ts +12 -4
- package/lib/internal/platform-comment.ts +12 -4
- package/lib/internal/platform-context.ts +3 -1
- package/lib/internal/platform-issue.ts +11 -4
- package/lib/internal/platform-metadata.ts +70 -0
- package/lib/internal/platform-pr-review.ts +14 -3
- package/lib/internal/platform-pr.ts +23 -10
- package/lib/internal/platform-release-notes.ts +5 -3
- package/lib/internal/platform-security.ts +83 -0
- package/lib/internal/pr-review-grade.ts +10 -3
- package/lib/internal/release-workflow.ts +17 -5
- package/lib/internal/sandbox-control.ts +19 -5
- package/lib/internal/task-activity.ts +12 -4
- package/lib/internal/task-artifact.ts +164 -25
- package/lib/internal/task-context.ts +3 -1
- package/lib/internal/task-create.ts +76 -15
- package/lib/internal/task-delivery.ts +3 -1
- package/lib/internal/task-event.ts +3 -1
- package/lib/internal/task-finalization.ts +3 -1
- package/lib/internal/task-invalidation.ts +3 -1
- package/lib/internal/task-ledger.ts +11 -1
- package/lib/internal/task-lifecycle.ts +3 -1
- package/lib/internal/task-operation-registry.ts +482 -0
- package/lib/internal/task-orchestration.ts +12 -0
- package/lib/internal/task-override.ts +9 -3
- package/lib/internal/task-qualification.ts +61 -0
- package/lib/internal/task-review.ts +3 -1
- package/lib/internal/task-short-id.ts +10 -1
- package/lib/internal/task-snapshot.ts +3 -1
- package/lib/internal/task-validate.ts +6 -2
- package/lib/internal/task-verify.ts +4 -1
- package/lib/internal/task-warning.ts +7 -1
- package/lib/platform/comment-safety.ts +250 -0
- package/lib/platform/github-provider.ts +229 -7
- package/lib/platform/issue-comments.ts +160 -35
- package/lib/platform/pr-change-report.ts +6 -12
- package/lib/platform/pr-summary.ts +27 -6
- package/lib/platform/provider-contract.ts +78 -3
- package/lib/platform/provider-validation.ts +85 -3
- package/lib/platform/pull-requests.ts +126 -178
- package/lib/platform/repository-metadata.ts +453 -0
- package/lib/platform/security-alerts.ts +191 -0
- package/lib/platform/verification-sync.ts +32 -28
- package/lib/process-data/index.ts +7 -0
- package/lib/run/index.ts +5 -0
- package/lib/sandbox/commands/control-status.ts +26 -0
- package/lib/sandbox/commands/create.ts +46 -6
- package/lib/sandbox/commands/ls.ts +7 -0
- package/lib/sandbox/commands/rm.ts +1000 -87
- package/lib/sandbox/commands/show.ts +16 -0
- package/lib/sandbox/control/client.ts +103 -9
- package/lib/sandbox/control/completed-reentry.ts +98 -0
- package/lib/sandbox/control/container-identity.ts +27 -43
- package/lib/sandbox/control/executor.ts +177 -12
- package/lib/sandbox/control/lifecycle.ts +629 -17
- package/lib/sandbox/control/native-file-lock.ts +209 -0
- package/lib/sandbox/control/protocol.ts +5 -2
- package/lib/sandbox/control/server.ts +319 -37
- package/lib/sandbox/control/state.ts +6 -3
- package/lib/sandbox/control/task-view.ts +240 -0
- package/lib/sandbox/engines/authority.ts +365 -0
- package/lib/sandbox/engines/docker-exact-discovery.ts +132 -0
- package/lib/sandbox/index.ts +7 -0
- package/lib/sandbox/recovery.ts +21 -1
- package/lib/sandbox/workspace-view.ts +9 -3
- package/lib/sandbox/worktree-safety.ts +4 -1
- package/lib/server/index.ts +10 -0
- package/lib/task/artifact-lifecycle.ts +38 -16
- package/lib/task/artifact-operations.ts +626 -0
- package/lib/task/artifact-repair-intent.ts +71 -0
- package/lib/task/artifact-schema.ts +192 -0
- package/lib/task/capabilities.ts +70 -1
- package/lib/task/control-authority.ts +11 -1
- package/lib/task/create-service.ts +131 -6
- package/lib/task/create.ts +27 -0
- package/lib/task/events.ts +161 -65
- package/lib/task/index.ts +16 -3
- package/lib/task/local-artifact-finalization.ts +87 -218
- package/lib/task/orchestration.ts +172 -49
- package/lib/task/qualification-audit.ts +599 -0
- package/lib/task/qualification-confirmation.ts +117 -0
- package/lib/task/qualification-intents.ts +150 -0
- package/lib/task/review-artifacts.ts +21 -22
- package/lib/task/review-finalization.ts +133 -4
- package/lib/task/sections.ts +6 -4
- package/lib/task/verification-config.ts +12 -0
- package/lib/task/verification-engine.ts +55 -4
- package/package.json +6 -1
- package/templates/.agents/rules/README.en.md +2 -0
- package/templates/.agents/rules/README.zh-CN.md +2 -0
- package/templates/.agents/rules/decision-qualification.en.md +22 -0
- package/templates/.agents/rules/decision-qualification.zh-CN.md +22 -0
- package/templates/.agents/rules/evidence-reporting.en.md +28 -0
- package/templates/.agents/rules/evidence-reporting.zh-CN.md +28 -0
- package/templates/.agents/rules/label-milestone-setup.en.md +7 -7
- package/templates/.agents/rules/label-milestone-setup.zh-CN.md +7 -7
- package/templates/.agents/rules/local-artifact-repair.en.md +5 -1
- package/templates/.agents/rules/local-artifact-repair.zh-CN.md +5 -1
- package/templates/.agents/rules/security-alerts.en.md +8 -8
- package/templates/.agents/rules/security-alerts.zh-CN.md +8 -8
- package/templates/.agents/skills/analyze-task/SKILL.en.md +15 -2
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +19 -2
- package/templates/.agents/skills/analyze-task/config/verify.en.json +1 -12
- package/templates/.agents/skills/analyze-task/config/verify.zh-CN.json +1 -12
- package/templates/.agents/skills/close-codescan/SKILL.en.md +2 -2
- package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/close-dependabot/SKILL.en.md +2 -2
- package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/code-task/SKILL.en.md +14 -1
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +14 -1
- package/templates/.agents/skills/code-task/config/verify.en.json +5 -14
- package/templates/.agents/skills/code-task/config/verify.zh-CN.json +5 -14
- package/templates/.agents/skills/code-task/reference/code-rules.en.md +5 -4
- package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +5 -4
- package/templates/.agents/skills/code-task/reference/dual-mode.en.md +1 -1
- package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +1 -1
- package/templates/.agents/skills/code-task/reference/fix-mode.en.md +3 -1
- package/templates/.agents/skills/code-task/reference/fix-mode.zh-CN.md +3 -1
- package/templates/.agents/skills/code-task/reference/report-template.en.md +42 -15
- package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +39 -12
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +5 -1
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +5 -1
- package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +2 -0
- package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +2 -0
- package/templates/.agents/skills/complete-task/SKILL.en.md +9 -22
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +9 -22
- package/templates/.agents/skills/create-task/SKILL.en.md +8 -0
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +7 -0
- 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-dependabot/SKILL.en.md +1 -1
- package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/init-labels/SKILL.en.md +8 -8
- package/templates/.agents/skills/init-labels/SKILL.zh-CN.md +8 -8
- package/templates/.agents/skills/init-milestones/SKILL.en.md +9 -11
- package/templates/.agents/skills/init-milestones/SKILL.zh-CN.md +9 -11
- package/templates/.agents/skills/plan-task/SKILL.en.md +14 -1
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +16 -1
- package/templates/.agents/skills/plan-task/config/verify.en.json +4 -14
- package/templates/.agents/skills/plan-task/config/verify.zh-CN.json +4 -14
- package/templates/.agents/skills/review-analysis/SKILL.en.md +14 -1
- package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +14 -1
- package/templates/.agents/skills/review-analysis/config/verify.en.json +1 -18
- package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -18
- package/templates/.agents/skills/review-analysis/reference/report-template.en.md +50 -23
- package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +45 -18
- package/templates/.agents/skills/review-code/SKILL.en.md +14 -1
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +14 -1
- package/templates/.agents/skills/review-code/config/verify.en.json +1 -23
- package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -23
- package/templates/.agents/skills/review-code/reference/report-template.en.md +49 -22
- package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +45 -18
- package/templates/.agents/skills/review-plan/SKILL.en.md +14 -1
- package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +14 -1
- package/templates/.agents/skills/review-plan/config/verify.en.json +1 -18
- package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -18
- package/templates/.agents/skills/review-plan/reference/report-template.en.md +49 -22
- package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +45 -18
- package/templates/.agents/skills/review-pr/SKILL.en.md +5 -1
- package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +5 -1
- package/templates/.agents/skills/review-pr/reference/report-template.en.md +4 -3
- package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +4 -3
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +5 -1
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +5 -1
- package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +3 -1
- package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +3 -1
- package/templates/.agents/skills/test/SKILL.en.md +13 -2
- package/templates/.agents/skills/test/SKILL.zh-CN.md +13 -2
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +14 -0
- package/templates/.agents/templates/task.en.md +6 -0
- package/templates/.agents/templates/task.zh-CN.md +6 -0
- package/templates/.agents/scripts/security-alerts.github.sh +0 -151
- package/templates/.agents/scripts/security-alerts.sh +0 -31
- package/templates/.agents/skills/init-labels/scripts/init-labels.github.sh +0 -146
- package/templates/.agents/skills/init-labels/scripts/init-labels.sh +0 -32
- package/templates/.agents/skills/init-milestones/scripts/init-milestones.github.sh +0 -294
- package/templates/.agents/skills/init-milestones/scripts/init-milestones.sh +0 -6
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export const SANDBOX_TASK_VIEW_STATES = ['not-applicable', 'current', 'finalized-stale', 'unknown'] as const;
|
|
5
|
+
export const SANDBOX_TASK_VIEW_SOURCES = ['active', 'completed', 'unknown'] as const;
|
|
6
|
+
|
|
7
|
+
export type SandboxTaskViewState = typeof SANDBOX_TASK_VIEW_STATES[number];
|
|
8
|
+
export type SandboxTaskViewSource = typeof SANDBOX_TASK_VIEW_SOURCES[number];
|
|
9
|
+
export type SandboxTaskViewReceipt = Readonly<{
|
|
10
|
+
receiptId: string;
|
|
11
|
+
revision: number;
|
|
12
|
+
generation: string;
|
|
13
|
+
requestId: string;
|
|
14
|
+
}>;
|
|
15
|
+
export type SandboxTaskView = Readonly<{
|
|
16
|
+
state: SandboxTaskViewState;
|
|
17
|
+
taskId: string | null;
|
|
18
|
+
observedSource: SandboxTaskViewSource | null;
|
|
19
|
+
receipt: SandboxTaskViewReceipt | null;
|
|
20
|
+
reasonCode: string | null;
|
|
21
|
+
}>;
|
|
22
|
+
|
|
23
|
+
export type TaskViewProjectionInput = Readonly<{
|
|
24
|
+
mode: 'task-bound' | 'branch-only';
|
|
25
|
+
taskId: string | null;
|
|
26
|
+
generation: string;
|
|
27
|
+
source: SandboxTaskViewSource;
|
|
28
|
+
sourceMatches: boolean;
|
|
29
|
+
receipt?: unknown;
|
|
30
|
+
requestId?: string;
|
|
31
|
+
}>;
|
|
32
|
+
|
|
33
|
+
export type TaskViewAccessEffect =
|
|
34
|
+
| 'diagnostic'
|
|
35
|
+
| 'progress'
|
|
36
|
+
| 'artifact-write'
|
|
37
|
+
| 'terminal-verdict'
|
|
38
|
+
| 'recovery'
|
|
39
|
+
| 'cleanup'
|
|
40
|
+
| 'remote-write';
|
|
41
|
+
|
|
42
|
+
export type TaskViewAccess = Readonly<{
|
|
43
|
+
allowed: boolean;
|
|
44
|
+
state: SandboxTaskViewState;
|
|
45
|
+
reasonCode: string | null;
|
|
46
|
+
message: string | null;
|
|
47
|
+
}>;
|
|
48
|
+
|
|
49
|
+
function invalidView(message: string): Error {
|
|
50
|
+
return new Error(`SANDBOX_TASK_VIEW_INVALID: ${message}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function receiptIdentity(value: unknown, generation: string, requestId?: string): SandboxTaskViewReceipt | null {
|
|
54
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
|
55
|
+
const receipt = value as Record<string, unknown>;
|
|
56
|
+
const binding = receipt.controlBinding;
|
|
57
|
+
if (!binding || typeof binding !== 'object' || Array.isArray(binding)) return null;
|
|
58
|
+
const controlBinding = binding as Record<string, unknown>;
|
|
59
|
+
if (receipt.version !== 2 || typeof receipt.receiptId !== 'string' || receipt.receiptId.length === 0
|
|
60
|
+
|| !Number.isSafeInteger(receipt.revision) || (receipt.revision as number) < 0
|
|
61
|
+
|| receipt.lifecycle !== 'done'
|
|
62
|
+
|| controlBinding.generation !== generation
|
|
63
|
+
|| typeof controlBinding.requestId !== 'string'
|
|
64
|
+
|| !/^[a-f0-9-]{16,64}$/u.test(controlBinding.requestId)
|
|
65
|
+
|| (requestId !== undefined && controlBinding.requestId !== requestId)) return null;
|
|
66
|
+
return {
|
|
67
|
+
receiptId: receipt.receiptId,
|
|
68
|
+
revision: receipt.revision as number,
|
|
69
|
+
generation,
|
|
70
|
+
requestId: controlBinding.requestId
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function parseSandboxTaskView(value: unknown): SandboxTaskView {
|
|
75
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw invalidView('taskView must be an object');
|
|
76
|
+
const view = value as Record<string, unknown>;
|
|
77
|
+
const receipt = view.receipt === null ? null : parseSandboxTaskViewReceipt(view.receipt);
|
|
78
|
+
if (!SANDBOX_TASK_VIEW_STATES.includes(view.state as SandboxTaskViewState)
|
|
79
|
+
|| (view.taskId !== null && typeof view.taskId !== 'string')
|
|
80
|
+
|| (view.observedSource !== null && !SANDBOX_TASK_VIEW_SOURCES.includes(view.observedSource as SandboxTaskViewSource))
|
|
81
|
+
|| (view.reasonCode !== null && typeof view.reasonCode !== 'string')
|
|
82
|
+
|| (view.receipt !== null && receipt === null)) {
|
|
83
|
+
throw invalidView('taskView schema is invalid');
|
|
84
|
+
}
|
|
85
|
+
if (view.state === 'not-applicable' && (view.taskId !== null || view.observedSource !== null || view.receipt !== null)) {
|
|
86
|
+
throw invalidView('not-applicable taskView cannot carry task evidence');
|
|
87
|
+
}
|
|
88
|
+
if (view.state === 'current' && (view.taskId === null || view.observedSource === null)) {
|
|
89
|
+
throw invalidView('current taskView requires taskId and observedSource');
|
|
90
|
+
}
|
|
91
|
+
if (view.state === 'current' && view.observedSource === 'completed' && view.receipt === null) {
|
|
92
|
+
throw invalidView('completed current taskView requires receipt');
|
|
93
|
+
}
|
|
94
|
+
if (view.state === 'finalized-stale' && (view.taskId === null || view.receipt === null)) {
|
|
95
|
+
throw invalidView('finalized-stale taskView requires taskId and receipt');
|
|
96
|
+
}
|
|
97
|
+
return view as SandboxTaskView;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function parseSandboxTaskViewReceipt(value: unknown): SandboxTaskViewReceipt | null {
|
|
101
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
|
102
|
+
const receipt = value as Record<string, unknown>;
|
|
103
|
+
if (Object.keys(receipt).sort().join(',') !== 'generation,receiptId,requestId,revision'
|
|
104
|
+
|| typeof receipt.receiptId !== 'string' || receipt.receiptId.length === 0
|
|
105
|
+
|| !Number.isSafeInteger(receipt.revision) || (receipt.revision as number) < 0
|
|
106
|
+
|| typeof receipt.generation !== 'string' || receipt.generation.length === 0
|
|
107
|
+
|| typeof receipt.requestId !== 'string' || !/^[a-f0-9-]{16,64}$/u.test(receipt.requestId)) return null;
|
|
108
|
+
return receipt as SandboxTaskViewReceipt;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function taskViewForManifest(params: Readonly<{
|
|
112
|
+
repoRoot: string;
|
|
113
|
+
mode: 'task-bound' | 'branch-only';
|
|
114
|
+
taskId: string | null;
|
|
115
|
+
generation: string;
|
|
116
|
+
receipt?: unknown;
|
|
117
|
+
}>): SandboxTaskView {
|
|
118
|
+
if (params.mode !== 'task-bound' || !params.taskId) {
|
|
119
|
+
return { state: 'not-applicable', taskId: null, observedSource: null, receipt: null, reasonCode: null };
|
|
120
|
+
}
|
|
121
|
+
const active = path.join(params.repoRoot, '.agents', 'workspace', 'active', params.taskId, 'task.md');
|
|
122
|
+
const completed = path.join(params.repoRoot, '.agents', 'workspace', 'completed', params.taskId, 'task.md');
|
|
123
|
+
const activeExists = fs.existsSync(active);
|
|
124
|
+
const completedExists = fs.existsSync(completed);
|
|
125
|
+
if (activeExists === completedExists) {
|
|
126
|
+
return {
|
|
127
|
+
state: 'unknown', taskId: params.taskId,
|
|
128
|
+
observedSource: activeExists ? 'unknown' : null,
|
|
129
|
+
receipt: null, reasonCode: activeExists ? 'SANDBOX_TASK_VIEW_SOURCE_CONFLICT' : 'SANDBOX_TASK_VIEW_SOURCE_MISSING'
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
const source = completedExists ? 'completed' : 'active';
|
|
133
|
+
const receipt = receiptIdentity(params.receipt, params.generation);
|
|
134
|
+
const receiptProvided = params.receipt !== undefined && params.receipt !== null;
|
|
135
|
+
if (source === 'completed' && receipt) {
|
|
136
|
+
return { state: 'current', taskId: params.taskId, observedSource: source, receipt, reasonCode: null };
|
|
137
|
+
}
|
|
138
|
+
if (source === 'active' && receipt) {
|
|
139
|
+
return {
|
|
140
|
+
state: 'finalized-stale', taskId: params.taskId, observedSource: source, receipt,
|
|
141
|
+
reasonCode: 'SANDBOX_TASK_VIEW_SOURCE_STALE'
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
if (receiptProvided) {
|
|
145
|
+
return {
|
|
146
|
+
state: 'unknown', taskId: params.taskId, observedSource: source, receipt: null,
|
|
147
|
+
reasonCode: 'SANDBOX_TASK_VIEW_RECEIPT_INVALID'
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
if (source === 'active') {
|
|
151
|
+
return { state: 'current', taskId: params.taskId, observedSource: source, receipt: null, reasonCode: null };
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
state: 'unknown', taskId: params.taskId, observedSource: source, receipt: null,
|
|
155
|
+
reasonCode: 'SANDBOX_TASK_VIEW_EVIDENCE_UNAVAILABLE'
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function projectSandboxTaskView(input: TaskViewProjectionInput): SandboxTaskView {
|
|
160
|
+
if (input.mode !== 'task-bound' || !input.taskId) {
|
|
161
|
+
return { state: 'not-applicable', taskId: null, observedSource: null, receipt: null, reasonCode: null };
|
|
162
|
+
}
|
|
163
|
+
const receipt = receiptIdentity(input.receipt, input.generation, input.requestId);
|
|
164
|
+
if (input.source === 'completed' && receipt && input.sourceMatches) {
|
|
165
|
+
return { state: 'current', taskId: input.taskId, observedSource: 'completed', receipt, reasonCode: null };
|
|
166
|
+
}
|
|
167
|
+
if (receipt && input.source === 'active' && !input.sourceMatches) {
|
|
168
|
+
return { state: 'finalized-stale', taskId: input.taskId, observedSource: 'active', receipt, reasonCode: 'SANDBOX_TASK_VIEW_SOURCE_STALE' };
|
|
169
|
+
}
|
|
170
|
+
if (!receipt && input.source === 'active' && input.sourceMatches) {
|
|
171
|
+
return { state: 'current', taskId: input.taskId, observedSource: 'active', receipt: null, reasonCode: null };
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
state: 'unknown',
|
|
175
|
+
taskId: input.taskId,
|
|
176
|
+
observedSource: input.source,
|
|
177
|
+
receipt,
|
|
178
|
+
reasonCode: receipt ? 'SANDBOX_TASK_VIEW_SOURCE_UNCONFIRMED' : 'SANDBOX_TASK_VIEW_EVIDENCE_UNAVAILABLE'
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function taskViewAfterFinalization(params: Readonly<{
|
|
183
|
+
taskId: string;
|
|
184
|
+
generation: string;
|
|
185
|
+
requestId: string;
|
|
186
|
+
receipt: unknown;
|
|
187
|
+
}>): SandboxTaskView {
|
|
188
|
+
const receipt = receiptIdentity(params.receipt, params.generation, params.requestId);
|
|
189
|
+
if (!receipt) {
|
|
190
|
+
return {
|
|
191
|
+
state: 'unknown', taskId: params.taskId, observedSource: 'unknown', receipt: null,
|
|
192
|
+
reasonCode: 'SANDBOX_TASK_VIEW_RECEIPT_INVALID'
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
state: 'finalized-stale', taskId: params.taskId, observedSource: 'active', receipt,
|
|
197
|
+
reasonCode: 'SANDBOX_TASK_VIEW_FINALIZED'
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function mergeSandboxTaskView(
|
|
202
|
+
canonical: SandboxTaskView,
|
|
203
|
+
previous: SandboxTaskView
|
|
204
|
+
): SandboxTaskView {
|
|
205
|
+
if (previous.state !== 'finalized-stale' && previous.state !== 'unknown') return canonical;
|
|
206
|
+
return previous;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function accessSandboxTaskView(view: SandboxTaskView, effect: TaskViewAccessEffect): TaskViewAccess {
|
|
210
|
+
if (effect === 'diagnostic' || effect === 'cleanup' || effect === 'recovery') {
|
|
211
|
+
return { allowed: true, state: view.state, reasonCode: view.reasonCode, message: null };
|
|
212
|
+
}
|
|
213
|
+
if (view.state === 'current' && view.observedSource === 'active') {
|
|
214
|
+
return { allowed: true, state: view.state, reasonCode: null, message: null };
|
|
215
|
+
}
|
|
216
|
+
const reasonCode = view.state === 'current'
|
|
217
|
+
? 'SANDBOX_TASK_VIEW_COMPLETED_READ_ONLY'
|
|
218
|
+
: view.reasonCode ?? `SANDBOX_TASK_VIEW_${view.state.toUpperCase().replaceAll('-', '_')}`;
|
|
219
|
+
return {
|
|
220
|
+
allowed: false,
|
|
221
|
+
state: view.state,
|
|
222
|
+
reasonCode,
|
|
223
|
+
message: view.state === 'unknown'
|
|
224
|
+
? 'task view cannot be confirmed; inspect status, recover the original request, or explicitly clean up the sandbox'
|
|
225
|
+
: view.state === 'finalized-stale'
|
|
226
|
+
? 'task view is finalized/stale; exit or clean up the sandbox, or use a validated re-entry'
|
|
227
|
+
: 'completed task views are read-only'
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function taskViewFromStatus(value: unknown): SandboxTaskView {
|
|
232
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw invalidView('status must be an object');
|
|
233
|
+
const status = value as Record<string, unknown>;
|
|
234
|
+
if (status.version !== 3) throw invalidView('status version is unsupported');
|
|
235
|
+
const taskView = parseSandboxTaskView(status.taskView);
|
|
236
|
+
if (taskView.receipt && taskView.receipt.generation !== status.generation) {
|
|
237
|
+
throw invalidView('taskView receipt generation does not match status generation');
|
|
238
|
+
}
|
|
239
|
+
return taskView;
|
|
240
|
+
}
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import type { SpawnSyncReturns } from 'node:child_process';
|
|
3
|
+
import { runProbe } from '../shell.ts';
|
|
4
|
+
import { getAdapter } from './index.ts';
|
|
5
|
+
|
|
6
|
+
export type SandboxAuthorityEvidenceV1 = Readonly<{
|
|
7
|
+
version: 1;
|
|
8
|
+
provider: string;
|
|
9
|
+
lockDomain: string;
|
|
10
|
+
routeKind: 'default' | 'context' | 'endpoint' | 'wsl2';
|
|
11
|
+
routeSelector: Readonly<Record<string, string>>;
|
|
12
|
+
normalizedEndpoint: string;
|
|
13
|
+
endpointFingerprint: string;
|
|
14
|
+
daemonIdentity: Readonly<{
|
|
15
|
+
kind: 'docker-server-id';
|
|
16
|
+
fingerprint: string;
|
|
17
|
+
}>;
|
|
18
|
+
apiVersion: Readonly<{ major: number; minor: number }>;
|
|
19
|
+
authorityFingerprint: string;
|
|
20
|
+
}>;
|
|
21
|
+
|
|
22
|
+
export type AuthorityProbe = (cmd: string, args: string[], options?: { timeout?: number }) => SpawnSyncReturns<string | Buffer>;
|
|
23
|
+
|
|
24
|
+
export type AuthorityCaptureOptions = Readonly<{
|
|
25
|
+
probe?: AuthorityProbe;
|
|
26
|
+
timeoutMs?: number;
|
|
27
|
+
env?: NodeJS.ProcessEnv;
|
|
28
|
+
lockDomain?: string;
|
|
29
|
+
route?: SandboxAuthorityEvidenceV1;
|
|
30
|
+
}>;
|
|
31
|
+
|
|
32
|
+
export type AuthorityVerification = Readonly<{
|
|
33
|
+
state: 'verified' | 'unknown' | 'conflict';
|
|
34
|
+
reason?: string;
|
|
35
|
+
}>;
|
|
36
|
+
|
|
37
|
+
function digest(value: string): string {
|
|
38
|
+
return createHash('sha256').update(value).digest('hex');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function canonical(value: unknown): string {
|
|
42
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`;
|
|
43
|
+
if (!value || typeof value !== 'object') return JSON.stringify(value) ?? 'null';
|
|
44
|
+
return `{${Object.entries(value as Record<string, unknown>)
|
|
45
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
46
|
+
.map(([key, entry]) => `${JSON.stringify(key)}:${canonical(entry)}`)
|
|
47
|
+
.join(',')}}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function text(value: string | Buffer | null | undefined): string {
|
|
51
|
+
return typeof value === 'string' ? value : value?.toString('utf8') ?? '';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function safeEndpoint(value: string): string {
|
|
55
|
+
if (!value) return 'docker-default';
|
|
56
|
+
try {
|
|
57
|
+
const parsed = new URL(value);
|
|
58
|
+
const hasUnreplayableParts = Boolean(parsed.username || parsed.password || parsed.search || parsed.hash);
|
|
59
|
+
parsed.username = '';
|
|
60
|
+
parsed.password = '';
|
|
61
|
+
parsed.search = '';
|
|
62
|
+
parsed.hash = '';
|
|
63
|
+
const normalized = parsed.toString().replace(/\/$/u, '');
|
|
64
|
+
return hasUnreplayableParts
|
|
65
|
+
? `${normalized}#<redacted>`
|
|
66
|
+
: normalized;
|
|
67
|
+
} catch {
|
|
68
|
+
const normalized = value
|
|
69
|
+
.replace(/\/\/[^/@]+@/u, '//<redacted>@')
|
|
70
|
+
.replace(/[?&](?:token|password|secret|key)=[^&]*/giu, '');
|
|
71
|
+
return /[@?#]/u.test(value) ? `${normalized.replace(/[?#].*$/u, '')}#<redacted>` : normalized;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function lockDomainFor(engine: string, endpoint: string): string {
|
|
76
|
+
return digest(`${process.platform}\0${process.arch}\0${engine}\0${endpoint}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
type SandboxAuthorityRoute = Readonly<{
|
|
80
|
+
kind: SandboxAuthorityEvidenceV1['routeKind'];
|
|
81
|
+
selector: Readonly<Record<string, string>>;
|
|
82
|
+
endpoint: string;
|
|
83
|
+
command: (args: string[]) => { cmd: string; args: string[] };
|
|
84
|
+
}>;
|
|
85
|
+
|
|
86
|
+
function contextRoute(context: string): SandboxAuthorityRoute {
|
|
87
|
+
return {
|
|
88
|
+
kind: 'context',
|
|
89
|
+
selector: { context },
|
|
90
|
+
endpoint: `docker-context://${context}`,
|
|
91
|
+
command: (args) => ({ cmd: 'docker', args: ['--context', context, ...args] })
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function endpointRoute(endpoint: string, rawEndpoint: string, unreplayable?: string): SandboxAuthorityRoute {
|
|
96
|
+
return {
|
|
97
|
+
kind: 'endpoint',
|
|
98
|
+
selector: {
|
|
99
|
+
source: 'DOCKER_HOST',
|
|
100
|
+
endpoint,
|
|
101
|
+
...(unreplayable ? { unreplayable } : {})
|
|
102
|
+
},
|
|
103
|
+
endpoint,
|
|
104
|
+
command: (args) => ({ cmd: 'docker', args: ['--host', rawEndpoint, ...args] })
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function tlsEnvironmentReason(env: NodeJS.ProcessEnv): string | undefined {
|
|
109
|
+
return env.DOCKER_TLS_VERIFY || env.DOCKER_CERT_PATH || env.DOCKER_TLS
|
|
110
|
+
? 'tls-environment'
|
|
111
|
+
: undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function routeFor(engine: string, env: NodeJS.ProcessEnv): SandboxAuthorityRoute {
|
|
115
|
+
const context = getAdapter(engine).dockerContext;
|
|
116
|
+
if (context) {
|
|
117
|
+
return contextRoute(context);
|
|
118
|
+
}
|
|
119
|
+
if (env.DOCKER_CONTEXT) {
|
|
120
|
+
return contextRoute(env.DOCKER_CONTEXT);
|
|
121
|
+
}
|
|
122
|
+
if (env.DOCKER_HOST) {
|
|
123
|
+
return endpointRoute(safeEndpoint(env.DOCKER_HOST), env.DOCKER_HOST, tlsEnvironmentReason(env));
|
|
124
|
+
}
|
|
125
|
+
return contextRoute('default');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function validRouteName(value: string): boolean {
|
|
129
|
+
return /^[A-Za-z0-9][A-Za-z0-9_.@/-]*$/u.test(value);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function resolveWslDistro(probe: AuthorityProbe, timeoutMs?: number, explicit?: string): string {
|
|
133
|
+
if (explicit) {
|
|
134
|
+
if (!validRouteName(explicit)) throw new Error('SANDBOX_AUTHORITY_ROUTE_CAPTURE_FAILED');
|
|
135
|
+
return explicit;
|
|
136
|
+
}
|
|
137
|
+
const result = probe(
|
|
138
|
+
'wsl.exe',
|
|
139
|
+
['--list', '--verbose'],
|
|
140
|
+
timeoutMs === undefined ? {} : { timeout: timeoutMs }
|
|
141
|
+
);
|
|
142
|
+
if (result.status !== 0) throw new Error('SANDBOX_AUTHORITY_ROUTE_CAPTURE_FAILED');
|
|
143
|
+
const output = text(result.stdout).replace(/\0/gu, '').replace(/\r/gu, '');
|
|
144
|
+
const defaultLine = output.split('\n').find((line) => /^\s*\*\s+/u.test(line));
|
|
145
|
+
const distro = defaultLine?.replace(/^\s*\*\s+/u, '').replace(/\s+(?:Running|Stopped)\s+\d+\s*$/iu, '').trim();
|
|
146
|
+
if (!distro || !validRouteName(distro)) throw new Error('SANDBOX_AUTHORITY_ROUTE_CAPTURE_FAILED');
|
|
147
|
+
return distro;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function wslRoute(
|
|
151
|
+
distro: string,
|
|
152
|
+
context: string | undefined,
|
|
153
|
+
env: NodeJS.ProcessEnv
|
|
154
|
+
): SandboxAuthorityRoute {
|
|
155
|
+
const endpoint = env.DOCKER_HOST
|
|
156
|
+
? safeEndpoint(env.DOCKER_HOST)
|
|
157
|
+
: `wsl://${distro}/${context ?? 'default'}`;
|
|
158
|
+
const unreplayable = tlsEnvironmentReason(env)
|
|
159
|
+
?? (endpoint.includes('<redacted>') ? 'ambient-endpoint-secret' : undefined);
|
|
160
|
+
return {
|
|
161
|
+
kind: 'wsl2',
|
|
162
|
+
selector: {
|
|
163
|
+
distro,
|
|
164
|
+
...(context ? { context } : {}),
|
|
165
|
+
...(unreplayable ? { unreplayable } : {}),
|
|
166
|
+
...(env.DOCKER_HOST ? { endpoint } : {})
|
|
167
|
+
},
|
|
168
|
+
endpoint,
|
|
169
|
+
command: (args) => ({
|
|
170
|
+
cmd: 'wsl.exe',
|
|
171
|
+
args: [
|
|
172
|
+
'--distribution', distro,
|
|
173
|
+
'--exec', 'docker',
|
|
174
|
+
...(context ? ['--context', context] : []),
|
|
175
|
+
...(env.DOCKER_HOST ? ['--host', env.DOCKER_HOST] : []),
|
|
176
|
+
...args
|
|
177
|
+
]
|
|
178
|
+
})
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function currentWslRoute(
|
|
183
|
+
env: NodeJS.ProcessEnv,
|
|
184
|
+
probe: AuthorityProbe,
|
|
185
|
+
timeoutMs?: number
|
|
186
|
+
): SandboxAuthorityRoute {
|
|
187
|
+
const distro = resolveWslDistro(probe, timeoutMs, env.WSL_DISTRO_NAME);
|
|
188
|
+
let context = env.DOCKER_CONTEXT;
|
|
189
|
+
if (!context && !env.DOCKER_HOST) {
|
|
190
|
+
const result = probe(
|
|
191
|
+
'wsl.exe',
|
|
192
|
+
['--distribution', distro, '--exec', 'docker', 'context', 'show'],
|
|
193
|
+
timeoutMs === undefined ? {} : { timeout: timeoutMs }
|
|
194
|
+
);
|
|
195
|
+
context = text(result.stdout).trim();
|
|
196
|
+
if (result.status !== 0 || !validRouteName(context)) {
|
|
197
|
+
throw new Error('SANDBOX_AUTHORITY_ROUTE_CAPTURE_FAILED');
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return wslRoute(distro, context, env);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function routeFromEvidence(evidence: SandboxAuthorityEvidenceV1): SandboxAuthorityRoute {
|
|
204
|
+
return {
|
|
205
|
+
kind: evidence.routeKind,
|
|
206
|
+
selector: evidence.routeSelector,
|
|
207
|
+
endpoint: evidence.normalizedEndpoint,
|
|
208
|
+
command: (args) => commandForSandboxAuthority(evidence, 'docker', args)
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function currentContextRoute(
|
|
213
|
+
engine: string,
|
|
214
|
+
env: NodeJS.ProcessEnv,
|
|
215
|
+
probe: AuthorityProbe,
|
|
216
|
+
timeoutMs?: number
|
|
217
|
+
): SandboxAuthorityRoute {
|
|
218
|
+
const configured = routeFor(engine, env);
|
|
219
|
+
if (configured.kind !== 'context' || configured.selector.context !== 'default') return configured;
|
|
220
|
+
if (env.DOCKER_CONTEXT || env.DOCKER_HOST || getAdapter(engine).dockerContext) return configured;
|
|
221
|
+
const result = probe('docker', ['context', 'show'], timeoutMs === undefined ? {} : { timeout: timeoutMs });
|
|
222
|
+
const context = text(result.stdout).trim();
|
|
223
|
+
if (result.status !== 0 || !/^[A-Za-z0-9][A-Za-z0-9_.@/-]*$/u.test(context)) {
|
|
224
|
+
throw new Error('SANDBOX_AUTHORITY_ROUTE_CAPTURE_FAILED');
|
|
225
|
+
}
|
|
226
|
+
return contextRoute(context);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Build a command using only the route persisted with the sandbox authority. */
|
|
230
|
+
export function commandForSandboxAuthority(
|
|
231
|
+
evidence: SandboxAuthorityEvidenceV1,
|
|
232
|
+
cmd: string,
|
|
233
|
+
args: string[] = []
|
|
234
|
+
): { cmd: string; args: string[] } {
|
|
235
|
+
if (cmd !== 'docker') {
|
|
236
|
+
throw new Error('SANDBOX_AUTHORITY_ROUTE_UNSUPPORTED');
|
|
237
|
+
}
|
|
238
|
+
if (evidence.routeKind === 'context') {
|
|
239
|
+
const context = evidence.routeSelector.context;
|
|
240
|
+
if (!context) throw new Error('SANDBOX_AUTHORITY_ROUTE_INVALID');
|
|
241
|
+
return { cmd, args: ['--context', context, ...args] };
|
|
242
|
+
}
|
|
243
|
+
if (evidence.routeKind === 'endpoint') {
|
|
244
|
+
const endpoint = evidence.routeSelector.endpoint;
|
|
245
|
+
if (!endpoint || endpoint.includes('<redacted>') || evidence.routeSelector.unreplayable) {
|
|
246
|
+
throw new Error('SANDBOX_AUTHORITY_ROUTE_UNREPLAYABLE');
|
|
247
|
+
}
|
|
248
|
+
return { cmd, args: ['--host', endpoint, ...args] };
|
|
249
|
+
}
|
|
250
|
+
if (evidence.routeKind === 'wsl2') {
|
|
251
|
+
const distro = evidence.routeSelector.distro;
|
|
252
|
+
const context = evidence.routeSelector.context;
|
|
253
|
+
const endpoint = evidence.routeSelector.endpoint;
|
|
254
|
+
if (!distro || evidence.routeSelector.unreplayable || endpoint?.includes('<redacted>')) {
|
|
255
|
+
throw new Error('SANDBOX_AUTHORITY_ROUTE_UNREPLAYABLE');
|
|
256
|
+
}
|
|
257
|
+
return {
|
|
258
|
+
cmd: 'wsl.exe',
|
|
259
|
+
args: [
|
|
260
|
+
'--distribution', distro,
|
|
261
|
+
'--exec', cmd,
|
|
262
|
+
...(context ? ['--context', context] : []),
|
|
263
|
+
...(endpoint && !endpoint.startsWith('wsl://') ? ['--host', endpoint] : []),
|
|
264
|
+
...args
|
|
265
|
+
]
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
return { cmd, args: ['--context', 'default', ...args] };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function authorityVersionArgs(): string[] {
|
|
272
|
+
return ['version', '--format', '{{json .Server}}'];
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function captureSandboxAuthority(
|
|
276
|
+
engine: string,
|
|
277
|
+
options: AuthorityCaptureOptions = {}
|
|
278
|
+
): SandboxAuthorityEvidenceV1 {
|
|
279
|
+
const env = options.env ?? process.env;
|
|
280
|
+
const probe = options.probe ?? runProbe;
|
|
281
|
+
const route = options.route
|
|
282
|
+
? routeFromEvidence(options.route)
|
|
283
|
+
: engine === 'wsl2'
|
|
284
|
+
? currentWslRoute(env, probe, options.timeoutMs)
|
|
285
|
+
: currentContextRoute(engine, env, probe, options.timeoutMs);
|
|
286
|
+
function probeJson(args: string[]): unknown {
|
|
287
|
+
const command = options.route
|
|
288
|
+
? commandForSandboxAuthority(options.route, 'docker', args)
|
|
289
|
+
: route.command(args);
|
|
290
|
+
try {
|
|
291
|
+
const response = probe(
|
|
292
|
+
command.cmd,
|
|
293
|
+
command.args,
|
|
294
|
+
options.timeoutMs === undefined ? {} : { timeout: options.timeoutMs }
|
|
295
|
+
);
|
|
296
|
+
if (response.status !== 0) throw new Error('SANDBOX_AUTHORITY_CAPTURE_FAILED');
|
|
297
|
+
return JSON.parse(text(response.stdout).trim());
|
|
298
|
+
} catch {
|
|
299
|
+
throw new Error('SANDBOX_AUTHORITY_CAPTURE_FAILED');
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
const server = probeJson(authorityVersionArgs());
|
|
303
|
+
if (!server || typeof server !== 'object' || Array.isArray(server)) {
|
|
304
|
+
throw new Error('SANDBOX_AUTHORITY_CAPTURE_FAILED');
|
|
305
|
+
}
|
|
306
|
+
const record = server as Record<string, unknown>;
|
|
307
|
+
const api = typeof record.ApiVersion === 'string' ? /^(\d+)\.(\d+)$/u.exec(record.ApiVersion) : null;
|
|
308
|
+
if (!api) throw new Error('SANDBOX_AUTHORITY_CAPTURE_FAILED');
|
|
309
|
+
// Docker version exposes API metadata; the daemon ID belongs to Docker info.
|
|
310
|
+
const serverId = probeJson(['info', '--format', '{{json .ID}}']);
|
|
311
|
+
if (typeof serverId !== 'string' || !serverId.trim()) throw new Error('SANDBOX_AUTHORITY_CAPTURE_FAILED');
|
|
312
|
+
const normalizedEndpoint = route.endpoint;
|
|
313
|
+
const evidenceWithoutFingerprint = {
|
|
314
|
+
version: 1 as const,
|
|
315
|
+
provider: engine,
|
|
316
|
+
lockDomain: options.lockDomain ?? options.route?.lockDomain ?? lockDomainFor(engine, normalizedEndpoint),
|
|
317
|
+
routeKind: route.kind,
|
|
318
|
+
routeSelector: route.selector,
|
|
319
|
+
normalizedEndpoint,
|
|
320
|
+
endpointFingerprint: digest(normalizedEndpoint),
|
|
321
|
+
daemonIdentity: { kind: 'docker-server-id' as const, fingerprint: digest(serverId) },
|
|
322
|
+
apiVersion: { major: Number(api[1]), minor: Number(api[2]) }
|
|
323
|
+
};
|
|
324
|
+
return {
|
|
325
|
+
...evidenceWithoutFingerprint,
|
|
326
|
+
authorityFingerprint: digest(canonical(evidenceWithoutFingerprint))
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function verifySandboxAuthority(
|
|
331
|
+
expected: SandboxAuthorityEvidenceV1 | null | undefined,
|
|
332
|
+
actual: SandboxAuthorityEvidenceV1 | null
|
|
333
|
+
): AuthorityVerification {
|
|
334
|
+
if (!expected) return { state: 'unknown', reason: 'SANDBOX_AUTHORITY_EVIDENCE_MISSING' };
|
|
335
|
+
if (!actual) return { state: 'unknown', reason: 'SANDBOX_AUTHORITY_UNAVAILABLE' };
|
|
336
|
+
if (expected.authorityFingerprint !== actual.authorityFingerprint
|
|
337
|
+
|| expected.lockDomain !== actual.lockDomain
|
|
338
|
+
|| expected.routeKind !== actual.routeKind
|
|
339
|
+
|| expected.endpointFingerprint !== actual.endpointFingerprint
|
|
340
|
+
|| expected.daemonIdentity.fingerprint !== actual.daemonIdentity.fingerprint) {
|
|
341
|
+
return { state: 'conflict', reason: 'SANDBOX_AUTHORITY_DRIFT' };
|
|
342
|
+
}
|
|
343
|
+
return { state: 'verified' };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export function isSandboxAuthorityEvidence(value: unknown): value is SandboxAuthorityEvidenceV1 {
|
|
347
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
348
|
+
const evidence = value as Record<string, unknown>;
|
|
349
|
+
const selector = evidence.routeSelector as Record<string, unknown> | null;
|
|
350
|
+
const daemon = evidence.daemonIdentity as Record<string, unknown> | null;
|
|
351
|
+
const api = evidence.apiVersion as Record<string, unknown> | null;
|
|
352
|
+
return evidence.version === 1
|
|
353
|
+
&& typeof evidence.provider === 'string'
|
|
354
|
+
&& /^[a-f0-9]{64}$/u.test(evidence.lockDomain as string)
|
|
355
|
+
&& ['default', 'context', 'endpoint', 'wsl2'].includes(evidence.routeKind as string)
|
|
356
|
+
&& !!selector && typeof selector === 'object' && !Array.isArray(selector)
|
|
357
|
+
&& Object.values(selector).every((entry) => typeof entry === 'string')
|
|
358
|
+
&& typeof evidence.normalizedEndpoint === 'string'
|
|
359
|
+
&& /^[a-f0-9]{64}$/u.test(evidence.endpointFingerprint as string)
|
|
360
|
+
&& !!daemon && daemon.kind === 'docker-server-id'
|
|
361
|
+
&& /^[a-f0-9]{64}$/u.test(daemon.fingerprint as string)
|
|
362
|
+
&& !!api && Number.isSafeInteger(api.major) && (api.major as number) >= 0
|
|
363
|
+
&& Number.isSafeInteger(api.minor) && (api.minor as number) >= 0
|
|
364
|
+
&& /^[a-f0-9]{64}$/u.test(evidence.authorityFingerprint as string);
|
|
365
|
+
}
|