@fitlab-ai/agent-infra 0.11.13 → 0.11.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/internal-cli.js +2 -7
- package/dist/lib/agent-clients/adapter.js +0 -3
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +5 -9
- package/dist/lib/agent-clients/adapters/codex-lifecycle/cli.js +1 -5
- package/dist/lib/agent-clients/adapters/codex-lifecycle/recovery.js +1 -5
- package/dist/lib/agent-clients/adapters/codex-orchestration.js +1 -3
- package/dist/lib/agent-clients/adapters/codex.js +0 -2
- package/dist/lib/internal/cli-route-inventory.js +0 -1
- package/dist/lib/internal/task-finalization.js +7 -5
- package/dist/lib/internal/task-lifecycle.js +3 -1
- package/dist/lib/internal/task-operation-registry.js +1 -3
- package/dist/lib/internal/task-validate.js +14 -124
- package/dist/lib/platform/pr-change-report.js +10 -3
- package/dist/lib/platform/release-notes.js +2 -2
- package/dist/lib/process-data/index.js +15 -4
- package/dist/lib/sandbox/commands/control-status.js +0 -1
- package/dist/lib/sandbox/commands/create.js +5 -4
- package/dist/lib/sandbox/config.js +12 -0
- package/dist/lib/sandbox/control/audit.js +1 -2
- package/dist/lib/sandbox/control/client.js +12 -47
- package/dist/lib/sandbox/control/executor.js +6 -10
- package/dist/lib/sandbox/control/host-recovery.js +89 -27
- package/dist/lib/sandbox/control/lifecycle.js +22 -37
- package/dist/lib/sandbox/control/protocol.js +8 -36
- package/dist/lib/sandbox/control/server.js +2 -21
- package/dist/lib/sandbox/environment.js +7 -8
- package/dist/lib/sandbox/mounts.js +71 -59
- package/dist/lib/sandbox/recovery.js +2 -3
- package/dist/lib/sandbox/removal.js +19 -4
- package/dist/lib/sandbox/workspace-identity.js +30 -15
- package/dist/lib/sandbox/workspace-view.js +41 -4
- package/dist/lib/task/control-authority.js +39 -17
- package/dist/lib/task/control-recovery.js +1 -2
- package/dist/lib/task/finalization.js +13 -2
- package/dist/lib/task/lifecycle.js +122 -35
- package/dist/lib/task/short-id.js +2 -2
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapter.ts +0 -27
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +6 -12
- package/lib/agent-clients/adapters/codex-lifecycle/cli.ts +1 -5
- package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +0 -7
- package/lib/agent-clients/adapters/codex-lifecycle/recovery.ts +1 -5
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +1 -4
- package/lib/agent-clients/adapters/codex-orchestration.ts +1 -3
- package/lib/agent-clients/adapters/codex.ts +0 -2
- package/lib/internal/cli-route-inventory.ts +0 -1
- package/lib/internal/task-finalization.ts +8 -6
- package/lib/internal/task-lifecycle.ts +3 -1
- package/lib/internal/task-operation-registry.ts +1 -3
- package/lib/internal/task-validate.ts +13 -127
- package/lib/platform/pr-change-report.ts +9 -3
- package/lib/platform/release-notes.ts +2 -2
- package/lib/process-data/index.ts +15 -4
- package/lib/sandbox/commands/control-status.ts +0 -1
- package/lib/sandbox/commands/create.ts +5 -4
- package/lib/sandbox/config.ts +13 -0
- package/lib/sandbox/control/audit.ts +1 -2
- package/lib/sandbox/control/client.ts +13 -61
- package/lib/sandbox/control/executor.ts +7 -13
- package/lib/sandbox/control/host-recovery.ts +94 -21
- package/lib/sandbox/control/lifecycle.ts +22 -37
- package/lib/sandbox/control/protocol.ts +9 -45
- package/lib/sandbox/control/server.ts +2 -17
- package/lib/sandbox/environment.ts +7 -8
- package/lib/sandbox/mounts.ts +92 -75
- package/lib/sandbox/recovery.ts +2 -3
- package/lib/sandbox/removal.ts +16 -4
- package/lib/sandbox/workspace-identity.ts +36 -13
- package/lib/sandbox/workspace-view.ts +40 -5
- package/lib/task/control-authority.ts +40 -26
- package/lib/task/control-recovery.ts +2 -3
- package/lib/task/finalization.ts +15 -2
- package/lib/task/lifecycle.ts +135 -33
- package/lib/task/short-id.ts +5 -2
- package/package.json +1 -1
- package/templates/.agents/rules/lifecycle-orchestration.en.md +3 -4
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +3 -4
- package/templates/.agents/rules/task-short-id.en.md +18 -12
- package/templates/.agents/rules/task-short-id.zh-CN.md +12 -7
- package/templates/.agents/skills/complete-task/SKILL.en.md +10 -2
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +10 -2
- package/templates/.agents/skills/create-pr/reference/change-report.en.md +12 -2
- package/templates/.agents/skills/create-pr/reference/change-report.zh-CN.md +12 -2
- package/templates/.agents/skills/create-release-note/SKILL.en.md +3 -1
- package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +3 -1
- package/templates/.agents/skills/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/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/watch-pr/SKILL.en.md +19 -2
- package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +20 -3
- 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/dist/lib/agent-clients/adapters/codex-lifecycle/controller-client.js +0 -130
- package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-context.js +0 -127
- package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-operation.js +0 -131
- package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-registration.js +0 -275
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller-cli.js +0 -106
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +0 -345
- package/dist/lib/runtime/agent-runtime.js +0 -33
- package/dist/lib/sandbox/control/agent-operation.js +0 -34
- package/dist/lib/task/runtime-paths.js +0 -10
- package/lib/agent-clients/adapters/codex-lifecycle/controller-client.ts +0 -158
- package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +0 -163
- package/lib/agent-clients/adapters/codex-lifecycle/controller-operation.ts +0 -151
- package/lib/agent-clients/adapters/codex-lifecycle/controller-registration.ts +0 -352
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller-cli.ts +0 -109
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +0 -437
- package/lib/runtime/agent-runtime.ts +0 -41
- package/lib/sandbox/control/agent-operation.ts +0 -49
- package/lib/task/runtime-paths.ts +0 -12
|
@@ -2,27 +2,9 @@ import { execFileSync, spawnSync } from 'node:child_process';
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
-
import { randomUUID } from 'node:crypto';
|
|
6
5
|
import { loadConfig } from '../sandbox/config.ts';
|
|
7
|
-
import { detectEngine } from '../sandbox/engine.ts';
|
|
8
|
-
import { runEngine } from '../sandbox/shell.ts';
|
|
9
|
-
import { sandboxBranchLabel, sandboxLabel } from '../sandbox/constants.ts';
|
|
10
|
-
import { fetchSandboxRows, selectSandboxContainer } from '../sandbox/commands/list-running.ts';
|
|
11
|
-
import { containerNameCandidates } from '../sandbox/constants.ts';
|
|
12
6
|
import { resolveSandboxTarget } from '../sandbox/workspace-identity.ts';
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
appendSandboxControlAudit,
|
|
16
|
-
readActiveLease,
|
|
17
|
-
readSandboxControlStatus
|
|
18
|
-
} from '../sandbox/control/state.ts';
|
|
19
|
-
import type { SandboxControlLease } from '../sandbox/control/protocol.ts';
|
|
20
|
-
import { readSandboxControlManifest } from '../sandbox/control/lifecycle.ts';
|
|
21
|
-
import {
|
|
22
|
-
SANDBOX_CONTROL_FUTURE_SKEW_MS,
|
|
23
|
-
SANDBOX_CONTROL_STATUS_STALE_MS
|
|
24
|
-
} from '../sandbox/control/protocol.ts';
|
|
25
|
-
import { getProcessStartTime } from '../server/process-state.ts';
|
|
7
|
+
import { resolveBranchWorktree } from '../git/branch-worktree.ts';
|
|
26
8
|
import { assertGitWorktreeBinding } from '../git/worktree-identity.ts';
|
|
27
9
|
import { createManualValidationEvidence, manualValidationEvidenceSummary } from '../task/manual-validation-evidence.ts';
|
|
28
10
|
import type { ManualValidationCleanup } from '../task/manual-validation-evidence.ts';
|
|
@@ -31,7 +13,6 @@ import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-in
|
|
|
31
13
|
|
|
32
14
|
const USAGE = `Usage: agent-infra-internal task-validate <branch | TASK-id | N> [--scope snapshot|inplace] [--timeout <ms>] [--format text|json] [--evidence-file <path>] -- <command> [args...]`;
|
|
33
15
|
const MAX_TIMEOUT_MS = 60 * 60 * 1000;
|
|
34
|
-
const RECOVERY_GRACE_MS = 30_000;
|
|
35
16
|
|
|
36
17
|
type ValidateOptions = {
|
|
37
18
|
target: string;
|
|
@@ -108,20 +89,6 @@ function git(root: string, args: string[]): string {
|
|
|
108
89
|
return execFileSync('git', args, { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
|
|
109
90
|
}
|
|
110
91
|
|
|
111
|
-
function waitForState(statusDir: string, predicate: (state: ReturnType<typeof readSandboxControlStatus>) => boolean, timeoutMs = 5_000): void {
|
|
112
|
-
const deadline = Date.now() + timeoutMs;
|
|
113
|
-
while (Date.now() < deadline) {
|
|
114
|
-
try {
|
|
115
|
-
const status = readSandboxControlStatus(statusDir);
|
|
116
|
-
if (predicate(status)) return;
|
|
117
|
-
} catch {
|
|
118
|
-
// Atomic status publication may be between renames.
|
|
119
|
-
}
|
|
120
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
|
|
121
|
-
}
|
|
122
|
-
throw new Error('SANDBOX_VALIDATION_BROKER_STATE_TIMEOUT');
|
|
123
|
-
}
|
|
124
|
-
|
|
125
92
|
function snapshotValidation(repoRoot: string, commit: string, options: ValidateOptions) {
|
|
126
93
|
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-infra-validation-'));
|
|
127
94
|
const worktree = path.join(tempRoot, 'worktree');
|
|
@@ -150,109 +117,28 @@ function snapshotValidation(repoRoot: string, commit: string, options: ValidateO
|
|
|
150
117
|
}
|
|
151
118
|
|
|
152
119
|
function inplaceValidation(config: ReturnType<typeof loadConfig>, target: ReturnType<typeof resolveSandboxTarget>, options: ValidateOptions) {
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
const
|
|
158
|
-
base: config.controlBase, repoRoot: config.repoRoot, project: config.project,
|
|
159
|
-
container: row.name, identity: target.workspace
|
|
160
|
-
});
|
|
161
|
-
const manifest = readSandboxControlManifest(control.manifestPath);
|
|
162
|
-
const initial = readSandboxControlStatus(control.statusDir);
|
|
163
|
-
const now = Date.now();
|
|
164
|
-
if (initial.generation !== manifest.generation
|
|
165
|
-
|| now - initial.updatedAt > SANDBOX_CONTROL_STATUS_STALE_MS
|
|
166
|
-
|| initial.updatedAt > now + SANDBOX_CONTROL_FUTURE_SKEW_MS) {
|
|
167
|
-
throw new Error('SANDBOX_VALIDATION_BROKER_STATUS_STALE');
|
|
168
|
-
}
|
|
169
|
-
if (initial.state !== 'healthy' || initial.activeRequestId !== null) {
|
|
170
|
-
throw new Error('SANDBOX_VALIDATION_BROKER_NOT_IDLE');
|
|
171
|
-
}
|
|
172
|
-
const startTime = getProcessStartTime(process.pid);
|
|
173
|
-
if (!startTime) throw new Error('SANDBOX_VALIDATION_OWNER_IDENTITY_UNAVAILABLE');
|
|
174
|
-
const leasePath = path.join(control.root, 'lease.json');
|
|
175
|
-
const nonce = randomUUID();
|
|
176
|
-
const issuedAt = Date.now();
|
|
177
|
-
const lease: SandboxControlLease = {
|
|
178
|
-
version: 2, generation: manifest.generation, nonce,
|
|
179
|
-
owner: { pid: process.pid, startTime }, issuedAt,
|
|
180
|
-
expiresAt: issuedAt + Math.min(options.timeoutMs + RECOVERY_GRACE_MS, MAX_TIMEOUT_MS + RECOVERY_GRACE_MS),
|
|
181
|
-
taskId: target.workspace.mode === 'task-bound' ? target.workspace.taskId : null,
|
|
182
|
-
branch: target.branch, reason: 'manual-validation'
|
|
183
|
-
};
|
|
184
|
-
const originalBranch = git(manifest.worktreeRoot, ['symbolic-ref', '--quiet', '--short', 'HEAD']);
|
|
185
|
-
const originalHead = git(manifest.worktreeRoot, ['rev-parse', 'HEAD']);
|
|
186
|
-
fs.writeFileSync(leasePath, `${JSON.stringify(lease)}\n`, { flag: 'wx', mode: 0o600 });
|
|
187
|
-
let stopped = false;
|
|
120
|
+
const targetRoot = resolveBranchWorktree(config.repoRoot, target.branch);
|
|
121
|
+
if (!targetRoot) throw new Error(`TASK_VALIDATION_WORKTREE_NOT_FOUND: ${target.branch}`);
|
|
122
|
+
const worktree = assertGitWorktreeBinding(config.repoRoot, targetRoot, target.branch);
|
|
123
|
+
const originalBranch = git(worktree.worktreeRoot, ['symbolic-ref', '--quiet', '--short', 'HEAD']);
|
|
124
|
+
const originalHead = git(worktree.worktreeRoot, ['rev-parse', 'HEAD']);
|
|
188
125
|
let restored = false;
|
|
189
126
|
try {
|
|
190
|
-
|
|
191
|
-
waitForState(control.statusDir, (status) => status.state === 'parked' && status.reasonCode === 'SANDBOX_CONTROL_HANDOFF_ACTIVE');
|
|
192
|
-
if (row.running) {
|
|
193
|
-
runEngine(engine, 'docker', ['stop', row.name]);
|
|
194
|
-
stopped = true;
|
|
195
|
-
}
|
|
196
|
-
const refreshedRows = fetchSandboxRows(engine, sandboxLabel(config), sandboxBranchLabel(config));
|
|
197
|
-
const refreshed = selectSandboxContainer(
|
|
198
|
-
[...refreshedRows.running, ...refreshedRows.nonRunning],
|
|
199
|
-
containerNameCandidates(config, target.branch)
|
|
200
|
-
);
|
|
201
|
-
if (!refreshed || refreshed.running) throw new Error('SANDBOX_VALIDATION_CONTAINER_STILL_RUNNING');
|
|
202
|
-
const activeLease = readActiveLease(manifest);
|
|
203
|
-
if (!activeLease || activeLease.nonce !== nonce) throw new Error('SANDBOX_VALIDATION_LEASE_OWNERSHIP_LOST');
|
|
204
|
-
assertGitWorktreeBinding(config.repoRoot, manifest.worktreeRoot, target.branch);
|
|
205
|
-
const result = runValidationCommand(options.command, manifest.worktreeRoot, 'inplace', options.timeoutMs, options.format === 'json');
|
|
127
|
+
const result = runValidationCommand(options.command, worktree.worktreeRoot, 'inplace', options.timeoutMs, options.format === 'json');
|
|
206
128
|
return { ...result, cleanup: () => restored ? 'completed' : 'pending' };
|
|
207
129
|
} finally {
|
|
208
|
-
const recoveryErrors: string[] = [];
|
|
209
130
|
try {
|
|
210
|
-
const currentBranch = git(
|
|
211
|
-
if (currentBranch !== originalBranch) git(
|
|
131
|
+
const currentBranch = git(worktree.worktreeRoot, ['symbolic-ref', '--quiet', '--short', 'HEAD']);
|
|
132
|
+
if (currentBranch !== originalBranch) git(worktree.worktreeRoot, ['switch', originalBranch]);
|
|
212
133
|
} catch {
|
|
213
134
|
try {
|
|
214
|
-
git(
|
|
215
|
-
git(
|
|
216
|
-
} catch (fallbackError) {
|
|
217
|
-
recoveryErrors.push(`git binding restore failed: ${fallbackError instanceof Error ? fallbackError.message : String(fallbackError)}`);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
if (stopped) {
|
|
221
|
-
try {
|
|
222
|
-
runEngine(engine, 'docker', ['start', row.name]);
|
|
223
|
-
} catch (error) {
|
|
224
|
-
recoveryErrors.push(`container start failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
try {
|
|
228
|
-
const currentLease = JSON.parse(fs.readFileSync(leasePath, 'utf8')) as { nonce?: unknown };
|
|
229
|
-
if (currentLease.nonce !== nonce) throw new Error('SANDBOX_VALIDATION_LEASE_OWNERSHIP_LOST');
|
|
230
|
-
} catch (error) {
|
|
231
|
-
recoveryErrors.push(error instanceof Error ? error.message : String(error));
|
|
232
|
-
}
|
|
233
|
-
if (recoveryErrors.length === 0) {
|
|
234
|
-
try {
|
|
235
|
-
fs.unlinkSync(leasePath);
|
|
236
|
-
appendSandboxControlAudit(manifest, 'lease-release', { ownerPid: process.pid });
|
|
237
|
-
waitForState(control.statusDir, (status) => status.state === 'healthy');
|
|
238
|
-
restored = true;
|
|
135
|
+
git(worktree.worktreeRoot, ['checkout', '--detach', originalHead]);
|
|
136
|
+
git(worktree.worktreeRoot, ['switch', originalBranch]);
|
|
239
137
|
} catch (error) {
|
|
240
|
-
|
|
241
|
-
if (!fs.existsSync(leasePath)) {
|
|
242
|
-
fs.writeFileSync(leasePath, `${JSON.stringify(lease)}\n`, { flag: 'wx', mode: 0o600 });
|
|
243
|
-
}
|
|
244
|
-
appendSandboxControlAudit(manifest, 'lease-release-failed', { ownerPid: process.pid });
|
|
245
|
-
} catch (leaseError) {
|
|
246
|
-
recoveryErrors.push(`lease preservation failed: ${leaseError instanceof Error ? leaseError.message : String(leaseError)}`);
|
|
247
|
-
}
|
|
248
|
-
recoveryErrors.push(error instanceof Error ? error.message : String(error));
|
|
138
|
+
throw new Error(`TASK_VALIDATION_WORKTREE_RESTORE_FAILED: ${error instanceof Error ? error.message : String(error)}`);
|
|
249
139
|
}
|
|
250
140
|
}
|
|
251
|
-
|
|
252
|
-
throw new Error(
|
|
253
|
-
`SANDBOX_VALIDATION_RECOVERY_INCOMPLETE: ${recoveryErrors.join('; ')}; run 'ai sandbox start ${target.branch}'`
|
|
254
|
-
);
|
|
255
|
-
}
|
|
141
|
+
restored = true;
|
|
256
142
|
}
|
|
257
143
|
}
|
|
258
144
|
|
|
@@ -429,12 +429,15 @@ function runMechanicalChangeReport(repoRoot: string, baseSha: string, headSha: s
|
|
|
429
429
|
return validated.value;
|
|
430
430
|
}
|
|
431
431
|
|
|
432
|
+
const DOCUMENTATION_CATEGORY = { label: '文档', prefixes: ['docs/'] } as const;
|
|
433
|
+
|
|
432
434
|
const CHANGE_CATEGORIES = [
|
|
433
435
|
{ label: '运行时代码', prefixes: ['bin/', 'lib/', 'src/', 'scripts/'] },
|
|
434
436
|
{ label: '测试与校验', prefixes: ['test/', 'tests/'] },
|
|
435
437
|
{ label: '模板与生成内容', prefixes: ['templates/'] },
|
|
436
438
|
{ label: '技能与协作配置', prefixes: ['.agents/'] },
|
|
437
|
-
|
|
439
|
+
DOCUMENTATION_CATEGORY,
|
|
440
|
+
{ label: '其他', prefixes: [] }
|
|
438
441
|
] as const;
|
|
439
442
|
|
|
440
443
|
function emptyChangeTotals(): ChangeTotals {
|
|
@@ -443,7 +446,10 @@ function emptyChangeTotals(): ChangeTotals {
|
|
|
443
446
|
|
|
444
447
|
function categoryForChangeFile(file: ChangeFile): (typeof CHANGE_CATEGORIES)[number] {
|
|
445
448
|
const filePath = file.newPath || file.oldPath || '';
|
|
446
|
-
|
|
449
|
+
const category = CHANGE_CATEGORIES.find((entry) => entry.prefixes.some((prefix) => prefix && filePath.startsWith(prefix)));
|
|
450
|
+
if (category) return category;
|
|
451
|
+
if (/\.(?:md|mdx|markdown|rst)$/i.test(filePath)) return DOCUMENTATION_CATEGORY;
|
|
452
|
+
return CHANGE_CATEGORIES[CHANGE_CATEGORIES.length - 1]!;
|
|
447
453
|
}
|
|
448
454
|
|
|
449
455
|
function summarizeChangeCategories(files: ChangeFile[]): Array<{ label: string; totals: ChangeTotals }> {
|
|
@@ -553,7 +559,7 @@ function renderEvidence(evidence: Evidence): string {
|
|
|
553
559
|
function renderPrechecks(checks: Precheck[]): string[] {
|
|
554
560
|
return checks.flatMap((check) => [
|
|
555
561
|
`- **${PRECHECK_LABELS[check.id]}(${check.id})**:${check.verdict === 'pass' ? '通过' : '需复核'}。${escapeReportText(check.rationale)}`,
|
|
556
|
-
...check.evidence.
|
|
562
|
+
...check.evidence.map(renderEvidence)
|
|
557
563
|
]);
|
|
558
564
|
}
|
|
559
565
|
|
|
@@ -136,8 +136,8 @@ async function releaseNoteContext(
|
|
|
136
136
|
options: ReleaseNoteOptions = {}
|
|
137
137
|
) {
|
|
138
138
|
const historyLimit = input.historyLimit ?? 3;
|
|
139
|
-
if (!input.fromTag || !input.toTag || !input.branch || !Number.isInteger(historyLimit) || historyLimit < 1 || historyLimit >
|
|
140
|
-
return { ...baseResult('failed'), error: { code: 'RELEASE_NOTES_INPUT_INVALID', message: 'from-tag, to-tag, branch, and a history limit from 1 to
|
|
139
|
+
if (!input.fromTag || !input.toTag || !input.branch || !Number.isInteger(historyLimit) || historyLimit < 1 || historyLimit > 100) {
|
|
140
|
+
return { ...baseResult('failed'), error: { code: 'RELEASE_NOTES_INPUT_INVALID', message: 'from-tag, to-tag, branch, and a history limit from 1 to 100 are required', retryable: false } };
|
|
141
141
|
}
|
|
142
142
|
const cwd = path.resolve(options.cwd || process.cwd());
|
|
143
143
|
const platformType = options.platformType ?? configuredPlatform(cwd);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
|
|
4
5
|
import { detectRepoRoot } from '../task/resolve-ref.ts';
|
|
@@ -52,16 +53,26 @@ function parseArgs(args: string[], valueOptions: Set<string>, booleanOptions: Se
|
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
function dataRoot(repoRoot: string, requested?: string): string {
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
let boundary = repoRoot;
|
|
57
|
+
let input = requested;
|
|
58
|
+
if (input === undefined) {
|
|
59
|
+
const config = JSON.parse(fs.readFileSync(path.join(repoRoot, '.agents', '.airc.json'), 'utf8')) as { project?: unknown };
|
|
60
|
+
if (typeof config.project !== 'string' || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(config.project)) {
|
|
61
|
+
throw new Error('.agents/.airc.json must contain a valid project name in its project field');
|
|
62
|
+
}
|
|
63
|
+
boundary = path.resolve(os.homedir());
|
|
64
|
+
input = path.join(boundary, '.agent-infra', 'data-store', config.project);
|
|
65
|
+
}
|
|
66
|
+
const resolved = path.resolve(repoRoot, input);
|
|
57
67
|
const relative = path.relative(repoRoot, resolved);
|
|
58
|
-
if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) throw new Error('data root must be inside the repository');
|
|
68
|
+
if (requested !== undefined && (!relative || relative.startsWith('..') || path.isAbsolute(relative))) throw new Error('data root must be inside the repository');
|
|
69
|
+
const workspace = path.resolve(repoRoot, '.agents', 'workspace');
|
|
59
70
|
const taskRoots = ['active', 'blocked', 'completed', 'archive'].map((state) => path.join(workspace, state));
|
|
60
71
|
if (resolved === workspace || taskRoots.some((taskRoot) => resolved === taskRoot || resolved.startsWith(`${taskRoot}${path.sep}`))) {
|
|
61
72
|
throw new Error('data root cannot be the workspace root or a task directory');
|
|
62
73
|
}
|
|
63
74
|
let current = resolved;
|
|
64
|
-
while (current !==
|
|
75
|
+
while (current !== boundary) {
|
|
65
76
|
if (fs.existsSync(current) && fs.lstatSync(current).isSymbolicLink()) throw new Error('data root cannot traverse a symlink');
|
|
66
77
|
current = path.dirname(current);
|
|
67
78
|
}
|
|
@@ -1108,7 +1108,6 @@ export async function create(
|
|
|
1108
1108
|
const container = containerName(effectiveConfig, branch);
|
|
1109
1109
|
const controlPaths = sandboxControlPaths({
|
|
1110
1110
|
base: effectiveConfig.controlBase,
|
|
1111
|
-
repoRoot: effectiveConfig.repoRoot,
|
|
1112
1111
|
project: effectiveConfig.project,
|
|
1113
1112
|
container,
|
|
1114
1113
|
identity: target.workspace
|
|
@@ -1493,6 +1492,7 @@ export async function create(
|
|
|
1493
1492
|
shellConfigHostDir: hostShellConfig.hostDir,
|
|
1494
1493
|
workspaceViewRoot: workspaceView.root,
|
|
1495
1494
|
controlDir: control.channelDir,
|
|
1495
|
+
...(target.workspace.mode === 'task-bound' ? { controlRootDir: control.root } : {}),
|
|
1496
1496
|
controlStatusDir: control.statusDir,
|
|
1497
1497
|
...(target.workspace.mode === 'task-bound' ? { runtimeDir: control.runtimeDir } : {}),
|
|
1498
1498
|
...(target.workspace.mode === 'task-bound'
|
|
@@ -1588,8 +1588,6 @@ export async function create(
|
|
|
1588
1588
|
]
|
|
1589
1589
|
: []),
|
|
1590
1590
|
'-e',
|
|
1591
|
-
`AGENT_INFRA_CONTROL_TOKEN=${control.token}`,
|
|
1592
|
-
'-e',
|
|
1593
1591
|
`AGENT_INFRA_CONTROL_GENERATION=${control.generation}`,
|
|
1594
1592
|
'-e',
|
|
1595
1593
|
'AGENT_INFRA_CONTROL_DIR=/run/agent-infra/control',
|
|
@@ -1598,7 +1596,10 @@ export async function create(
|
|
|
1598
1596
|
'-e',
|
|
1599
1597
|
`AGENT_INFRA_CONTROL_ROOT_ID=${control.controlRootId}`,
|
|
1600
1598
|
...(target.workspace.mode === 'task-bound'
|
|
1601
|
-
? [
|
|
1599
|
+
? [
|
|
1600
|
+
'-e', 'AGENT_INFRA_CONTROL_ROOT_DIR=/run/agent-infra/control-root',
|
|
1601
|
+
'-e', 'AGENT_INFRA_RUNTIME_DIR=/run/agent-infra/runtime'
|
|
1602
|
+
]
|
|
1602
1603
|
: []),
|
|
1603
1604
|
'--label',
|
|
1604
1605
|
`${sandboxRuntimeCapabilityLabel(effectiveConfig)}=${capabilityPlan.runtimeSignature}`,
|
package/lib/sandbox/config.ts
CHANGED
|
@@ -237,3 +237,16 @@ export function loadConfig({
|
|
|
237
237
|
}
|
|
238
238
|
};
|
|
239
239
|
}
|
|
240
|
+
|
|
241
|
+
export function loadSandboxControlLocation(repoRoot = detectRepoRoot()): Readonly<{
|
|
242
|
+
controlBase: string;
|
|
243
|
+
project: string;
|
|
244
|
+
}> | null {
|
|
245
|
+
const configPath = path.join(repoRoot, '.agents', '.airc.json');
|
|
246
|
+
if (!fs.existsSync(configPath)) return null;
|
|
247
|
+
const airc = JSON.parse(fs.readFileSync(configPath, 'utf8')) as AircConfig;
|
|
248
|
+
if (typeof airc.project !== 'string' || !airc.project) return null;
|
|
249
|
+
const home = homedir();
|
|
250
|
+
if (!home) throw new Error('sandbox: home directory is required');
|
|
251
|
+
return { controlBase: hostJoin(home, '.agent-infra', 'sandbox-control'), project: airc.project };
|
|
252
|
+
}
|
|
@@ -41,8 +41,7 @@ const AUDIT_MAX_BYTES = 1024 * 1024;
|
|
|
41
41
|
|
|
42
42
|
export function requestAuditFields(manifest: SandboxControlManifest, request: SandboxControlRequest) {
|
|
43
43
|
return {
|
|
44
|
-
requestId: request.id, requestFamily: request.family, sandboxTaskId: manifest.taskId
|
|
45
|
-
requestGeneration: request.generation, requestIssuedAt: request.issuedAt, requestExpiresAt: request.expiresAt
|
|
44
|
+
requestId: request.id, requestFamily: request.family, sandboxTaskId: manifest.taskId
|
|
46
45
|
};
|
|
47
46
|
}
|
|
48
47
|
|
|
@@ -2,17 +2,14 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
import {
|
|
5
|
-
SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
|
|
6
5
|
SANDBOX_CONTROL_MAX_BYTES,
|
|
7
6
|
SANDBOX_CONTROL_STATUS_STALE_MS,
|
|
8
7
|
isSandboxControlFamily,
|
|
9
8
|
type SandboxControlError,
|
|
10
9
|
type SandboxControlRequest,
|
|
11
10
|
type SandboxControlResponse,
|
|
12
|
-
type SandboxAgentClientRequest,
|
|
13
11
|
type SandboxTaskCreateRequest
|
|
14
12
|
} from './protocol.ts';
|
|
15
|
-
import { isAgentClientId } from '../../agent-clients/types.ts';
|
|
16
13
|
import { readSandboxControlPayload, readSandboxControlStatus } from './state.ts';
|
|
17
14
|
import type { TaskCreateCandidateV1 } from '../../task/create.ts';
|
|
18
15
|
import { accessSandboxTaskView, taskViewFromStatus, type TaskViewAccessEffect } from './task-view.ts';
|
|
@@ -53,11 +50,10 @@ function clientError(
|
|
|
53
50
|
|
|
54
51
|
function preflight(
|
|
55
52
|
statusDir: string,
|
|
56
|
-
generation: string,
|
|
57
53
|
taskViewEffect: TaskViewAccessEffect | null = null,
|
|
58
54
|
now = Date.now(),
|
|
59
55
|
env: NodeJS.ProcessEnv = process.env
|
|
60
|
-
):
|
|
56
|
+
): string {
|
|
61
57
|
if (fs.existsSync(statusDir)) {
|
|
62
58
|
let identity;
|
|
63
59
|
try {
|
|
@@ -68,9 +64,6 @@ function preflight(
|
|
|
68
64
|
: 'SANDBOX_CONTROL_IDENTITY_MALFORMED';
|
|
69
65
|
clientError(code, 'sandbox control identity is missing or invalid', false);
|
|
70
66
|
}
|
|
71
|
-
if (identity.generation !== generation) {
|
|
72
|
-
clientError('SANDBOX_CONTROL_IDENTITY_GENERATION_MISMATCH', 'sandbox control identity generation does not match the request', false);
|
|
73
|
-
}
|
|
74
67
|
if (env.AGENT_INFRA_CONTROL_ROOT_ID && identity.controlRootId !== env.AGENT_INFRA_CONTROL_ROOT_ID) {
|
|
75
68
|
clientError('SANDBOX_CONTROL_IDENTITY_ROOT_ID_MISMATCH', 'sandbox control identity root does not match the client configuration', false);
|
|
76
69
|
}
|
|
@@ -81,9 +74,6 @@ function preflight(
|
|
|
81
74
|
} catch {
|
|
82
75
|
clientError('SANDBOX_CONTROL_BROKER_UNAVAILABLE', 'broker status is missing or invalid', true);
|
|
83
76
|
}
|
|
84
|
-
if (status.generation !== generation) {
|
|
85
|
-
clientError('SANDBOX_CONTROL_GENERATION_INVALID', 'broker status generation does not match this sandbox', false);
|
|
86
|
-
}
|
|
87
77
|
if (now - status.updatedAt > SANDBOX_CONTROL_STATUS_STALE_MS || status.updatedAt > now + 1_000) {
|
|
88
78
|
clientError('SANDBOX_CONTROL_BROKER_UNAVAILABLE', 'broker status heartbeat is stale', true);
|
|
89
79
|
}
|
|
@@ -107,6 +97,7 @@ function preflight(
|
|
|
107
97
|
);
|
|
108
98
|
}
|
|
109
99
|
}
|
|
100
|
+
return status.generation;
|
|
110
101
|
}
|
|
111
102
|
|
|
112
103
|
function taskViewEffectForRequest(request: SandboxControlRequest): TaskViewAccessEffect | null {
|
|
@@ -201,7 +192,7 @@ function exchangeSandboxControl(request: SandboxControlRequest, params: Readonly
|
|
|
201
192
|
const channelDir = params.channelDir ?? process.env.AGENT_INFRA_CONTROL_DIR ?? '/run/agent-infra/control';
|
|
202
193
|
const statusDir = params.statusDir ?? process.env.AGENT_INFRA_CONTROL_STATUS_DIR ?? '/run/agent-infra/control-status';
|
|
203
194
|
const taskViewEffect = taskViewEffectForRequest(request);
|
|
204
|
-
preflight(statusDir,
|
|
195
|
+
const hostGeneration = preflight(statusDir, taskViewEffect);
|
|
205
196
|
const identityPath = path.join(statusDir, 'identity.json');
|
|
206
197
|
if (fs.existsSync(statusDir)) {
|
|
207
198
|
const identity = readSandboxControlIdentitySentinel(statusDir);
|
|
@@ -246,7 +237,7 @@ function exchangeSandboxControl(request: SandboxControlRequest, params: Readonly
|
|
|
246
237
|
try {
|
|
247
238
|
// The request-specific terminal proves the broker claimed the request; preserve recovery evidence if marker cleanup won the race.
|
|
248
239
|
accepted = true;
|
|
249
|
-
response = readPublishedResponse(raw, request.id, channelDir, accepted,
|
|
240
|
+
response = readPublishedResponse(raw, request.id, channelDir, accepted, hostGeneration);
|
|
250
241
|
malformedResponseRaw = null;
|
|
251
242
|
} catch (error) {
|
|
252
243
|
if (!(error instanceof SyntaxError)) throw error;
|
|
@@ -268,7 +259,7 @@ function exchangeSandboxControl(request: SandboxControlRequest, params: Readonly
|
|
|
268
259
|
}
|
|
269
260
|
if (!accepted) {
|
|
270
261
|
try {
|
|
271
|
-
preflight(statusDir,
|
|
262
|
+
preflight(statusDir, taskViewEffect);
|
|
272
263
|
} catch (error) {
|
|
273
264
|
if (cancelPendingRequest(requestPath)) throw error;
|
|
274
265
|
clientError(
|
|
@@ -314,6 +305,9 @@ export function recoverSandboxControl(requestId: string, params: Readonly<{
|
|
|
314
305
|
const config = loadConfig();
|
|
315
306
|
const response = recoverSandboxControlFromHost(requestId, {
|
|
316
307
|
managedRoot: path.join(config.controlBase, config.project),
|
|
308
|
+
controlBase: config.controlBase,
|
|
309
|
+
project: config.project,
|
|
310
|
+
repoRoot: config.repoRoot,
|
|
317
311
|
timeoutMs: params.timeoutMs
|
|
318
312
|
});
|
|
319
313
|
if (response.phase === 'rejected' && response.error?.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN') {
|
|
@@ -402,72 +396,30 @@ export function recoverSandboxControlFromChannel(requestId: string, params: Read
|
|
|
402
396
|
);
|
|
403
397
|
}
|
|
404
398
|
|
|
405
|
-
function authority(params: { token?: string; generation?: string }): { token: string; generation: string } {
|
|
406
|
-
const token = params.token ?? process.env.AGENT_INFRA_CONTROL_TOKEN;
|
|
407
|
-
const generation = params.generation ?? process.env.AGENT_INFRA_CONTROL_GENERATION;
|
|
408
|
-
if (!token || !generation) clientError('SANDBOX_CONTROL_UNAVAILABLE', 'control authority is missing', false);
|
|
409
|
-
return { token, generation };
|
|
410
|
-
}
|
|
411
|
-
|
|
412
399
|
export function requestSandboxControl(params: Readonly<{
|
|
413
400
|
family: string; args: string[]; channelDir?: string; statusDir?: string;
|
|
414
|
-
|
|
401
|
+
timeoutMs?: number;
|
|
415
402
|
}>): SandboxControlResponse {
|
|
416
403
|
if (!isSandboxControlFamily(params.family)) clientError('SANDBOX_CONTROL_COMMAND_DENIED', `'${params.family}' is not allowed`, false);
|
|
417
|
-
if (params.family === 'task-create'
|
|
404
|
+
if (params.family === 'task-create') {
|
|
418
405
|
clientError('SANDBOX_CONTROL_COMMAND_DENIED', `'${params.family}' requires a typed request`, false);
|
|
419
406
|
}
|
|
420
407
|
if (params.args[1] === 'complete') {
|
|
421
408
|
clientError('SANDBOX_CONTROL_COMMAND_DENIED', 'complete must use task-finalization', false);
|
|
422
409
|
}
|
|
423
|
-
const auth = authority(params);
|
|
424
|
-
const issuedAt = Date.now();
|
|
425
410
|
const request: SandboxControlRequest = {
|
|
426
|
-
version:
|
|
427
|
-
expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
|
|
411
|
+
version: 5, id: randomUUID(),
|
|
428
412
|
family: params.family as 'task-lifecycle', args: params.args
|
|
429
413
|
};
|
|
430
414
|
return exchangeSandboxControl(request, params);
|
|
431
415
|
}
|
|
432
416
|
|
|
433
|
-
export function requestSandboxAgentOperation(params: Readonly<{
|
|
434
|
-
agentClient: string;
|
|
435
|
-
operation: string;
|
|
436
|
-
payload: Record<string, unknown>;
|
|
437
|
-
channelDir?: string;
|
|
438
|
-
statusDir?: string;
|
|
439
|
-
token?: string;
|
|
440
|
-
generation?: string;
|
|
441
|
-
timeoutMs?: number;
|
|
442
|
-
}>): SandboxControlResponse {
|
|
443
|
-
if (!isAgentClientId(params.agentClient)) {
|
|
444
|
-
clientError('SANDBOX_CONTROL_REQUEST_INVALID', 'agent-client id is invalid', false);
|
|
445
|
-
}
|
|
446
|
-
const auth = authority(params);
|
|
447
|
-
const issuedAt = Date.now();
|
|
448
|
-
const request: SandboxAgentClientRequest = {
|
|
449
|
-
version: 4,
|
|
450
|
-
id: randomUUID(),
|
|
451
|
-
...auth,
|
|
452
|
-
issuedAt,
|
|
453
|
-
expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
|
|
454
|
-
family: 'agent-client',
|
|
455
|
-
agentClient: params.agentClient,
|
|
456
|
-
operation: params.operation,
|
|
457
|
-
payload: params.payload
|
|
458
|
-
};
|
|
459
|
-
return exchangeSandboxControl(request, params);
|
|
460
|
-
}
|
|
461
|
-
|
|
462
417
|
export function requestSandboxTaskCreate(params: Readonly<{
|
|
463
418
|
candidate: TaskCreateCandidateV1; channelDir?: string; statusDir?: string;
|
|
464
|
-
|
|
419
|
+
timeoutMs?: number;
|
|
465
420
|
}>): SandboxControlResponse {
|
|
466
|
-
const auth = authority(params);
|
|
467
|
-
const issuedAt = Date.now();
|
|
468
421
|
const request: SandboxTaskCreateRequest = {
|
|
469
|
-
version:
|
|
470
|
-
expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
|
|
422
|
+
version: 5, id: randomUUID(),
|
|
471
423
|
family: 'task-create', candidate: params.candidate,
|
|
472
424
|
};
|
|
473
425
|
return exchangeSandboxControl(request, { ...params, timeoutMs: params.timeoutMs ?? 120_000 });
|
|
@@ -33,8 +33,6 @@ import {
|
|
|
33
33
|
type TaskControlOperation
|
|
34
34
|
} from '../../task/control-authority.ts';
|
|
35
35
|
import { assertSandboxControlBrokerOwner, readSandboxControlManifest, type BrokerOwner } from './lifecycle.ts';
|
|
36
|
-
import { executeAgentClientOperation, validateAgentClientOperation } from './agent-operation.ts';
|
|
37
|
-
import type { AgentClientSandboxControlOperation } from '../../agent-clients/adapter.ts';
|
|
38
36
|
|
|
39
37
|
export type SandboxControlExecutionResult = {
|
|
40
38
|
exitCode: number;
|
|
@@ -256,7 +254,6 @@ function taskCreateExecutionFailure(requestId: string, code: string, message: st
|
|
|
256
254
|
|
|
257
255
|
type ExecuteRequestOptions = Readonly<{
|
|
258
256
|
createTask?: typeof createTask;
|
|
259
|
-
agentClientOperation?: AgentClientSandboxControlOperation;
|
|
260
257
|
}>;
|
|
261
258
|
|
|
262
259
|
async function executeRequestInner(
|
|
@@ -279,10 +276,6 @@ async function executeRequestInner(
|
|
|
279
276
|
return taskCreateExecutionFailure(request.id, code, detail);
|
|
280
277
|
}
|
|
281
278
|
}
|
|
282
|
-
if (request.family === 'agent-client') {
|
|
283
|
-
validateAgentClientOperation(request, manifest);
|
|
284
|
-
return executeAgentClientOperation(request, manifest, manifestPath, options.agentClientOperation);
|
|
285
|
-
}
|
|
286
279
|
const boundArgs = bindSandboxControlTask(request, manifest.taskId!);
|
|
287
280
|
let operation: TaskControlOperation;
|
|
288
281
|
try {
|
|
@@ -346,9 +339,10 @@ export async function runSandboxControlExecutor(requestPath: string, nonce: stri
|
|
|
346
339
|
const gate = await waitForGate(nonce);
|
|
347
340
|
const gateOwner = gate.owner;
|
|
348
341
|
process.disconnect?.();
|
|
349
|
-
const
|
|
350
|
-
if (!
|
|
351
|
-
const manifest = readSandboxControlManifest(
|
|
342
|
+
const requestedManifestPath = process.env.AGENT_INFRA_EXECUTOR_MANIFEST;
|
|
343
|
+
if (!requestedManifestPath) throw new Error('SANDBOX_CONTROL_EXECUTOR_MANIFEST_MISSING');
|
|
344
|
+
const manifest = readSandboxControlManifest(requestedManifestPath);
|
|
345
|
+
const manifestPath = fs.realpathSync.native(requestedManifestPath);
|
|
352
346
|
const identity = validateSandboxControlIdentity({
|
|
353
347
|
publicStatusDir: manifest.publicStatusDir,
|
|
354
348
|
root: path.dirname(path.resolve(manifestPath)),
|
|
@@ -390,17 +384,17 @@ export async function runSandboxControlExecutor(requestPath: string, nonce: stri
|
|
|
390
384
|
let result: SandboxControlExecutionResult;
|
|
391
385
|
try {
|
|
392
386
|
assertSandboxControlExecutorAuthority(manifest, gateOwner);
|
|
393
|
-
|
|
387
|
+
const operation = request.family === 'task-lifecycle'
|
|
394
388
|
? (() => {
|
|
395
389
|
try { return parseTaskControlOperation(request.family, request.args); } catch { return null; }
|
|
396
390
|
})()
|
|
397
|
-
:
|
|
391
|
+
: null;
|
|
398
392
|
const context = createSandboxControlAuditContext(manifest, {
|
|
399
393
|
requestId: request.id,
|
|
400
394
|
family: request.family,
|
|
401
395
|
operation: operation && typeof operation === 'object' && 'family' in operation && operation.family === 'task-lifecycle'
|
|
402
396
|
? operation.request.intent
|
|
403
|
-
:
|
|
397
|
+
: null,
|
|
404
398
|
phase: 'started-committed',
|
|
405
399
|
outcome: 'in-progress'
|
|
406
400
|
});
|