@fitlab-ai/agent-infra 0.9.8 → 0.9.9
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/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/bin/internal-cli.js +6 -1
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +58 -21
- package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +8 -1
- package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +173 -10
- package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-context.js +147 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +56 -67
- package/dist/lib/git/workflow.js +10 -1
- package/dist/lib/internal/codex-lifecycle.js +19 -5
- package/dist/lib/internal/codex-sandbox-controller.js +6 -13
- package/dist/lib/internal/git-workflow.js +5 -7
- package/dist/lib/internal/platform-pr.js +20 -7
- package/dist/lib/internal/release-workflow.js +2 -1
- package/dist/lib/internal/sandbox-control.js +79 -2
- package/dist/lib/internal/task-finalization.js +54 -0
- package/dist/lib/internal/task-orchestration.js +156 -291
- package/dist/lib/platform/pr-summary.js +48 -10
- package/dist/lib/platform/pull-requests.js +205 -41
- package/dist/lib/sandbox/commands/rm.js +316 -55
- package/dist/lib/sandbox/control/client.js +190 -6
- package/dist/lib/sandbox/control/controller-registration.js +273 -0
- package/dist/lib/sandbox/control/executor.js +137 -5
- package/dist/lib/sandbox/control/lifecycle.js +3 -1
- package/dist/lib/sandbox/control/protocol.js +67 -6
- package/dist/lib/sandbox/index.js +5 -3
- package/dist/lib/sandbox/managed-fs.js +5 -0
- package/dist/lib/sandbox/task-resolver.js +50 -6
- package/dist/lib/sandbox/workspace-identity.js +27 -0
- package/dist/lib/sandbox/worktree-safety.js +215 -2
- package/dist/lib/task/codex-orchestration.js +158 -33
- package/dist/lib/task/commands/status.js +59 -1
- package/dist/lib/task/commit-operation.js +335 -0
- package/dist/lib/task/commit-policy.js +18 -0
- package/dist/lib/task/delegation-receipts.js +243 -2
- package/dist/lib/task/finalization.js +567 -0
- package/dist/lib/task/operation-outcome.js +33 -0
- package/dist/lib/task/orchestration.js +187 -761
- package/dist/lib/task/short-id.js +59 -1
- package/dist/lib/task/task-execution-lock.js +6 -1
- package/dist/lib/task/verification-engine.js +113 -60
- package/dist/lib/task/verification.js +2 -1
- package/dist/lib/task/workflow-warning-intents.js +50 -1
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +61 -19
- package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +12 -0
- package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +276 -12
- package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +180 -0
- package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -0
- package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +9 -1
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +83 -91
- package/lib/git/workflow.ts +11 -2
- package/lib/internal/codex-lifecycle.ts +18 -5
- package/lib/internal/codex-sandbox-controller.ts +6 -12
- package/lib/internal/git-workflow.ts +5 -6
- package/lib/internal/platform-pr.ts +14 -7
- package/lib/internal/release-workflow.ts +2 -1
- package/lib/internal/sandbox-control.ts +92 -2
- package/lib/internal/task-finalization.ts +68 -0
- package/lib/internal/task-orchestration.ts +20 -103
- package/lib/platform/pr-summary.ts +55 -11
- package/lib/platform/pull-requests.ts +211 -41
- package/lib/sandbox/commands/rm.ts +413 -61
- package/lib/sandbox/control/client.ts +255 -6
- package/lib/sandbox/control/controller-registration.ts +347 -0
- package/lib/sandbox/control/executor.ts +163 -5
- package/lib/sandbox/control/lifecycle.ts +2 -1
- package/lib/sandbox/control/protocol.ts +80 -7
- package/lib/sandbox/index.ts +5 -3
- package/lib/sandbox/managed-fs.ts +5 -0
- package/lib/sandbox/task-resolver.ts +58 -7
- package/lib/sandbox/workspace-identity.ts +50 -2
- package/lib/sandbox/workspace-view.ts +3 -3
- package/lib/sandbox/worktree-safety.ts +236 -2
- package/lib/task/codex-orchestration.ts +167 -33
- package/lib/task/commands/status.ts +78 -1
- package/lib/task/commit-operation.ts +430 -0
- package/lib/task/commit-policy.ts +30 -0
- package/lib/task/delegation-receipts.ts +258 -1
- package/lib/task/finalization.ts +719 -0
- package/lib/task/operation-outcome.ts +48 -0
- package/lib/task/orchestration.ts +217 -982
- package/lib/task/short-id.ts +65 -1
- package/lib/task/task-execution-lock.ts +10 -1
- package/lib/task/verification-engine.ts +118 -63
- package/lib/task/verification.ts +3 -2
- package/lib/task/workflow-warning-intents.ts +57 -2
- package/package.json +2 -2
- package/templates/.agents/QUICKSTART.en.md +2 -2
- package/templates/.agents/QUICKSTART.zh-CN.md +2 -2
- package/templates/.agents/hooks/lifecycle-delegation.js +1 -21
- package/templates/.agents/rules/lifecycle-orchestration.en.md +1 -1
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +1 -1
- package/templates/.agents/rules/task-management.en.md +2 -2
- package/templates/.agents/rules/task-management.zh-CN.md +2 -2
- package/templates/.agents/skills/block-task/SKILL.en.md +2 -4
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -4
- package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/close-codescan/SKILL.en.md +6 -2
- package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +6 -2
- package/templates/.agents/skills/close-dependabot/SKILL.en.md +6 -2
- package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +6 -2
- package/templates/.agents/skills/commit/SKILL.en.md +41 -125
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +52 -123
- package/templates/.agents/skills/commit/reference/commit-message.en.md +1 -1
- package/templates/.agents/skills/commit/reference/commit-message.zh-CN.md +1 -1
- package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +14 -43
- package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +14 -49
- package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +1 -1
- package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +1 -1
- package/templates/.agents/skills/commit/reference/task-status-update.en.md +2 -35
- package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +2 -35
- package/templates/.agents/skills/complete-task/SKILL.en.md +31 -38
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +31 -38
- package/templates/.agents/skills/complete-task/config/verify.en.json +1 -0
- package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +1 -0
- package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +1 -1
- package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +1 -1
- package/templates/.agents/skills/create-pr/SKILL.en.md +6 -4
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +6 -4
- package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +2 -2
- package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +2 -2
- package/templates/.agents/skills/run-task/SKILL.en.md +5 -5
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +5 -5
- 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/templates/.codex/hooks.json +4 -4
- package/dist/lib/task/commit-finalization.js +0 -305
- package/dist/lib/task/commit-intent.js +0 -165
- package/lib/task/commit-finalization.ts +0 -419
- package/lib/task/commit-intent.ts +0 -230
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
3
4
|
import path from 'node:path';
|
|
4
5
|
import { runProbe } from './shell.ts';
|
|
6
|
+
import { resolveSandboxTarget } from './workspace-identity.ts';
|
|
5
7
|
|
|
6
8
|
type WorktreeChange = {
|
|
7
9
|
indexStatus: string;
|
|
@@ -16,6 +18,8 @@ type WorktreeSnapshot = {
|
|
|
16
18
|
head: string;
|
|
17
19
|
changes: readonly WorktreeChange[];
|
|
18
20
|
identity: string;
|
|
21
|
+
source?: 'registered' | 'recovered';
|
|
22
|
+
recovery?: WorktreeRecoveryContext;
|
|
19
23
|
};
|
|
20
24
|
|
|
21
25
|
type WorktreeInspection =
|
|
@@ -28,6 +32,14 @@ type WorktreeRemovalPermit = {
|
|
|
28
32
|
snapshot: WorktreeSnapshot;
|
|
29
33
|
};
|
|
30
34
|
|
|
35
|
+
type WorktreeRecoveryContext = Readonly<{
|
|
36
|
+
repoRoot: string;
|
|
37
|
+
worktreeBase: string;
|
|
38
|
+
branch: string;
|
|
39
|
+
identitySource: 'branch-only' | 'task-bound';
|
|
40
|
+
taskId: string | null;
|
|
41
|
+
}>;
|
|
42
|
+
|
|
31
43
|
type Probe = typeof runProbe;
|
|
32
44
|
|
|
33
45
|
function probeText(probe: Probe, cwd: string, args: string[]): string {
|
|
@@ -164,6 +176,203 @@ function captureOnce(worktree: string, probe: Probe): WorktreeSnapshot {
|
|
|
164
176
|
return { worktree: resolvedWorktree, branch, head, changes, identity: hash.digest('hex') };
|
|
165
177
|
}
|
|
166
178
|
|
|
179
|
+
function probeTextWithEnv(
|
|
180
|
+
probe: Probe,
|
|
181
|
+
repoRoot: string,
|
|
182
|
+
args: string[],
|
|
183
|
+
env: NodeJS.ProcessEnv
|
|
184
|
+
): string {
|
|
185
|
+
const result = probe('git', args, { cwd: repoRoot, encoding: 'utf8', env });
|
|
186
|
+
if (result.error) throw result.error;
|
|
187
|
+
if (result.status !== 0) {
|
|
188
|
+
const stderr = typeof result.stderr === 'string' ? result.stderr.trim() : result.stderr.toString('utf8').trim();
|
|
189
|
+
throw new Error(stderr || `git ${args[0] ?? 'command'} failed with exit code ${result.status ?? 'unknown'}`);
|
|
190
|
+
}
|
|
191
|
+
return typeof result.stdout === 'string' ? result.stdout : result.stdout.toString('utf8');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function probeMetadataCommand(
|
|
195
|
+
probe: Probe,
|
|
196
|
+
repoRoot: string,
|
|
197
|
+
args: string[],
|
|
198
|
+
env: NodeJS.ProcessEnv
|
|
199
|
+
): boolean {
|
|
200
|
+
const result = probe('git', args, { cwd: repoRoot, encoding: 'utf8', env });
|
|
201
|
+
if (result.error) throw result.error;
|
|
202
|
+
return result.status === 0;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function hasValidHeadContent(content: string): boolean {
|
|
206
|
+
return /^(?:ref:\s+refs\/\S+|[0-9a-f]{40}|[0-9a-f]{64})$/i.test(content.trim());
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function assertRecoverableMetadata(worktree: string, repoRoot: string, probe: Probe): void {
|
|
210
|
+
const dotGit = path.join(worktree, '.git');
|
|
211
|
+
let dotGitStat: fs.Stats;
|
|
212
|
+
try {
|
|
213
|
+
dotGitStat = fs.lstatSync(dotGit);
|
|
214
|
+
} catch (error) {
|
|
215
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return;
|
|
216
|
+
throw error;
|
|
217
|
+
}
|
|
218
|
+
if (dotGitStat.isSymbolicLink() || dotGitStat.isDirectory()) {
|
|
219
|
+
throw new Error('WORKTREE_RECOVERY_METADATA_NOT_ELIGIBLE');
|
|
220
|
+
}
|
|
221
|
+
if (!dotGitStat.isFile()) throw new Error('WORKTREE_RECOVERY_METADATA_INVALID');
|
|
222
|
+
|
|
223
|
+
const content = fs.readFileSync(dotGit, 'utf8').trim();
|
|
224
|
+
const match = /^gitdir:\s*(.+)$/i.exec(content);
|
|
225
|
+
if (!match) throw new Error('WORKTREE_RECOVERY_METADATA_INVALID');
|
|
226
|
+
|
|
227
|
+
const commonDir = fs.realpathSync.native(path.resolve(repoRoot, probeText(probe, repoRoot, [
|
|
228
|
+
'rev-parse', '--git-common-dir'
|
|
229
|
+
]).trim()));
|
|
230
|
+
const adminRoot = path.join(commonDir, 'worktrees');
|
|
231
|
+
const adminPath = path.resolve(path.dirname(dotGit), match[1]!);
|
|
232
|
+
const relative = path.relative(adminRoot, adminPath);
|
|
233
|
+
if (!relative || relative.startsWith('..') || path.isAbsolute(relative) || relative.includes(path.sep)) {
|
|
234
|
+
throw new Error('WORKTREE_RECOVERY_METADATA_INVALID');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (!fs.existsSync(adminPath)) return;
|
|
238
|
+
const adminStat = fs.lstatSync(adminPath);
|
|
239
|
+
if (adminStat.isSymbolicLink()) throw new Error('WORKTREE_RECOVERY_METADATA_INVALID');
|
|
240
|
+
if (!adminStat.isDirectory()) return;
|
|
241
|
+
|
|
242
|
+
const requiredFiles = ['HEAD', 'gitdir', 'commondir', 'index'];
|
|
243
|
+
if (requiredFiles.some((name) => !fs.existsSync(path.join(adminPath, name)))) return;
|
|
244
|
+
for (const name of requiredFiles) {
|
|
245
|
+
const filePath = path.join(adminPath, name);
|
|
246
|
+
const stat = fs.lstatSync(filePath);
|
|
247
|
+
if (stat.isSymbolicLink() || !stat.isFile()) throw new Error('WORKTREE_RECOVERY_METADATA_INVALID');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const adminGitdir = fs.readFileSync(path.join(adminPath, 'gitdir'), 'utf8').trim();
|
|
251
|
+
const adminCommonDir = fs.readFileSync(path.join(adminPath, 'commondir'), 'utf8').trim();
|
|
252
|
+
const adminGitdirPath = adminGitdir
|
|
253
|
+
? fs.realpathSync.native(path.resolve(adminPath, adminGitdir))
|
|
254
|
+
: '';
|
|
255
|
+
const canonicalDotGit = fs.realpathSync.native(dotGit);
|
|
256
|
+
const adminCommonDirPath = adminCommonDir
|
|
257
|
+
? fs.realpathSync.native(path.resolve(adminPath, adminCommonDir))
|
|
258
|
+
: '';
|
|
259
|
+
if (!adminGitdir || adminGitdirPath !== canonicalDotGit
|
|
260
|
+
|| !adminCommonDir || adminCommonDirPath !== commonDir) {
|
|
261
|
+
throw new Error('WORKTREE_RECOVERY_METADATA_INVALID');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const headContent = fs.readFileSync(path.join(adminPath, 'HEAD'), 'utf8');
|
|
265
|
+
const indexContent = fs.readFileSync(path.join(adminPath, 'index'));
|
|
266
|
+
if (!hasValidHeadContent(headContent) || indexContent.subarray(0, 4).toString('ascii') !== 'DIRC') return;
|
|
267
|
+
|
|
268
|
+
const validationEnv: NodeJS.ProcessEnv = { ...process.env };
|
|
269
|
+
for (const key of ['GIT_DIR', 'GIT_COMMON_DIR', 'GIT_INDEX_FILE', 'GIT_WORK_TREE', 'GIT_PREFIX']) {
|
|
270
|
+
delete validationEnv[key];
|
|
271
|
+
}
|
|
272
|
+
validationEnv.GIT_DIR = adminPath;
|
|
273
|
+
validationEnv.GIT_WORK_TREE = path.resolve(worktree);
|
|
274
|
+
if (!probeMetadataCommand(probe, repoRoot, ['rev-parse', '--verify', 'HEAD^{commit}'], validationEnv)) return;
|
|
275
|
+
if (!probeMetadataCommand(probe, repoRoot, ['ls-files', '--stage', '-z'], validationEnv)) return;
|
|
276
|
+
throw new Error('WORKTREE_RECOVERY_METADATA_REGISTERED');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function parseRecoveredStatus(raw: string): WorktreeChange[] {
|
|
280
|
+
const records = raw.split('\0');
|
|
281
|
+
const changes: WorktreeChange[] = [];
|
|
282
|
+
for (let index = 0; index < records.length;) {
|
|
283
|
+
const status = records[index++] ?? '';
|
|
284
|
+
if (!status) continue;
|
|
285
|
+
const code = status[0] ?? '';
|
|
286
|
+
if (code === 'R' || code === 'C') {
|
|
287
|
+
const originalPath = records[index++];
|
|
288
|
+
const nextPath = records[index++];
|
|
289
|
+
if (!originalPath || !nextPath) throw new Error('Unable to parse recovered Git rename record');
|
|
290
|
+
changes.push({ indexStatus: code, worktreeStatus: ' ', path: nextPath, originalPath });
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
const filePath = records[index++];
|
|
294
|
+
if (!filePath) throw new Error('Unable to parse recovered Git status record');
|
|
295
|
+
changes.push({
|
|
296
|
+
indexStatus: code === 'D' ? ' ' : code,
|
|
297
|
+
worktreeStatus: code === 'D' ? 'D' : ' ',
|
|
298
|
+
path: filePath
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
return changes;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function captureRecoveredOnce(
|
|
305
|
+
worktree: string,
|
|
306
|
+
recovery: WorktreeRecoveryContext,
|
|
307
|
+
probe: Probe
|
|
308
|
+
): WorktreeSnapshot {
|
|
309
|
+
const resolvedWorktree = path.resolve(worktree);
|
|
310
|
+
const resolvedBase = path.resolve(recovery.worktreeBase);
|
|
311
|
+
const relative = path.relative(resolvedBase, resolvedWorktree);
|
|
312
|
+
if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
313
|
+
throw new Error(`WORKTREE_RECOVERY_PATH_INVALID: ${resolvedWorktree}`);
|
|
314
|
+
}
|
|
315
|
+
const stat = fs.lstatSync(resolvedWorktree);
|
|
316
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error('WORKTREE_RECOVERY_PATH_INVALID');
|
|
317
|
+
const canonicalBase = fs.realpathSync.native(resolvedBase);
|
|
318
|
+
const canonicalWorktree = fs.realpathSync.native(resolvedWorktree);
|
|
319
|
+
const canonicalRelative = path.relative(canonicalBase, canonicalWorktree);
|
|
320
|
+
if (!canonicalRelative || canonicalRelative.startsWith('..') || path.isAbsolute(canonicalRelative)) {
|
|
321
|
+
throw new Error('WORKTREE_RECOVERY_PATH_INVALID');
|
|
322
|
+
}
|
|
323
|
+
const resolvedWorkspace = resolveSandboxTarget(recovery.branch, recovery.repoRoot).workspace;
|
|
324
|
+
if (recovery.identitySource === 'branch-only' && resolvedWorkspace.mode !== 'branch-only') {
|
|
325
|
+
throw new Error('WORKTREE_RECOVERY_IDENTITY_CHANGED');
|
|
326
|
+
}
|
|
327
|
+
if (recovery.identitySource === 'task-bound'
|
|
328
|
+
&& (resolvedWorkspace.mode !== 'task-bound' || resolvedWorkspace.taskId !== recovery.taskId)) {
|
|
329
|
+
throw new Error('WORKTREE_RECOVERY_IDENTITY_CHANGED');
|
|
330
|
+
}
|
|
331
|
+
assertRecoverableMetadata(resolvedWorktree, recovery.repoRoot, probe);
|
|
332
|
+
|
|
333
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-infra-recovered-index-'));
|
|
334
|
+
const env: NodeJS.ProcessEnv = { ...process.env };
|
|
335
|
+
for (const key of ['GIT_DIR', 'GIT_COMMON_DIR', 'GIT_PREFIX']) delete env[key];
|
|
336
|
+
env.GIT_INDEX_FILE = path.join(tempDir, 'index');
|
|
337
|
+
env.GIT_WORK_TREE = resolvedWorktree;
|
|
338
|
+
try {
|
|
339
|
+
const baseline = probeTextWithEnv(probe, recovery.repoRoot, [
|
|
340
|
+
'rev-parse', '--verify', `refs/heads/${recovery.branch}^{commit}`
|
|
341
|
+
], env).trim();
|
|
342
|
+
if (!baseline) throw new Error(`WORKTREE_RECOVERY_BRANCH_INVALID: ${recovery.branch}`);
|
|
343
|
+
probeTextWithEnv(probe, recovery.repoRoot, ['read-tree', baseline], env);
|
|
344
|
+
probeTextWithEnv(probe, recovery.repoRoot, ['add', '-A', '--', '.'], env);
|
|
345
|
+
const status = probeTextWithEnv(probe, recovery.repoRoot, [
|
|
346
|
+
'diff', '--cached', '--name-status', '--find-renames', '-z', '--', '.'
|
|
347
|
+
], env);
|
|
348
|
+
const changes = parseRecoveredStatus(status);
|
|
349
|
+
const hash = createHash('sha256');
|
|
350
|
+
updatePart(hash, 'source', 'recovered');
|
|
351
|
+
updatePart(hash, 'worktree', resolvedWorktree);
|
|
352
|
+
updatePart(hash, 'repo-root', path.resolve(recovery.repoRoot));
|
|
353
|
+
updatePart(hash, 'branch', recovery.branch);
|
|
354
|
+
updatePart(hash, 'baseline', baseline);
|
|
355
|
+
updatePart(hash, 'identity-source', recovery.identitySource);
|
|
356
|
+
updatePart(hash, 'task-id', recovery.taskId ?? '');
|
|
357
|
+
updatePart(hash, 'status', status);
|
|
358
|
+
for (const change of changes) {
|
|
359
|
+
hashPath(hash, resolvedWorktree, change.path, probe);
|
|
360
|
+
if (change.originalPath) hashPath(hash, resolvedWorktree, change.originalPath, probe);
|
|
361
|
+
}
|
|
362
|
+
return {
|
|
363
|
+
worktree: resolvedWorktree,
|
|
364
|
+
branch: recovery.branch,
|
|
365
|
+
head: baseline,
|
|
366
|
+
changes,
|
|
367
|
+
identity: hash.digest('hex'),
|
|
368
|
+
source: 'recovered',
|
|
369
|
+
recovery: { ...recovery }
|
|
370
|
+
};
|
|
371
|
+
} finally {
|
|
372
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
167
376
|
function inspectWorktree(worktree: string, { probe = runProbe }: { probe?: Probe } = {}): WorktreeInspection {
|
|
168
377
|
const resolvedWorktree = path.resolve(worktree);
|
|
169
378
|
try {
|
|
@@ -182,6 +391,28 @@ function inspectWorktree(worktree: string, { probe = runProbe }: { probe?: Probe
|
|
|
182
391
|
}
|
|
183
392
|
}
|
|
184
393
|
|
|
394
|
+
function inspectRecoveredWorktree(
|
|
395
|
+
worktree: string,
|
|
396
|
+
recovery: WorktreeRecoveryContext,
|
|
397
|
+
{ probe = runProbe }: { probe?: Probe } = {}
|
|
398
|
+
): WorktreeInspection {
|
|
399
|
+
const resolvedWorktree = path.resolve(worktree);
|
|
400
|
+
try {
|
|
401
|
+
const first = captureRecoveredOnce(resolvedWorktree, recovery, probe);
|
|
402
|
+
const second = captureRecoveredOnce(resolvedWorktree, recovery, probe);
|
|
403
|
+
if (first.identity !== second.identity) {
|
|
404
|
+
return { status: 'failed', worktree: resolvedWorktree, message: 'Snapshot changed while recovering worktree' };
|
|
405
|
+
}
|
|
406
|
+
return { status: second.changes.length === 0 ? 'clean' : 'dirty', snapshot: second };
|
|
407
|
+
} catch (error) {
|
|
408
|
+
return {
|
|
409
|
+
status: 'failed',
|
|
410
|
+
worktree: resolvedWorktree,
|
|
411
|
+
message: error instanceof Error ? error.message : String(error)
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
185
416
|
function inspectWorktrees(worktrees: readonly string[]): WorktreeInspection[] {
|
|
186
417
|
return [...new Set(worktrees.map((worktree) => path.resolve(worktree)))].sort()
|
|
187
418
|
.map((worktree) => inspectWorktree(worktree));
|
|
@@ -201,7 +432,9 @@ function verifyWorktreePermit(
|
|
|
201
432
|
permit: WorktreeRemovalPermit,
|
|
202
433
|
options: { probe?: Probe } = {}
|
|
203
434
|
): WorktreeSnapshot {
|
|
204
|
-
const current =
|
|
435
|
+
const current = permit.snapshot.source === 'recovered' && permit.snapshot.recovery
|
|
436
|
+
? inspectRecoveredWorktree(permit.snapshot.worktree, permit.snapshot.recovery, options)
|
|
437
|
+
: inspectWorktree(permit.snapshot.worktree, options);
|
|
205
438
|
if (current.status === 'failed') throw new Error(`Unable to verify worktree permit: ${current.message}`);
|
|
206
439
|
if (current.snapshot.identity !== permit.snapshot.identity || (permit.mode === 'clean' && current.status !== 'clean')) {
|
|
207
440
|
throw new Error(`Worktree changed after authorization: ${permit.snapshot.worktree}`);
|
|
@@ -227,8 +460,9 @@ export {
|
|
|
227
460
|
createCleanPermit,
|
|
228
461
|
createDiscardPermit,
|
|
229
462
|
formatWorktreeSnapshot,
|
|
463
|
+
inspectRecoveredWorktree,
|
|
230
464
|
inspectWorktree,
|
|
231
465
|
inspectWorktrees,
|
|
232
466
|
verifyWorktreePermit
|
|
233
467
|
};
|
|
234
|
-
export type { WorktreeChange, WorktreeInspection, WorktreeRemovalPermit, WorktreeSnapshot };
|
|
468
|
+
export type { WorktreeChange, WorktreeInspection, WorktreeRecoveryContext, WorktreeRemovalPermit, WorktreeSnapshot };
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
|
|
3
1
|
import {
|
|
4
2
|
preflightCodexLifecycleEvidence,
|
|
5
3
|
resolveCodexSpawnedChild,
|
|
@@ -7,16 +5,22 @@ import {
|
|
|
7
5
|
resolveCodexThread
|
|
8
6
|
} from '../agent-clients/adapters/codex-lifecycle/app-server.ts';
|
|
9
7
|
import { createCodexLifecycleStore } from '../agent-clients/adapters/codex-lifecycle/store.ts';
|
|
10
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
createCodexCapabilityStore,
|
|
10
|
+
isCodexCapabilityProvenanceDetail
|
|
11
|
+
} from '../agent-clients/adapters/codex-lifecycle/capability-store.ts';
|
|
12
|
+
import type { CodexCapabilityProvenanceDetail } from '../agent-clients/adapters/codex-lifecycle/capability-store.ts';
|
|
11
13
|
import { computeLifecycleBuildIdentity } from '../agent-clients/adapters/codex-lifecycle/build-identity.ts';
|
|
12
14
|
import type { LifecycleBuildIdentity } from '../agent-clients/adapters/codex-lifecycle/build-identity.ts';
|
|
13
15
|
import { verifyCodexSandboxControllerContext } from '../agent-clients/adapters/codex-lifecycle/sandbox-controller.ts';
|
|
16
|
+
import { verifyCodexSandboxControllerContext as verifyControllerContextFile } from '../agent-clients/adapters/codex-lifecycle/controller-context.ts';
|
|
14
17
|
import type { AgentClientId } from '../agent-clients/types.ts';
|
|
15
18
|
import { resolveTaskRef } from './resolve-ref.ts';
|
|
16
19
|
import {
|
|
17
20
|
activateMatchingOrchestrationDelegation,
|
|
18
21
|
hasActivatableOrchestrationDelegation,
|
|
19
22
|
hasSealableOrchestrationDelegation,
|
|
23
|
+
OrchestrationStateError,
|
|
20
24
|
pauseMatchingOrchestrationDelegation,
|
|
21
25
|
prepareOrchestrationDelegation,
|
|
22
26
|
reconcileMatchingOrchestrationDelegation,
|
|
@@ -52,10 +56,27 @@ function coreOptions(options: CodexBridgeOptions): OrchestrationOptions {
|
|
|
52
56
|
return { ...options.orchestrationOptions, repoRoot: options.repoRoot ?? options.orchestrationOptions?.repoRoot };
|
|
53
57
|
}
|
|
54
58
|
|
|
55
|
-
function bridgeFailure(code: string, message: string): OrchestrationResult {
|
|
59
|
+
function bridgeFailure(code: string, message: string, detail?: CodexCapabilityProvenanceDetail): OrchestrationResult {
|
|
56
60
|
return {
|
|
57
61
|
status: 'failed', changed: false, taskId: null, run: null, next: null,
|
|
58
|
-
error: { code, message }
|
|
62
|
+
error: { code, message, ...(detail ? { detail } : {}) }
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function capabilityFailure(error: unknown): NonNullable<OrchestrationResult['error']> {
|
|
67
|
+
const detailValue = error instanceof Error
|
|
68
|
+
? (error as Error & { detail?: unknown }).detail
|
|
69
|
+
: undefined;
|
|
70
|
+
return {
|
|
71
|
+
code: error instanceof Error && error.name.startsWith('CODEX_CAPABILITY_')
|
|
72
|
+
? error.name
|
|
73
|
+
: 'ORCHESTRATION_CLIENT_PREFLIGHT_FAILED',
|
|
74
|
+
message: error instanceof Error ? error.message : String(error),
|
|
75
|
+
...(error instanceof Error
|
|
76
|
+
&& error.name === 'CODEX_CAPABILITY_PROVENANCE_MISMATCH'
|
|
77
|
+
&& isCodexCapabilityProvenanceDetail(detailValue)
|
|
78
|
+
? { detail: detailValue }
|
|
79
|
+
: {})
|
|
59
80
|
};
|
|
60
81
|
}
|
|
61
82
|
|
|
@@ -71,11 +92,54 @@ function requiredStore(options: CodexBridgeOptions): LifecycleStore {
|
|
|
71
92
|
return options.store;
|
|
72
93
|
}
|
|
73
94
|
|
|
74
|
-
function controllerContext(
|
|
95
|
+
function controllerContext(
|
|
96
|
+
repoRoot: string,
|
|
97
|
+
brokerController: Readonly<{ instanceDigest: string; controlGeneration: string }> | null
|
|
98
|
+
) {
|
|
75
99
|
const contextPath = process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT;
|
|
76
|
-
return
|
|
77
|
-
|
|
78
|
-
|
|
100
|
+
if (!contextPath) return null;
|
|
101
|
+
if (brokerController) {
|
|
102
|
+
return verifyControllerContextFile(contextPath, {
|
|
103
|
+
repoRoot,
|
|
104
|
+
generation: process.env.AGENT_INFRA_CONTROL_GENERATION ?? brokerController.controlGeneration
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return verifyCodexSandboxControllerContext(contextPath, { repoRoot });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function brokerBindingConflictsWithContext(
|
|
111
|
+
repoRoot: string,
|
|
112
|
+
brokerController: Readonly<{ instanceDigest: string; controlGeneration: string }> | null
|
|
113
|
+
): boolean {
|
|
114
|
+
const contextPath = process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT;
|
|
115
|
+
if (!contextPath || !brokerController) return false;
|
|
116
|
+
const context = verifyControllerContextFile(contextPath, {
|
|
117
|
+
repoRoot,
|
|
118
|
+
generation: process.env.AGENT_INFRA_CONTROL_GENERATION ?? brokerController.controlGeneration
|
|
119
|
+
});
|
|
120
|
+
return context.controllerInstanceDigest !== brokerController.instanceDigest
|
|
121
|
+
|| context.controlGeneration !== brokerController.controlGeneration;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function brokerControllerBinding(): Readonly<{ instanceDigest: string; controlGeneration: string }> | null {
|
|
125
|
+
const raw = process.env.AGENT_INFRA_CONTROL_CONTROLLER_BINDING;
|
|
126
|
+
if (!raw) return null;
|
|
127
|
+
let value: unknown;
|
|
128
|
+
try {
|
|
129
|
+
value = JSON.parse(raw);
|
|
130
|
+
} catch {
|
|
131
|
+
throw new Error('CODEX_SANDBOX_CONTROLLER_BINDING_INVALID');
|
|
132
|
+
}
|
|
133
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
134
|
+
throw new Error('CODEX_SANDBOX_CONTROLLER_BINDING_INVALID');
|
|
135
|
+
}
|
|
136
|
+
const binding = value as Record<string, unknown>;
|
|
137
|
+
if (Object.keys(binding).sort().join(',') !== 'controlGeneration,instanceDigest'
|
|
138
|
+
|| typeof binding.instanceDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(binding.instanceDigest)
|
|
139
|
+
|| typeof binding.controlGeneration !== 'string' || binding.controlGeneration.length === 0) {
|
|
140
|
+
throw new Error('CODEX_SANDBOX_CONTROLLER_BINDING_INVALID');
|
|
141
|
+
}
|
|
142
|
+
return binding as { instanceDigest: string; controlGeneration: string };
|
|
79
143
|
}
|
|
80
144
|
|
|
81
145
|
async function prepareCodexOrchestrationDelegation(
|
|
@@ -101,36 +165,101 @@ async function prepareCodexOrchestrationDelegation(
|
|
|
101
165
|
);
|
|
102
166
|
}
|
|
103
167
|
const buildIdentity = options.buildIdentity ?? computeLifecycleBuildIdentity(repoRoot);
|
|
104
|
-
const
|
|
168
|
+
const brokerController = brokerControllerBinding();
|
|
169
|
+
if (brokerBindingConflictsWithContext(repoRoot, brokerController)) {
|
|
170
|
+
return bridgeFailure(
|
|
171
|
+
'CODEX_SANDBOX_CONTROLLER_BINDING_MISMATCH',
|
|
172
|
+
'broker and local controller bindings do not match'
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
const localController = controllerContext(repoRoot, brokerController);
|
|
176
|
+
if (localController && brokerController
|
|
177
|
+
&& (localController.controllerInstanceDigest !== brokerController.instanceDigest
|
|
178
|
+
|| localController.controlGeneration !== brokerController.controlGeneration)) {
|
|
179
|
+
return bridgeFailure(
|
|
180
|
+
'CODEX_SANDBOX_CONTROLLER_BINDING_MISMATCH',
|
|
181
|
+
'broker and local controller bindings do not match'
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
const controller = brokerController ?? (localController ? {
|
|
185
|
+
instanceDigest: localController.controllerInstanceDigest,
|
|
186
|
+
controlGeneration: localController.controlGeneration
|
|
187
|
+
} : null);
|
|
105
188
|
const capabilityStore = options.capabilityStore ?? createCodexCapabilityStore();
|
|
106
|
-
const
|
|
189
|
+
const attested = capabilityStore.inspect(input.capabilityToken);
|
|
190
|
+
const lifecycleProvenance = {
|
|
191
|
+
...attested.buildIdentity,
|
|
192
|
+
hookDefinitionHash: preflight.hookDefinitionHash,
|
|
193
|
+
...preflight.hookProvenance,
|
|
194
|
+
capabilitySessionId: attested.sessionId!,
|
|
195
|
+
capabilityTurnId: attested.turnId!,
|
|
196
|
+
capabilityToolUseId: attested.toolUseId!,
|
|
197
|
+
controllerInstanceDigest: controller?.instanceDigest ?? null,
|
|
198
|
+
controlGeneration: controller?.controlGeneration ?? null
|
|
199
|
+
};
|
|
200
|
+
const capabilityExpected = {
|
|
107
201
|
taskId: resolved.taskId,
|
|
108
202
|
hookDefinitionHash: preflight.hookDefinitionHash,
|
|
109
203
|
buildIdentity,
|
|
110
204
|
...(controller ? { controller: {
|
|
111
|
-
instanceDigest: controller.
|
|
205
|
+
instanceDigest: controller.instanceDigest,
|
|
112
206
|
controlGeneration: controller.controlGeneration
|
|
113
207
|
} } : {})
|
|
114
|
-
}
|
|
208
|
+
};
|
|
115
209
|
return prepareOrchestrationDelegation(taskRef, {
|
|
116
210
|
...input,
|
|
117
|
-
lifecycleProvenance
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
211
|
+
lifecycleProvenance
|
|
212
|
+
}, {
|
|
213
|
+
...coreOptions(options),
|
|
214
|
+
validateLifecycleCapability: () => {
|
|
215
|
+
try {
|
|
216
|
+
const validated = capabilityStore.validate(input.capabilityToken!, capabilityExpected);
|
|
217
|
+
if (validated.sessionId !== attested.sessionId
|
|
218
|
+
|| validated.turnId !== attested.turnId
|
|
219
|
+
|| validated.toolUseId !== attested.toolUseId) {
|
|
220
|
+
return {
|
|
221
|
+
code: 'CODEX_CAPABILITY_IDENTITY_CHANGED',
|
|
222
|
+
message: 'capability identity changed during validation'
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
return null;
|
|
226
|
+
} catch (error) {
|
|
227
|
+
return capabilityFailure(error);
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
consumeLifecycleCapability: () => {
|
|
231
|
+
try {
|
|
232
|
+
const consumed = capabilityStore.consume(input.capabilityToken!, capabilityExpected);
|
|
233
|
+
if (consumed.sessionId !== attested.sessionId
|
|
234
|
+
|| consumed.turnId !== attested.turnId
|
|
235
|
+
|| consumed.toolUseId !== attested.toolUseId) {
|
|
236
|
+
return {
|
|
237
|
+
code: 'CODEX_CAPABILITY_IDENTITY_CHANGED',
|
|
238
|
+
message: 'capability identity changed during consumption'
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
return null;
|
|
242
|
+
} catch (error) {
|
|
243
|
+
return capabilityFailure(error);
|
|
244
|
+
}
|
|
126
245
|
}
|
|
127
|
-
}
|
|
246
|
+
});
|
|
128
247
|
} catch (error) {
|
|
248
|
+
if (error instanceof OrchestrationStateError) return bridgeFailure(error.code, error.message);
|
|
249
|
+
const detailValue = error instanceof Error
|
|
250
|
+
? (error as Error & { detail?: unknown }).detail
|
|
251
|
+
: undefined;
|
|
252
|
+
const detail = error instanceof Error
|
|
253
|
+
&& error.name === 'CODEX_CAPABILITY_PROVENANCE_MISMATCH'
|
|
254
|
+
&& isCodexCapabilityProvenanceDetail(detailValue)
|
|
255
|
+
? detailValue
|
|
256
|
+
: undefined;
|
|
129
257
|
return bridgeFailure(
|
|
130
258
|
error instanceof Error && error.name.startsWith('CODEX_CAPABILITY_')
|
|
131
259
|
? error.name
|
|
132
260
|
: 'ORCHESTRATION_CLIENT_PREFLIGHT_FAILED',
|
|
133
|
-
error instanceof Error ? error.message : String(error)
|
|
261
|
+
error instanceof Error ? error.message : String(error),
|
|
262
|
+
detail
|
|
134
263
|
);
|
|
135
264
|
}
|
|
136
265
|
}
|
|
@@ -161,14 +290,8 @@ async function activateCodexOrchestrationDelegation(
|
|
|
161
290
|
});
|
|
162
291
|
const buildIdentity = options.buildIdentity ?? computeLifecycleBuildIdentity(repoRoot);
|
|
163
292
|
const contextPath = process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT;
|
|
164
|
-
const contextTaskId = contextPath
|
|
165
|
-
? (JSON.parse(fs.readFileSync(contextPath, 'utf8')) as { taskId?: unknown }).taskId
|
|
166
|
-
: null;
|
|
167
|
-
if (contextPath && typeof contextTaskId !== 'string') {
|
|
168
|
-
throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
|
|
169
|
-
}
|
|
170
293
|
const controller = contextPath
|
|
171
|
-
? verifyCodexSandboxControllerContext(contextPath,
|
|
294
|
+
? verifyCodexSandboxControllerContext(contextPath, { repoRoot })
|
|
172
295
|
: null;
|
|
173
296
|
return activateMatchingOrchestrationDelegation('codex', {
|
|
174
297
|
nativeAgent: evidence.nativeAgent,
|
|
@@ -196,6 +319,7 @@ async function activateCodexOrchestrationDelegation(
|
|
|
196
319
|
}
|
|
197
320
|
}, coreOptions(options));
|
|
198
321
|
} catch (error) {
|
|
322
|
+
if (error instanceof OrchestrationStateError) return bridgeFailure(error.code, error.message);
|
|
199
323
|
return pauseBridge('ORCHESTRATION_CODEX_START_FAILED', error instanceof Error ? error.message : String(error), options);
|
|
200
324
|
}
|
|
201
325
|
}
|
|
@@ -223,6 +347,7 @@ async function activateCodexSpawnDelegation(
|
|
|
223
347
|
resolveThread: async () => resolved
|
|
224
348
|
});
|
|
225
349
|
} catch (error) {
|
|
350
|
+
if (error instanceof OrchestrationStateError) return bridgeFailure(error.code, error.message);
|
|
226
351
|
return pauseBridge('ORCHESTRATION_CODEX_START_FAILED', error instanceof Error ? error.message : String(error), options);
|
|
227
352
|
}
|
|
228
353
|
}
|
|
@@ -238,7 +363,11 @@ async function sealCodexOrchestrationDelegation(
|
|
|
238
363
|
const store = requiredStore(options);
|
|
239
364
|
const existing = store.read(childThreadId);
|
|
240
365
|
if (!existing.consumer) {
|
|
241
|
-
|
|
366
|
+
const stopTurnId = existing.state.stop?.turnId;
|
|
367
|
+
if (!stopTurnId) {
|
|
368
|
+
return pauseBridge('ORCHESTRATION_CODEX_STOP_EVIDENCE_INVALID', 'Codex lifecycle stop hook is not available', options);
|
|
369
|
+
}
|
|
370
|
+
store.apply(await (options.resolveTerminal ?? resolveCodexTerminal)(childThreadId, stopTurnId));
|
|
242
371
|
}
|
|
243
372
|
const record = store.read(childThreadId);
|
|
244
373
|
const evidence = record.state.stopEvidence;
|
|
@@ -259,6 +388,7 @@ async function sealCodexOrchestrationDelegation(
|
|
|
259
388
|
coreOptions(options)
|
|
260
389
|
);
|
|
261
390
|
} catch (error) {
|
|
391
|
+
if (error instanceof OrchestrationStateError) return bridgeFailure(error.code, error.message);
|
|
262
392
|
return pauseBridge('ORCHESTRATION_CODEX_STOP_FAILED', error instanceof Error ? error.message : String(error), options);
|
|
263
393
|
}
|
|
264
394
|
}
|
|
@@ -280,7 +410,10 @@ async function sealCodexParentDelegation(
|
|
|
280
410
|
const start = active.state.startEvidence!;
|
|
281
411
|
const child = active.state.child!;
|
|
282
412
|
if (!active.state.terminal) {
|
|
283
|
-
|
|
413
|
+
const resolveTerminal = options.resolveTerminal ?? resolveCodexTerminal;
|
|
414
|
+
store.apply(active.state.stop
|
|
415
|
+
? await resolveTerminal(start.childThreadId, active.state.stop.turnId)
|
|
416
|
+
: await resolveTerminal(start.childThreadId));
|
|
284
417
|
}
|
|
285
418
|
if (!store.read(start.childThreadId).state.stop) {
|
|
286
419
|
store.apply({
|
|
@@ -294,6 +427,7 @@ async function sealCodexParentDelegation(
|
|
|
294
427
|
}
|
|
295
428
|
return sealCodexOrchestrationDelegation(start.childThreadId, options);
|
|
296
429
|
} catch (error) {
|
|
430
|
+
if (error instanceof OrchestrationStateError) return bridgeFailure(error.code, error.message);
|
|
297
431
|
if (error instanceof Error && error.message === 'CODEX_TURN_NOT_TERMINAL') {
|
|
298
432
|
return bridgeFailure('ORCHESTRATION_DELEGATION_MISSING', 'The matching Codex child has not completed');
|
|
299
433
|
}
|