@fitlab-ai/agent-infra 0.11.10 → 0.11.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/agent-clients/adapter.js +20 -0
- package/dist/lib/agent-clients/adapters/codex-sandbox.js +13 -0
- package/dist/lib/agent-clients/adapters/codex.js +4 -0
- package/dist/lib/internal/manual-validation.js +2 -2
- package/dist/lib/internal/platform-pr.js +4 -5
- package/dist/lib/internal/task-artifact.js +1 -1
- package/dist/lib/internal/task-create.js +15 -1
- package/dist/lib/internal/task-finalization.js +1 -3
- package/dist/lib/internal/task-operation-registry.js +4 -138
- package/dist/lib/platform/pr-summary.js +45 -8
- package/dist/lib/platform/pull-requests.js +79 -14
- package/dist/lib/platform/verification-sync.js +17 -14
- package/dist/lib/sandbox/commands/create.js +18 -2
- package/dist/lib/sandbox/commands/enter.js +47 -33
- package/dist/lib/sandbox/control/client.js +46 -12
- package/dist/lib/sandbox/control/executor.js +4 -11
- package/dist/lib/sandbox/control/host-recovery.js +218 -0
- package/dist/lib/sandbox/control/protocol.js +2 -3
- package/dist/lib/sandbox/control/server.js +42 -2
- package/dist/lib/sandbox/environment.js +150 -0
- package/dist/lib/sandbox/recovery.js +8 -0
- package/dist/lib/sandbox/workspace-identity.js +0 -14
- package/dist/lib/sandbox/workspace-view.js +33 -6
- package/dist/lib/task/artifact-command.js +7 -2
- package/dist/lib/task/artifact-lifecycle.js +2 -1
- package/dist/lib/task/capabilities.js +4 -1
- package/dist/lib/task/control-authority.js +0 -2
- package/dist/lib/task/events.js +1 -1
- package/dist/lib/task/finalization.js +37 -54
- package/dist/lib/task/frontmatter.js +24 -1
- package/dist/lib/task/ledger-intents.js +2 -1
- package/dist/lib/task/platform-operation-journal.js +12 -0
- package/dist/lib/task/platform-operation-recovery.js +14 -7
- package/dist/lib/task/resolve-ref.js +8 -3
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapter.ts +22 -0
- package/lib/agent-clients/adapters/codex-sandbox.ts +13 -0
- package/lib/agent-clients/adapters/codex.ts +4 -0
- package/lib/internal/manual-validation.ts +2 -2
- package/lib/internal/platform-pr.ts +3 -4
- package/lib/internal/task-artifact.ts +1 -1
- package/lib/internal/task-create.ts +15 -2
- package/lib/internal/task-finalization.ts +1 -3
- package/lib/internal/task-operation-registry.ts +4 -150
- package/lib/platform/pr-summary.ts +43 -7
- package/lib/platform/pull-requests.ts +74 -13
- package/lib/platform/verification-sync.ts +18 -14
- package/lib/sandbox/commands/create.ts +24 -2
- package/lib/sandbox/commands/enter.ts +42 -39
- package/lib/sandbox/control/client.ts +52 -15
- package/lib/sandbox/control/executor.ts +5 -12
- package/lib/sandbox/control/host-recovery.ts +195 -0
- package/lib/sandbox/control/protocol.ts +4 -5
- package/lib/sandbox/control/server.ts +42 -1
- package/lib/sandbox/environment.ts +162 -0
- package/lib/sandbox/recovery.ts +10 -0
- package/lib/sandbox/tool-types.ts +1 -0
- package/lib/sandbox/workspace-identity.ts +0 -26
- package/lib/sandbox/workspace-view.ts +33 -8
- package/lib/task/artifact-command.ts +9 -2
- package/lib/task/artifact-lifecycle.ts +2 -1
- package/lib/task/capabilities.ts +5 -2
- package/lib/task/control-authority.ts +0 -4
- package/lib/task/events.ts +1 -1
- package/lib/task/finalization.ts +34 -51
- package/lib/task/frontmatter.ts +25 -1
- package/lib/task/ledger-intents.ts +2 -1
- package/lib/task/platform-operation-journal.ts +29 -2
- package/lib/task/platform-operation-recovery.ts +12 -6
- package/lib/task/resolve-ref.ts +8 -3
- package/package.json +1 -1
- package/templates/.agents/rules/README.en.md +1 -1
- package/templates/.agents/rules/README.zh-CN.md +1 -1
- 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/no-mid-flow-questions.en.md +19 -0
- package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +19 -0
- package/templates/.agents/rules/review-handshake.en.md +3 -0
- package/templates/.agents/rules/review-handshake.zh-CN.md +3 -0
- package/templates/.agents/rules/task-management.en.md +1 -1
- package/templates/.agents/rules/task-management.zh-CN.md +1 -1
- package/templates/.agents/skills/analyze-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/block-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/cancel-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/close-codescan/SKILL.en.md +3 -0
- package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +3 -0
- package/templates/.agents/skills/close-dependabot/SKILL.en.md +3 -0
- package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +3 -0
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/code-task/reference/branch-management.en.md +1 -1
- package/templates/.agents/skills/code-task/reference/branch-management.zh-CN.md +1 -1
- package/templates/.agents/skills/code-task/reference/dual-mode.en.md +2 -0
- package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +2 -0
- package/templates/.agents/skills/code-task/reference/fix-mode.en.md +1 -1
- package/templates/.agents/skills/code-task/reference/fix-mode.zh-CN.md +1 -1
- package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +1 -1
- package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/create-pr/SKILL.en.md +1 -1
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/create-pr/reference/branch-strategy.en.md +2 -2
- package/templates/.agents/skills/create-pr/reference/branch-strategy.zh-CN.md +2 -2
- package/templates/.agents/skills/create-task/SKILL.en.md +3 -5
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +3 -4
- 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/import-issue/SKILL.en.md +1 -1
- package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/plan-task/SKILL.en.md +1 -1
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/review-code/SKILL.en.md +4 -4
- package/templates/.agents/skills/review-code/SKILL.zh-CN.md +4 -4
- package/templates/.agents/skills/review-pr/SKILL.en.md +1 -1
- package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +1 -1
- package/templates/.agents/skills/test/SKILL.en.md +4 -1
- package/templates/.agents/skills/test/SKILL.zh-CN.md +4 -1
- package/templates/.agents/skills/test-integration/SKILL.en.md +5 -2
- package/templates/.agents/skills/test-integration/SKILL.zh-CN.md +5 -2
- package/templates/.agents/skills/update-agent-infra/SKILL.en.md +4 -1
- package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +4 -1
- package/templates/.agents/skills/upgrade-dependency/SKILL.en.md +5 -2
- package/templates/.agents/skills/upgrade-dependency/SKILL.zh-CN.md +5 -2
- package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -2
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +2 -2
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +1 -1
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +1 -1
- package/dist/lib/task/finalization-handoff.js +0 -93
- package/lib/task/finalization-handoff.ts +0 -105
|
@@ -457,7 +457,7 @@ function checkTaskCommentContent(context, remoteData, shared) {
|
|
|
457
457
|
return shared.failResult(CHECK_TYPE, buildCommentContentMismatchMessage("task", context.issueNumber, expectedBody, commentBody), "check_failed");
|
|
458
458
|
}
|
|
459
459
|
function checkPrTypeLabel(context, remoteData, shared) {
|
|
460
|
-
if (!context.audit.enabled || !context.hasTriage || !context.
|
|
460
|
+
if (!context.audit.enabled || !context.hasTriage || !context.prIdentity || remoteData.prLabels === undefined || remoteData.prLabels === null) {
|
|
461
461
|
return infoResult('PR type-label audit is not applicable because the required PR metadata or capability is unavailable');
|
|
462
462
|
}
|
|
463
463
|
const expectedLabel = mapTaskTypeToLabel(context.task.metadata.type);
|
|
@@ -465,12 +465,12 @@ function checkPrTypeLabel(context, remoteData, shared) {
|
|
|
465
465
|
return infoResult('PR type-label audit is not applicable because the task type has no label mapping');
|
|
466
466
|
}
|
|
467
467
|
if (remoteData.prLabels.includes(expectedLabel)) {
|
|
468
|
-
return shared.passResult(CHECK_TYPE,
|
|
468
|
+
return shared.passResult(CHECK_TYPE, `${pullRequestLabel(context)} has expected type label '${expectedLabel}'`);
|
|
469
469
|
}
|
|
470
|
-
return shared.failResult(CHECK_TYPE, `Expected type label '${expectedLabel}' not found on
|
|
470
|
+
return shared.failResult(CHECK_TYPE, `Expected type label '${expectedLabel}' not found on ${pullRequestLabel(context)}`, "check_failed");
|
|
471
471
|
}
|
|
472
472
|
function checkInLabelsMatchPr(context, remoteData, shared) {
|
|
473
|
-
if (!context.audit.enabled || !context.hasTriage || !context.
|
|
473
|
+
if (!context.audit.enabled || !context.hasTriage || !context.prIdentity || remoteData.prLabels === undefined || remoteData.prLabels === null) {
|
|
474
474
|
return infoResult('PR in: label audit is not applicable because the required PR metadata or capability is unavailable');
|
|
475
475
|
}
|
|
476
476
|
const issueInLabels = extractLabelNames(remoteData.issue.labels)
|
|
@@ -480,9 +480,12 @@ function checkInLabelsMatchPr(context, remoteData, shared) {
|
|
|
480
480
|
.filter((label) => label.startsWith("in:"))
|
|
481
481
|
.sort();
|
|
482
482
|
if (arraysEqual(issueInLabels, prInLabels)) {
|
|
483
|
-
return shared.passResult(CHECK_TYPE,
|
|
483
|
+
return shared.passResult(CHECK_TYPE, `${pullRequestLabel(context)} in: labels match Issue #${context.issueNumber}`);
|
|
484
484
|
}
|
|
485
|
-
return shared.failResult(CHECK_TYPE, `in: labels mismatch —
|
|
485
|
+
return shared.failResult(CHECK_TYPE, `in: labels mismatch — ${pullRequestLabel(context)} has [${formatLabelList(prInLabels)}], Issue #${context.issueNumber} has [${formatLabelList(issueInLabels)}]`, "check_failed");
|
|
486
|
+
}
|
|
487
|
+
function pullRequestLabel(context) {
|
|
488
|
+
return context.prNumber ? `PR #${context.prNumber}` : 'Pull request';
|
|
486
489
|
}
|
|
487
490
|
function checkInLabelsComputed(context, remoteData, shared) {
|
|
488
491
|
if (!context.audit.enabled || !context.hasTriage) {
|
|
@@ -574,13 +577,13 @@ function checkIssueFields(context, remoteData, shared) {
|
|
|
574
577
|
return shared.passResult(CHECK_TYPE, `Issue #${context.issueNumber} fields match task metadata`);
|
|
575
578
|
}
|
|
576
579
|
function checkPrAssignee(context, remoteData, shared) {
|
|
577
|
-
if (!context.audit.enabled || !context.hasPush || !context.
|
|
580
|
+
if (!context.audit.enabled || !context.hasPush || !context.prIdentity || remoteData.prAssignees === undefined || remoteData.prAssignees === null) {
|
|
578
581
|
return infoResult('PR assignee audit is not applicable because it is disabled or required PR metadata/capability is unavailable');
|
|
579
582
|
}
|
|
580
|
-
if (
|
|
581
|
-
return shared.failResult(CHECK_TYPE,
|
|
583
|
+
if (remoteData.prAssignees.length === 0) {
|
|
584
|
+
return shared.failResult(CHECK_TYPE, `${pullRequestLabel(context)} has no assignee`, "check_failed");
|
|
582
585
|
}
|
|
583
|
-
return shared.passResult(CHECK_TYPE,
|
|
586
|
+
return shared.passResult(CHECK_TYPE, `${pullRequestLabel(context)} has an assignee`);
|
|
584
587
|
}
|
|
585
588
|
function checkMilestone(context, remoteData, shared) {
|
|
586
589
|
if (!context.hasTriage) {
|
|
@@ -589,16 +592,16 @@ function checkMilestone(context, remoteData, shared) {
|
|
|
589
592
|
if (!remoteData.issue?.milestone?.title) {
|
|
590
593
|
return shared.failResult(CHECK_TYPE, `Issue #${context.issueNumber} has no milestone set`, "check_failed");
|
|
591
594
|
}
|
|
592
|
-
if (context.
|
|
593
|
-
return shared.failResult(CHECK_TYPE,
|
|
595
|
+
if (context.prIdentity && remoteData.prMilestone !== undefined && !remoteData.prMilestone?.title) {
|
|
596
|
+
return shared.failResult(CHECK_TYPE, `${pullRequestLabel(context)} has no milestone set`, "check_failed");
|
|
594
597
|
}
|
|
595
598
|
if (context.audit.requireSpecificMilestone) {
|
|
596
599
|
const issueTitle = remoteData.issue.milestone.title;
|
|
597
600
|
if (VERSION_LINE_REGEX.test(issueTitle)) {
|
|
598
601
|
return shared.failResult(CHECK_TYPE, `Issue #${context.issueNumber} milestone '${issueTitle}' is a release line; narrow to a specific version (e.g. ${issueTitle.replace(/\.x$/, ".N")}) before continuing`, "check_failed");
|
|
599
602
|
}
|
|
600
|
-
if (context.
|
|
601
|
-
return shared.failResult(CHECK_TYPE,
|
|
603
|
+
if (context.prIdentity && remoteData.prMilestone?.title && VERSION_LINE_REGEX.test(remoteData.prMilestone.title)) {
|
|
604
|
+
return shared.failResult(CHECK_TYPE, `${pullRequestLabel(context)} milestone '${remoteData.prMilestone.title}' is a release line; narrow to a specific version before continuing`, "check_failed");
|
|
602
605
|
}
|
|
603
606
|
}
|
|
604
607
|
return shared.passResult(CHECK_TYPE, `Issue #${context.issueNumber} milestone satisfies the configured policy`);
|
|
@@ -672,7 +672,7 @@ function readProjectInitCommandDigest(controlRoot) {
|
|
|
672
672
|
function writeProjectInitCommandDigest(controlRoot, commandSha256) {
|
|
673
673
|
atomicWriteJson(path.join(controlRoot, 'project-init.json'), { version: 1, commandSha256 });
|
|
674
674
|
}
|
|
675
|
-
export async function create(args, { runProjectInitCommand = true } = {}) {
|
|
675
|
+
export async function create(args, { runProjectInitCommand = true, workspace: workspaceOverride } = {}) {
|
|
676
676
|
const { values, positionals } = parseArgs({
|
|
677
677
|
args,
|
|
678
678
|
allowPositionals: true,
|
|
@@ -696,7 +696,13 @@ export async function create(args, { runProjectInitCommand = true } = {}) {
|
|
|
696
696
|
validateSelinuxDisableEnv();
|
|
697
697
|
const config = loadConfig();
|
|
698
698
|
const [branchOrTaskId = '', base] = positionals;
|
|
699
|
-
const
|
|
699
|
+
const resolvedTarget = resolveSandboxTarget(branchOrTaskId, config.repoRoot);
|
|
700
|
+
const workspace = workspaceOverride?.mode === 'task-bound'
|
|
701
|
+
&& resolvedTarget.workspace.mode === 'task-bound'
|
|
702
|
+
&& workspaceOverride.taskId === resolvedTarget.workspace.taskId
|
|
703
|
+
? resolvedTarget.workspace
|
|
704
|
+
: workspaceOverride ?? resolvedTarget.workspace;
|
|
705
|
+
const target = { ...resolvedTarget, workspace };
|
|
700
706
|
const branch = target.branch;
|
|
701
707
|
assertValidBranchName(branch);
|
|
702
708
|
const effectiveConfig = {
|
|
@@ -866,6 +872,9 @@ export async function create(args, { runProjectInitCommand = true } = {}) {
|
|
|
866
872
|
task: async () => {
|
|
867
873
|
for (const { tool, dir } of effectiveResolvedTools) {
|
|
868
874
|
fs.mkdirSync(dir, { recursive: true });
|
|
875
|
+
for (const { hostSubdir } of tool.hostStateMounts ?? []) {
|
|
876
|
+
fs.mkdirSync(path.join(dir, hostSubdir), { recursive: true });
|
|
877
|
+
}
|
|
869
878
|
for (const { hostPath, sandboxName } of tool.hostPreSeedFiles ?? []) {
|
|
870
879
|
const destination = path.join(dir, sandboxName);
|
|
871
880
|
if (fs.existsSync(hostPath) && !fs.existsSync(destination)) {
|
|
@@ -1105,6 +1114,10 @@ export async function create(args, { runProjectInitCommand = true } = {}) {
|
|
|
1105
1114
|
'-v',
|
|
1106
1115
|
volumeArg(engine, hostPath, path.posix.join(tool.containerMount, containerSubpath))
|
|
1107
1116
|
]));
|
|
1117
|
+
const hostStateMountVolumes = effectiveResolvedTools.flatMap(({ tool, dir }) => (tool.hostStateMounts ?? []).flatMap(({ hostSubdir, containerSubpath }) => [
|
|
1118
|
+
'-v',
|
|
1119
|
+
volumeArg(engine, path.join(dir, hostSubdir), path.posix.join(tool.containerMount, containerSubpath))
|
|
1120
|
+
]));
|
|
1108
1121
|
fs.mkdirSync(shareCommon, { recursive: true });
|
|
1109
1122
|
fs.mkdirSync(shareBranch, { recursive: true });
|
|
1110
1123
|
fs.mkdirSync(clipboardHostDir(effectiveConfig.home), { recursive: true, mode: 0o700 });
|
|
@@ -1133,6 +1146,8 @@ export async function create(args, { runProjectInitCommand = true } = {}) {
|
|
|
1133
1146
|
`${sandboxBranchLabel(effectiveConfig)}=${branch}`,
|
|
1134
1147
|
'--label',
|
|
1135
1148
|
`${sandboxWorkspaceModeLabel(effectiveConfig)}=${target.workspace.mode}`,
|
|
1149
|
+
'-e',
|
|
1150
|
+
'AGENT_INFRA_SANDBOX=1',
|
|
1136
1151
|
...(target.workspace.mode === 'task-bound'
|
|
1137
1152
|
? [
|
|
1138
1153
|
'--label',
|
|
@@ -1163,6 +1178,7 @@ export async function create(args, { runProjectInitCommand = true } = {}) {
|
|
|
1163
1178
|
...tmpfsArgs,
|
|
1164
1179
|
...tmpfsSeedPlan.flatMap(({ volumeArgs }) => volumeArgs),
|
|
1165
1180
|
...liveMountVolumes,
|
|
1181
|
+
...hostStateMountVolumes,
|
|
1166
1182
|
...envFile.dockerArgs,
|
|
1167
1183
|
...tzFlags,
|
|
1168
1184
|
'-w',
|
|
@@ -8,15 +8,16 @@ import { dotfilesCacheDir, materializeDotfiles } from '../dotfiles.js';
|
|
|
8
8
|
import { redactCommandError } from '../redaction.js';
|
|
9
9
|
import { runInteractiveWithClipboardBridge } from '../clipboard/bridge.js';
|
|
10
10
|
import { detectHostTimezone } from '../host-timezone.js';
|
|
11
|
-
import { fetchSandboxRows, selectSandboxContainer, } from './list-running.js';
|
|
12
|
-
import {
|
|
11
|
+
import { fetchSandboxRows, selectSandboxContainer, startSandboxContainer, } from './list-running.js';
|
|
12
|
+
import { resolveSandboxTarget } from '../workspace-identity.js';
|
|
13
13
|
const USAGE = `Usage: ai sandbox exec [--recreate] <branch | TASK-id | N> [cmd...]
|
|
14
14
|
|
|
15
15
|
N references an active task short id from
|
|
16
16
|
.agents/workspace/active/.short-ids.json. They resolve only via that
|
|
17
17
|
registry — they do not reference a container's row position in
|
|
18
|
-
'ai sandbox ls' output. --recreate
|
|
19
|
-
|
|
18
|
+
'ai sandbox ls' output. --recreate rebuilds the matched container while
|
|
19
|
+
preserving its task or branch identity. The same token after the target is
|
|
20
|
+
passed to the container command.`;
|
|
20
21
|
const TMUX_ENTRY_PATH = '/usr/local/bin/sandbox-tmux-entry';
|
|
21
22
|
// Terminal-detection variables that interactive TUIs (e.g. claude-code)
|
|
22
23
|
// inspect to enable progressive enhancements such as the kitty keyboard
|
|
@@ -78,41 +79,54 @@ export async function enter(args) {
|
|
|
78
79
|
const config = loadConfig();
|
|
79
80
|
const engine = detectEngine(config);
|
|
80
81
|
const parsed = parseEnterArgs(args);
|
|
81
|
-
const
|
|
82
|
-
const
|
|
83
|
-
|
|
82
|
+
const fullTaskId = /^TASK-\d{8}-\d{6}$/.test(parsed.target);
|
|
83
|
+
const target = fullTaskId ? null : resolveSandboxTarget(parsed.target, config.repoRoot);
|
|
84
|
+
const branch = target?.branch ?? '';
|
|
85
|
+
if (branch)
|
|
86
|
+
assertValidBranchName(branch);
|
|
84
87
|
const { running, nonRunning } = fetchSandboxRows(engine, sandboxLabel(config), sandboxBranchLabel(config), {
|
|
85
88
|
mode: sandboxWorkspaceModeLabel(config),
|
|
86
89
|
taskId: sandboxTaskIdLabel(config)
|
|
87
90
|
});
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
+
const rows = [...running, ...nonRunning];
|
|
92
|
+
const found = fullTaskId
|
|
93
|
+
? rows.find((row) => row.taskId === parsed.target)
|
|
94
|
+
: rows.find((row) => row.branch === branch)
|
|
95
|
+
?? selectSandboxContainer(rows, containerNameCandidates(config, branch));
|
|
96
|
+
if (!found)
|
|
97
|
+
throw new Error(`No sandbox found for '${parsed.target}'.`);
|
|
98
|
+
const sandboxBranch = found.branch || branch;
|
|
99
|
+
if (sandboxBranch)
|
|
100
|
+
assertValidBranchName(sandboxBranch);
|
|
101
|
+
if (target?.workspace.mode === 'task-bound' && found.taskId !== target.workspace.taskId) {
|
|
102
|
+
throw new Error(`Sandbox task mismatch for short id '${parsed.target}'.`);
|
|
91
103
|
}
|
|
92
|
-
const
|
|
104
|
+
const workspace = found.workspaceMode === 'task-bound' && found.taskId
|
|
93
105
|
? { mode: 'task-bound', taskId: found.taskId }
|
|
94
|
-
:
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
115
|
-
|
|
106
|
+
: { mode: 'branch-only' };
|
|
107
|
+
let container = found.name;
|
|
108
|
+
if (parsed.recreate) {
|
|
109
|
+
const ready = await ensureSandboxReady({
|
|
110
|
+
config,
|
|
111
|
+
engine,
|
|
112
|
+
branch: sandboxBranch,
|
|
113
|
+
workspace,
|
|
114
|
+
row: found,
|
|
115
|
+
allowRecreate: true,
|
|
116
|
+
forceRecreate: true,
|
|
117
|
+
recreate: async () => {
|
|
118
|
+
const { create } = await import('./create.js');
|
|
119
|
+
await create([sandboxBranch, '--no-refresh'], {
|
|
120
|
+
runProjectInitCommand: false,
|
|
121
|
+
workspace
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
container = ready.container;
|
|
126
|
+
}
|
|
127
|
+
else if (!found.running) {
|
|
128
|
+
startSandboxContainer(engine, found.name);
|
|
129
|
+
}
|
|
116
130
|
const cmd = parsed.command;
|
|
117
131
|
const capabilityPlan = createSandboxCapabilityPlan(config);
|
|
118
132
|
const enterHookResults = await runSandboxHooks({
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
-
import { hasTaskBoundMarker } from '../../internal/task-operation-registry.js';
|
|
3
2
|
import path from 'node:path';
|
|
4
3
|
import { randomUUID } from 'node:crypto';
|
|
5
4
|
import { SANDBOX_CONTROL_ADMISSION_WINDOW_MS, SANDBOX_CONTROL_MAX_BYTES, SANDBOX_CONTROL_STATUS_STALE_MS, isSandboxControlFamily } from './protocol.js';
|
|
@@ -8,6 +7,9 @@ import { readSandboxControlPayload, readSandboxControlStatus } from './state.js'
|
|
|
8
7
|
import { accessSandboxTaskView, taskViewFromStatus } from './task-view.js';
|
|
9
8
|
import { readSandboxControlIdentitySentinel } from './identity-sentinel.js';
|
|
10
9
|
import { configuredShortIdLength, resolveShortIdReadOnly } from '../../task/short-id.js';
|
|
10
|
+
import { hasTaskBoundMarker, isSandbox } from '../environment.js';
|
|
11
|
+
import { loadConfig } from '../config.js';
|
|
12
|
+
import { recoverSandboxControlFromHost } from './host-recovery.js';
|
|
11
13
|
const SANDBOX_CONTROL_RESPONSE_SETTLE_MS = 250;
|
|
12
14
|
const SANDBOX_TASK_FINALIZATION_RECOVERY_BUDGET_MS = 5 * 60_000;
|
|
13
15
|
export class SandboxControlClientError extends Error {
|
|
@@ -98,7 +100,7 @@ function parseResponse(raw, id, accepted = false) {
|
|
|
98
100
|
}
|
|
99
101
|
return response;
|
|
100
102
|
}
|
|
101
|
-
function readPublishedResponse(raw, id, channelDir, accepted
|
|
103
|
+
function readPublishedResponse(raw, id, channelDir, accepted, generation) {
|
|
102
104
|
const response = parseResponse(raw, id, accepted);
|
|
103
105
|
if (response.outputState !== 'available')
|
|
104
106
|
return response;
|
|
@@ -112,6 +114,7 @@ function readPublishedResponse(raw, id, channelDir, accepted = false) {
|
|
|
112
114
|
}
|
|
113
115
|
if (!response.payload || response.payload.version !== payload.version
|
|
114
116
|
|| response.payload.id !== payload.id || response.payload.generation !== payload.generation
|
|
117
|
+
|| payload.generation !== generation
|
|
115
118
|
|| response.payload.stdoutBytes !== payload.stdoutBytes || response.payload.stderrBytes !== payload.stderrBytes
|
|
116
119
|
|| response.payload.stdoutSha256 !== payload.stdoutSha256 || response.payload.stderrSha256 !== payload.stderrSha256) {
|
|
117
120
|
clientError('SANDBOX_CONTROL_RESPONSE_INVALID', 'broker payload reference is invalid', false, true, id);
|
|
@@ -216,7 +219,7 @@ function exchangeSandboxControl(request, params) {
|
|
|
216
219
|
try {
|
|
217
220
|
// The request-specific terminal proves the broker claimed the request; preserve recovery evidence if marker cleanup won the race.
|
|
218
221
|
accepted = true;
|
|
219
|
-
response = readPublishedResponse(raw, request.id, channelDir, accepted);
|
|
222
|
+
response = readPublishedResponse(raw, request.id, channelDir, accepted, request.generation);
|
|
220
223
|
malformedResponseRaw = null;
|
|
221
224
|
}
|
|
222
225
|
catch (error) {
|
|
@@ -264,7 +267,40 @@ export function recoverSandboxControl(requestId, params = {}) {
|
|
|
264
267
|
if (!/^[a-f0-9-]{16,64}$/u.test(requestId)) {
|
|
265
268
|
clientError('SANDBOX_CONTROL_REQUEST_INVALID', 'request id is invalid', false, true);
|
|
266
269
|
}
|
|
267
|
-
|
|
270
|
+
try {
|
|
271
|
+
if (!isSandbox()) {
|
|
272
|
+
const config = loadConfig();
|
|
273
|
+
const response = recoverSandboxControlFromHost(requestId, {
|
|
274
|
+
managedRoot: path.join(config.controlBase, config.project),
|
|
275
|
+
timeoutMs: params.timeoutMs
|
|
276
|
+
});
|
|
277
|
+
if (response.phase === 'rejected' && response.error?.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN') {
|
|
278
|
+
clientError('SANDBOX_CONTROL_RESULT_UNKNOWN', 'recovery evidence is unavailable; inspect domain state before retrying', false, true, requestId);
|
|
279
|
+
}
|
|
280
|
+
return response;
|
|
281
|
+
}
|
|
282
|
+
const channelDir = process.env.AGENT_INFRA_CONTROL_DIR;
|
|
283
|
+
const statusDir = process.env.AGENT_INFRA_CONTROL_STATUS_DIR;
|
|
284
|
+
const identity = readSandboxControlIdentitySentinel(statusDir);
|
|
285
|
+
return recoverSandboxControlFromChannel(requestId, {
|
|
286
|
+
channelDir, generation: identity.generation, timeoutMs: params.timeoutMs
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
if (error instanceof SandboxControlClientError)
|
|
291
|
+
throw error;
|
|
292
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
293
|
+
const code = /^([A-Z][A-Z0-9_]+)/u.exec(message)?.[1] ?? 'SANDBOX_CONTROL_RECOVERY_INVALID';
|
|
294
|
+
clientError(code, 'sandbox control recovery evidence is invalid or unavailable', false, true, requestId);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
// Low-level channel reader for control integration tests and internal callers
|
|
298
|
+
// that already hold a manifest-bound channel path.
|
|
299
|
+
export function recoverSandboxControlFromChannel(requestId, params) {
|
|
300
|
+
if (!/^[a-f0-9-]{16,64}$/u.test(requestId)) {
|
|
301
|
+
clientError('SANDBOX_CONTROL_REQUEST_INVALID', 'request id is invalid', false, true);
|
|
302
|
+
}
|
|
303
|
+
const { channelDir } = params;
|
|
268
304
|
const responsePath = path.join(channelDir, 'responses', `${requestId}.json`);
|
|
269
305
|
const deadline = Date.now() + (params.timeoutMs ?? 30_000);
|
|
270
306
|
let accepted = false;
|
|
@@ -287,7 +323,7 @@ export function recoverSandboxControl(requestId, params = {}) {
|
|
|
287
323
|
try {
|
|
288
324
|
// The request-specific terminal proves the broker claimed the request; preserve recovery evidence if marker cleanup won the race.
|
|
289
325
|
accepted = true;
|
|
290
|
-
response = readPublishedResponse(raw, requestId, channelDir, accepted);
|
|
326
|
+
response = readPublishedResponse(raw, requestId, channelDir, accepted, params.generation);
|
|
291
327
|
malformedResponseRaw = null;
|
|
292
328
|
}
|
|
293
329
|
catch (error) {
|
|
@@ -338,7 +374,7 @@ export function requestSandboxControl(params) {
|
|
|
338
374
|
const auth = authority(params);
|
|
339
375
|
const issuedAt = Date.now();
|
|
340
376
|
const request = {
|
|
341
|
-
version:
|
|
377
|
+
version: 4, id: randomUUID(), ...auth, issuedAt,
|
|
342
378
|
expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
|
|
343
379
|
family: params.family, args: params.args,
|
|
344
380
|
controllerProcess: null,
|
|
@@ -353,9 +389,8 @@ export function requestSandboxTaskFinalization(params) {
|
|
|
353
389
|
const auth = authority(params);
|
|
354
390
|
const issuedAt = Date.now();
|
|
355
391
|
const id = randomUUID();
|
|
356
|
-
const handoffSha256 = (params.prepareHandoff ?? (() => '0'.repeat(64)))({ generation: auth.generation, requestId: id });
|
|
357
392
|
const request = {
|
|
358
|
-
version:
|
|
393
|
+
version: 4,
|
|
359
394
|
id,
|
|
360
395
|
...auth,
|
|
361
396
|
issuedAt,
|
|
@@ -363,7 +398,6 @@ export function requestSandboxTaskFinalization(params) {
|
|
|
363
398
|
family: 'task-finalization',
|
|
364
399
|
operation: 'complete',
|
|
365
400
|
agent,
|
|
366
|
-
handoffSha256,
|
|
367
401
|
args: [],
|
|
368
402
|
controllerProcess: null,
|
|
369
403
|
controllerProof: null
|
|
@@ -391,7 +425,7 @@ export function recoverAcceptedTaskFinalization(requestId, params, recover = rec
|
|
|
391
425
|
if (timeoutMs <= 0)
|
|
392
426
|
break;
|
|
393
427
|
try {
|
|
394
|
-
const response = recover(requestId, {
|
|
428
|
+
const response = recover(requestId, { timeoutMs });
|
|
395
429
|
if (response.phase === 'rejected' && response.error?.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN')
|
|
396
430
|
continue;
|
|
397
431
|
return response;
|
|
@@ -504,7 +538,7 @@ function requestCodexController(params) {
|
|
|
504
538
|
const auth = authority(params);
|
|
505
539
|
const issuedAt = Date.now();
|
|
506
540
|
const request = {
|
|
507
|
-
version:
|
|
541
|
+
version: 4,
|
|
508
542
|
id: randomUUID(),
|
|
509
543
|
...auth,
|
|
510
544
|
issuedAt,
|
|
@@ -551,7 +585,7 @@ export function requestSandboxTaskCreate(params) {
|
|
|
551
585
|
const auth = authority(params);
|
|
552
586
|
const issuedAt = Date.now();
|
|
553
587
|
const request = {
|
|
554
|
-
version:
|
|
588
|
+
version: 4, id: randomUUID(), ...auth, issuedAt,
|
|
555
589
|
expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
|
|
556
590
|
family: 'task-create', candidate: params.candidate,
|
|
557
591
|
controllerProcess: null,
|
|
@@ -2,7 +2,6 @@ import { spawn } from 'node:child_process';
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { randomUUID } from 'node:crypto';
|
|
5
|
-
import { homedir } from 'node:os';
|
|
6
5
|
import { getProcessStartTime } from '../../server/process-state.js';
|
|
7
6
|
import { createTask, projectTaskCreateResult, taskCreateExitCode, taskCreateFailure } from '../../task/create-service.js';
|
|
8
7
|
import { applyTaskFinalization } from '../../task/finalization.js';
|
|
@@ -15,10 +14,6 @@ import { createSandboxExecutorExecutionContext, dispatchTaskControlOperation, pa
|
|
|
15
14
|
import { assertSandboxControlBrokerOwner, readSandboxControlManifest } from './lifecycle.js';
|
|
16
15
|
import { computeLifecycleBuildIdentity } from '../../agent-clients/adapters/codex-lifecycle/build-identity.js';
|
|
17
16
|
import { closeCodexControllerRegistration, CodexControllerRegistrationError, openCodexControllerRegistration, resolveCodexControllerBinding } from './controller-registration.js';
|
|
18
|
-
import { shareBranchDir } from '../constants.js';
|
|
19
|
-
function handoffDirectory(manifest) {
|
|
20
|
-
return shareBranchDir({ shareBase: path.join(homedir(), '.agent-infra', 'share', manifest.project) }, manifest.branch);
|
|
21
|
-
}
|
|
22
17
|
function sameBrokerOwner(left, right) {
|
|
23
18
|
return left.version === right.version
|
|
24
19
|
&& left.pid === right.pid
|
|
@@ -233,7 +228,7 @@ function finalizationResult(result) {
|
|
|
233
228
|
async function executeRequestInner(manifest, manifestPath, request, options = {}) {
|
|
234
229
|
if (request.family === 'task-create') {
|
|
235
230
|
try {
|
|
236
|
-
const result = await createTask(request.candidate, { repoRoot: manifest.repoRoot });
|
|
231
|
+
const result = await (options.createTask ?? createTask)(request.candidate, { repoRoot: manifest.repoRoot });
|
|
237
232
|
return taskCreateExecutionResult(projectTaskCreateResult(result, {
|
|
238
233
|
requestId: request.id,
|
|
239
234
|
accepted: true,
|
|
@@ -304,7 +299,7 @@ async function executeRequestInner(manifest, manifestPath, request, options = {}
|
|
|
304
299
|
const parsed = parseTaskControlOperation('task-finalization', [manifest.taskId, 'complete', '--agent', request.agent]);
|
|
305
300
|
if (parsed.family !== 'task-finalization')
|
|
306
301
|
throw new Error('SANDBOX_CONTROL_FINALIZATION_OPERATION_INVALID');
|
|
307
|
-
const operation =
|
|
302
|
+
const operation = parsed;
|
|
308
303
|
const result = dispatchTaskControlOperation(createSandboxExecutorExecutionContext({
|
|
309
304
|
repoRoot: manifest.repoRoot,
|
|
310
305
|
worktreeRoot: manifest.worktreeRoot,
|
|
@@ -312,8 +307,7 @@ async function executeRequestInner(manifest, manifestPath, request, options = {}
|
|
|
312
307
|
taskId: manifest.taskId,
|
|
313
308
|
generation: manifest.generation,
|
|
314
309
|
manifestPath,
|
|
315
|
-
requestId: request.id
|
|
316
|
-
handoffDirectory: handoffDirectory(manifest)
|
|
310
|
+
requestId: request.id
|
|
317
311
|
}), operation);
|
|
318
312
|
return finalizationResult(await result);
|
|
319
313
|
}
|
|
@@ -334,8 +328,7 @@ async function executeRequestInner(manifest, manifestPath, request, options = {}
|
|
|
334
328
|
taskId: manifest.taskId,
|
|
335
329
|
generation: manifest.generation,
|
|
336
330
|
manifestPath,
|
|
337
|
-
requestId: request.id
|
|
338
|
-
handoffDirectory: handoffDirectory(manifest)
|
|
331
|
+
requestId: request.id
|
|
339
332
|
});
|
|
340
333
|
const format = (value) => {
|
|
341
334
|
const result = value;
|