@fitlab-ai/agent-infra 0.9.5 → 0.9.7
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 +8 -3
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +171 -20
- package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +162 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +244 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -2
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +370 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +68 -8
- package/dist/lib/agent-clients/adapters/codex.js +1 -1
- package/dist/lib/defaults.json +10 -2
- package/dist/lib/git/workflow.js +30 -4
- package/dist/lib/internal/codex-lifecycle.js +169 -3
- package/dist/lib/internal/codex-sandbox-controller.js +106 -0
- package/dist/lib/internal/release-workflow.js +108 -11
- package/dist/lib/internal/sandbox-control.js +43 -6
- package/dist/lib/internal/task-create.js +12 -4
- package/dist/lib/internal/task-orchestration.js +40 -7
- package/dist/lib/platform/context.js +6 -4
- package/dist/lib/platform/github-client.js +55 -2
- package/dist/lib/platform/issue-comments.js +2 -1
- package/dist/lib/process-data/checkpoint.js +319 -0
- package/dist/lib/process-data/index.js +128 -60
- package/dist/lib/process-data/materialize.js +84 -0
- package/dist/lib/process-data/normalize.js +34 -1
- package/dist/lib/process-data/reconcile.js +55 -1
- package/dist/lib/process-data/sources.js +422 -1
- package/dist/lib/process-data/store.js +341 -11
- package/dist/lib/release/workflow.js +29 -21
- package/dist/lib/sandbox/commands/create.js +111 -60
- package/dist/lib/sandbox/commands/prune.js +40 -16
- package/dist/lib/sandbox/commands/rm.js +372 -94
- package/dist/lib/sandbox/commands/start.js +4 -3
- package/dist/lib/sandbox/control/client.js +138 -22
- package/dist/lib/sandbox/control/container-identity.js +64 -0
- package/dist/lib/sandbox/control/executor.js +142 -0
- package/dist/lib/sandbox/control/lifecycle.js +557 -0
- package/dist/lib/sandbox/control/protocol.js +69 -26
- package/dist/lib/sandbox/control/server.js +395 -99
- package/dist/lib/sandbox/control/state.js +191 -0
- package/dist/lib/sandbox/control/timing.js +8 -0
- package/dist/lib/sandbox/index.js +2 -2
- package/dist/lib/sandbox/managed-fs.js +14 -2
- package/dist/lib/sandbox/mounts.js +22 -19
- package/dist/lib/sandbox/recovery.js +284 -50
- package/dist/lib/sandbox/workspace-view.js +90 -10
- package/dist/lib/sandbox/worktree-safety.js +196 -0
- package/dist/lib/server/daemon.js +1 -1
- package/dist/lib/server/process-control.js +17 -1
- package/dist/lib/server/process-state.js +57 -20
- package/dist/lib/task/artifact-lifecycle.js +87 -15
- package/dist/lib/task/artifact-receipts.js +128 -0
- package/dist/lib/task/codex-orchestration.js +236 -0
- package/dist/lib/task/commands/status.js +3 -0
- package/dist/lib/task/commands/validate.js +289 -0
- package/dist/lib/task/commit-finalization.js +36 -9
- package/dist/lib/task/create.js +10 -2
- package/dist/lib/task/delegation-receipts.js +109 -2
- package/dist/lib/task/events.js +149 -6
- package/dist/lib/task/finalization-artifacts.js +2 -1
- package/dist/lib/task/implementation-inputs.js +1 -7
- package/dist/lib/task/index.js +9 -0
- package/dist/lib/task/ledger.js +13 -1
- package/dist/lib/task/orchestration.js +365 -15
- package/dist/lib/task/sections.js +5 -1
- package/dist/lib/task/task-execution-lock.js +5 -4
- package/dist/lib/task/verification-engine.js +78 -14
- package/dist/lib/task/verification.js +1 -0
- package/dist/lifecycle-build-manifest.json +6 -0
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +202 -18
- package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +227 -0
- package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +321 -0
- package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -3
- package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +26 -0
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +453 -0
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +83 -6
- package/lib/agent-clients/adapters/codex.ts +1 -1
- package/lib/defaults.json +10 -2
- package/lib/git/workflow.ts +25 -3
- package/lib/internal/codex-lifecycle.ts +175 -4
- package/lib/internal/codex-sandbox-controller.ts +109 -0
- package/lib/internal/release-workflow.ts +107 -11
- package/lib/internal/sandbox-control.ts +38 -6
- package/lib/internal/task-create.ts +16 -4
- package/lib/internal/task-orchestration.ts +34 -7
- package/lib/platform/context.ts +8 -5
- package/lib/platform/github-client.ts +61 -3
- package/lib/platform/issue-comments.ts +2 -1
- package/lib/process-data/checkpoint.ts +335 -0
- package/lib/process-data/index.ts +112 -60
- package/lib/process-data/materialize.ts +83 -0
- package/lib/process-data/normalize.ts +29 -1
- package/lib/process-data/reconcile.ts +64 -2
- package/lib/process-data/sources.ts +449 -2
- package/lib/process-data/store.ts +304 -11
- package/lib/process-data/types.ts +124 -2
- package/lib/release/workflow.ts +69 -20
- package/lib/sandbox/commands/create.ts +70 -25
- package/lib/sandbox/commands/prune.ts +53 -15
- package/lib/sandbox/commands/rm.ts +423 -108
- package/lib/sandbox/commands/start.ts +4 -3
- package/lib/sandbox/control/client.ts +152 -28
- package/lib/sandbox/control/container-identity.ts +93 -0
- package/lib/sandbox/control/executor.ts +167 -0
- package/lib/sandbox/control/lifecycle.ts +573 -0
- package/lib/sandbox/control/protocol.ts +121 -68
- package/lib/sandbox/control/server.ts +398 -109
- package/lib/sandbox/control/state.ts +213 -0
- package/lib/sandbox/control/timing.ts +12 -0
- package/lib/sandbox/index.ts +2 -2
- package/lib/sandbox/managed-fs.ts +17 -2
- package/lib/sandbox/mounts.ts +16 -8
- package/lib/sandbox/recovery.ts +335 -65
- package/lib/sandbox/workspace-view.ts +102 -11
- package/lib/sandbox/worktree-safety.ts +234 -0
- package/lib/server/daemon.ts +1 -1
- package/lib/server/process-control.ts +24 -3
- package/lib/server/process-state.ts +82 -27
- package/lib/task/artifact-lifecycle.ts +78 -14
- package/lib/task/artifact-receipts.ts +161 -0
- package/lib/task/codex-orchestration.ts +319 -0
- package/lib/task/commands/status.ts +2 -0
- package/lib/task/commands/validate.ts +282 -0
- package/lib/task/commit-finalization.ts +52 -9
- package/lib/task/create.ts +11 -2
- package/lib/task/delegation-receipts.ts +208 -7
- package/lib/task/events.ts +131 -5
- package/lib/task/finalization-artifacts.ts +2 -1
- package/lib/task/implementation-inputs.ts +1 -5
- package/lib/task/index.ts +8 -0
- package/lib/task/ledger.ts +16 -1
- package/lib/task/orchestration.ts +552 -18
- package/lib/task/sections.ts +6 -2
- package/lib/task/task-execution-lock.ts +11 -9
- package/lib/task/verification-engine.ts +93 -24
- package/lib/task/verification.ts +2 -1
- package/package.json +1 -1
- package/templates/.agents/README.en.md +7 -0
- package/templates/.agents/README.zh-CN.md +5 -0
- package/templates/.agents/hooks/lifecycle-delegation.js +72 -12
- package/templates/.agents/rules/lifecycle-orchestration.en.md +8 -2
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +8 -2
- package/templates/.agents/rules/no-mid-flow-questions.en.md +1 -0
- package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +1 -0
- package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
- package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +1 -1
- package/templates/.agents/rules/review-handshake.en.md +3 -1
- package/templates/.agents/rules/review-handshake.zh-CN.md +3 -1
- package/templates/.agents/skills/code-task/reference/dual-mode.en.md +3 -1
- package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +3 -1
- package/templates/.agents/skills/complete-task/SKILL.en.md +4 -0
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +4 -0
- package/templates/.agents/skills/post-release/SKILL.en.md +32 -6
- package/templates/.agents/skills/post-release/SKILL.zh-CN.md +32 -6
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +42 -0
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +42 -0
- package/templates/.agents/skills/run-manual-validation/config/verify.en.json +25 -0
- package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +25 -0
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +67 -0
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +67 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +69 -0
- package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +69 -0
- package/templates/.agents/skills/run-task/SKILL.en.md +9 -7
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +9 -7
- package/templates/.agents/skills/run-task/reference/host-validation.en.md +5 -3
- package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +5 -3
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +75 -3
- package/templates/.claude/commands/run-manual-validation.en.md +9 -0
- package/templates/.claude/commands/run-manual-validation.zh-CN.md +9 -0
- package/templates/.codex/hooks.json +8 -8
- package/templates/.opencode/commands/run-manual-validation.en.md +11 -0
- package/templates/.opencode/commands/run-manual-validation.zh-CN.md +11 -0
- package/templates/.agents/workspace/README.en.md +0 -26
- package/templates/.agents/workspace/README.zh-CN.md +0 -26
|
@@ -12,7 +12,7 @@ import type { CommitAttempt } from './activity-log.ts';
|
|
|
12
12
|
import { commitIntentPath, digest, readCommitIntent } from './commit-intent.ts';
|
|
13
13
|
import type { CommitIntent } from './commit-intent.ts';
|
|
14
14
|
import { parseTypedTaskFrontmatter } from './frontmatter.ts';
|
|
15
|
-
import { parseReviewSummary } from './review-artifacts.ts';
|
|
15
|
+
import { artifactName, maxRound, parseReviewSummary } from './review-artifacts.ts';
|
|
16
16
|
import {
|
|
17
17
|
extractReviewBaseline,
|
|
18
18
|
extractReviewedSnapshotTree,
|
|
@@ -43,6 +43,7 @@ type CommitFinalizationInspection = Readonly<{
|
|
|
43
43
|
currentHead: string;
|
|
44
44
|
committedHead: string | null;
|
|
45
45
|
commitNote: string | null;
|
|
46
|
+
activityNote: string | null;
|
|
46
47
|
needsAnchor: boolean;
|
|
47
48
|
needsLog: boolean;
|
|
48
49
|
intent: CommitIntent | null;
|
|
@@ -87,6 +88,28 @@ function isAncestor(repoRoot: string, ancestor: string, descendant: string): boo
|
|
|
87
88
|
}).status === 0;
|
|
88
89
|
}
|
|
89
90
|
|
|
91
|
+
function hasMatchingApprovedReview(
|
|
92
|
+
taskDir: string,
|
|
93
|
+
beforeRound: number,
|
|
94
|
+
baseline: string,
|
|
95
|
+
tree: string
|
|
96
|
+
): boolean {
|
|
97
|
+
const entries = fs.readdirSync(taskDir);
|
|
98
|
+
for (let round = Math.min(beforeRound - 1, maxRound(entries, 'review-code')); round >= 1; round -= 1) {
|
|
99
|
+
const file = path.join(taskDir, artifactName('review-code', round));
|
|
100
|
+
if (!fs.existsSync(file)) continue;
|
|
101
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
102
|
+
const summary = parseReviewSummary(content);
|
|
103
|
+
if (
|
|
104
|
+
summary.ok
|
|
105
|
+
&& summary.summary.verdict === 'Approved'
|
|
106
|
+
&& extractReviewBaseline(content) === baseline
|
|
107
|
+
&& extractReviewedSnapshotTree(content) === tree
|
|
108
|
+
) return true;
|
|
109
|
+
}
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
|
|
90
113
|
function outcome(
|
|
91
114
|
disposition: CommitFinalizationDisposition,
|
|
92
115
|
overrides: Partial<CommitFinalizationInspection> = {}
|
|
@@ -98,6 +121,7 @@ function outcome(
|
|
|
98
121
|
currentHead: '',
|
|
99
122
|
committedHead: null,
|
|
100
123
|
commitNote: null,
|
|
124
|
+
activityNote: null,
|
|
101
125
|
needsAnchor: false,
|
|
102
126
|
needsLog: false,
|
|
103
127
|
intent: null,
|
|
@@ -212,6 +236,12 @@ function inspectCommitFinalization(
|
|
|
212
236
|
});
|
|
213
237
|
}
|
|
214
238
|
|
|
239
|
+
const commitNote = git(repoRoot, ['show', '-s', '--format=%h%x20%s', intent.committedHead]);
|
|
240
|
+
const matchingDone = commitRows.filter((row) => row.done !== '' && row.note === commitNote);
|
|
241
|
+
const pushOnlyRetry = intent.phase === 'pushed'
|
|
242
|
+
&& intent.baselineHead === intent.committedHead
|
|
243
|
+
&& matchingDone.length === 1
|
|
244
|
+
&& openRows.length === 1;
|
|
215
245
|
const review = findAuthoritativeReviewCodeArtifact(taskDir);
|
|
216
246
|
let needsAnchor = false;
|
|
217
247
|
if (review.ok && review.path) {
|
|
@@ -228,7 +258,16 @@ function inspectCommitFinalization(
|
|
|
228
258
|
return invalid('authoritative review-code evidence is not a valid approved snapshot', currentHead, 'COMMIT_FINALIZATION_EVIDENCE_INVALID', shared);
|
|
229
259
|
}
|
|
230
260
|
const committedTree = git(repoRoot, ['rev-parse', `${intent.committedHead}^{tree}`]);
|
|
231
|
-
|
|
261
|
+
const matchingCurrentReview = reviewBaseline === intent.baselineHead && reviewTree === committedTree;
|
|
262
|
+
if (
|
|
263
|
+
!matchingCurrentReview
|
|
264
|
+
&& !hasMatchingApprovedReview(
|
|
265
|
+
taskDir,
|
|
266
|
+
review.round,
|
|
267
|
+
intent.baselineHead,
|
|
268
|
+
committedTree
|
|
269
|
+
)
|
|
270
|
+
) {
|
|
232
271
|
return outcome('conflict', {
|
|
233
272
|
...shared,
|
|
234
273
|
code: 'COMMIT_FINALIZATION_CONFLICT',
|
|
@@ -237,18 +276,16 @@ function inspectCommitFinalization(
|
|
|
237
276
|
}
|
|
238
277
|
|
|
239
278
|
const anchor = String(metadata.last_reviewed_commit ?? '').trim();
|
|
240
|
-
if (anchor && anchor !== intent.committedHead) {
|
|
279
|
+
if (anchor && anchor !== intent.committedHead && anchor !== intent.baselineHead) {
|
|
241
280
|
return outcome('conflict', {
|
|
242
281
|
...shared,
|
|
243
282
|
code: 'COMMIT_FINALIZATION_CONFLICT',
|
|
244
283
|
message: 'task review anchor conflicts with the committed intent'
|
|
245
284
|
});
|
|
246
285
|
}
|
|
247
|
-
needsAnchor = anchor
|
|
286
|
+
needsAnchor = anchor !== intent.committedHead;
|
|
248
287
|
}
|
|
249
|
-
const
|
|
250
|
-
const matchingDone = commitRows.filter((row) => row.done !== '' && row.note === commitNote);
|
|
251
|
-
const needsLog = matchingDone.length === 0 && openRows.length === 1;
|
|
288
|
+
const needsLog = (matchingDone.length === 0 && openRows.length === 1) || pushOnlyRetry;
|
|
252
289
|
if (!(needsLog || (matchingDone.length === 1 && openRows.length === 0))) {
|
|
253
290
|
return outcome('conflict', {
|
|
254
291
|
...shared,
|
|
@@ -261,6 +298,7 @@ function inspectCommitFinalization(
|
|
|
261
298
|
...shared,
|
|
262
299
|
message: 'commit finalization can be recovered',
|
|
263
300
|
commitNote,
|
|
301
|
+
activityNote: pushOnlyRetry ? `Pushed ${commitNote}` : commitNote,
|
|
264
302
|
needsAnchor,
|
|
265
303
|
needsLog
|
|
266
304
|
});
|
|
@@ -272,7 +310,12 @@ function planCommitTaskFinalization(
|
|
|
272
310
|
agent: string,
|
|
273
311
|
timestamp: string
|
|
274
312
|
): CommitTaskFinalizationPlan {
|
|
275
|
-
if (
|
|
313
|
+
if (
|
|
314
|
+
inspection.disposition !== 'recoverable'
|
|
315
|
+
|| !inspection.committedHead
|
|
316
|
+
|| !inspection.commitNote
|
|
317
|
+
|| !inspection.activityNote
|
|
318
|
+
) {
|
|
276
319
|
throw new Error('commit finalization task mutations require a recoverable inspection');
|
|
277
320
|
}
|
|
278
321
|
const taskContent = fs.readFileSync(path.join(taskDir, 'task.md'), 'utf8');
|
|
@@ -293,7 +336,7 @@ function planCommitTaskFinalization(
|
|
|
293
336
|
time: timestamp,
|
|
294
337
|
step: 'Commit',
|
|
295
338
|
agent,
|
|
296
|
-
note: inspection.
|
|
339
|
+
note: inspection.activityNote
|
|
297
340
|
})
|
|
298
341
|
});
|
|
299
342
|
}
|
package/lib/task/create.ts
CHANGED
|
@@ -231,6 +231,14 @@ function assertRealDirectory(directory: string, code: string): void {
|
|
|
231
231
|
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error(code);
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
+
function ensureRealDirectory(directory: string, code: string): void {
|
|
235
|
+
if (fs.existsSync(directory)) {
|
|
236
|
+
assertRealDirectory(directory, code);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
fs.mkdirSync(directory, { mode: 0o700 });
|
|
240
|
+
}
|
|
241
|
+
|
|
234
242
|
function withCreateLock<T>(repoRoot: string, workspaceRoot: string, operation: () => T): T {
|
|
235
243
|
const lockRoot = path.join(workspaceRoot, '.task-create.lock');
|
|
236
244
|
fs.mkdirSync(lockRoot, { recursive: true, mode: 0o700 });
|
|
@@ -289,8 +297,9 @@ function createLocalTask(value: unknown, options: LocalTaskCreateOptions): Local
|
|
|
289
297
|
const repoRoot = fs.realpathSync.native(options.repoRoot);
|
|
290
298
|
const workspaceRoot = path.join(repoRoot, '.agents', 'workspace');
|
|
291
299
|
const activeRoot = path.join(workspaceRoot, 'active');
|
|
292
|
-
|
|
293
|
-
|
|
300
|
+
ensureRealDirectory(path.join(repoRoot, '.agents'), 'TASK_CREATE_WORKSPACE_INVALID');
|
|
301
|
+
ensureRealDirectory(workspaceRoot, 'TASK_CREATE_WORKSPACE_INVALID');
|
|
302
|
+
ensureRealDirectory(activeRoot, 'TASK_CREATE_WORKSPACE_INVALID');
|
|
294
303
|
const project = readProject(repoRoot);
|
|
295
304
|
const canonical = canonicalTaskCreateCandidate(candidate);
|
|
296
305
|
const candidateDigest = sha256(canonical);
|
|
@@ -6,6 +6,43 @@ import { normalizeAgentToken } from '../agent-clients/tokens.ts';
|
|
|
6
6
|
type DelegationRole = 'executor' | 'reviewer';
|
|
7
7
|
type DelegationStage = 'analysis' | 'review-analysis' | 'plan' | 'review-plan' | 'code' | 'review-code' | 'commit';
|
|
8
8
|
type DelegationStatus = 'prepared' | 'activated' | 'stage-completed' | 'sealed' | 'consumed' | 'aborted' | 'expired';
|
|
9
|
+
type DelegationHostEvidence = Readonly<{
|
|
10
|
+
kind: 'codex-lifecycle-v1' | 'codex-lifecycle-v2';
|
|
11
|
+
hookDefinitionHash: string;
|
|
12
|
+
startRevision: number;
|
|
13
|
+
stopRevision: number | null;
|
|
14
|
+
consumer: string | null;
|
|
15
|
+
consumedAt: string | null;
|
|
16
|
+
protocolVersion?: number;
|
|
17
|
+
packageVersion?: string;
|
|
18
|
+
internalExecutableBuildHash?: string;
|
|
19
|
+
lifecycleContractHash?: string;
|
|
20
|
+
hookSource?: 'project' | 'managed' | 'isolated-user';
|
|
21
|
+
hookSourcePathDigest?: string;
|
|
22
|
+
hookSourceHash?: string;
|
|
23
|
+
capabilitySessionId?: string;
|
|
24
|
+
capabilityTurnId?: string;
|
|
25
|
+
capabilityToolUseId?: string;
|
|
26
|
+
spawnToolUseId?: string;
|
|
27
|
+
spawnObservedAt?: string;
|
|
28
|
+
controllerInstanceDigest?: string | null;
|
|
29
|
+
controlGeneration?: string | null;
|
|
30
|
+
}>;
|
|
31
|
+
type DelegationLifecycleProvenance = Readonly<{
|
|
32
|
+
protocolVersion: number;
|
|
33
|
+
packageVersion: string;
|
|
34
|
+
internalExecutableBuildHash: string;
|
|
35
|
+
lifecycleContractHash: string;
|
|
36
|
+
hookDefinitionHash: string;
|
|
37
|
+
hookSource: 'project' | 'managed' | 'isolated-user';
|
|
38
|
+
hookSourcePathDigest: string;
|
|
39
|
+
hookSourceHash: string;
|
|
40
|
+
capabilitySessionId: string;
|
|
41
|
+
capabilityTurnId: string;
|
|
42
|
+
capabilityToolUseId: string;
|
|
43
|
+
controllerInstanceDigest: string | null;
|
|
44
|
+
controlGeneration: string | null;
|
|
45
|
+
}>;
|
|
9
46
|
|
|
10
47
|
type DelegationReceipt = Readonly<{
|
|
11
48
|
id: string;
|
|
@@ -28,10 +65,19 @@ type DelegationReceipt = Readonly<{
|
|
|
28
65
|
agent: string | null;
|
|
29
66
|
status: DelegationStatus;
|
|
30
67
|
workspaceSnapshotScope?: 'task';
|
|
68
|
+
lifecycleProvenance?: DelegationLifecycleProvenance | null;
|
|
69
|
+
hostEvidence?: DelegationHostEvidence | null;
|
|
31
70
|
beforeFingerprint: string;
|
|
32
71
|
afterFingerprint: string | null;
|
|
33
72
|
changedPaths: readonly string[];
|
|
34
73
|
createdAt: string;
|
|
74
|
+
preparedMonotonicMs: number;
|
|
75
|
+
spawnDispatchMonotonicMs: number | null;
|
|
76
|
+
activationDeadlineMonotonicMs: number | null;
|
|
77
|
+
spawnDispatchedAt: string | null;
|
|
78
|
+
activationDeadlineAt: string | null;
|
|
79
|
+
startEvidenceMonotonicMs: number | null;
|
|
80
|
+
activatedMonotonicMs: number | null;
|
|
35
81
|
activatedAt: string | null;
|
|
36
82
|
sealedAt: string | null;
|
|
37
83
|
consumedAt: string | null;
|
|
@@ -55,9 +101,10 @@ function fail(code: string, message: string): ReceiptFailure {
|
|
|
55
101
|
}
|
|
56
102
|
|
|
57
103
|
function prepareDelegation(
|
|
58
|
-
input: Omit<DelegationReceipt, 'id' | 'requestedModel' | 'requestedReasoningEffort' | 'actualModel' | 'actualReasoningEffort' | 'modelFallbackReason' | 'reasoningEffortFallbackReason' | 'parentId' | 'childId' | 'spawnMode' | 'agent' | 'status' | 'afterFingerprint' | 'changedPaths' | 'createdAt' | 'activatedAt' | 'sealedAt' | 'consumedAt'> & Readonly<{ requestedModel: string; requestedReasoningEffort: string }>,
|
|
59
|
-
options: { id?: () => string; now?: () => string } = {}
|
|
104
|
+
input: Omit<DelegationReceipt, 'id' | 'requestedModel' | 'requestedReasoningEffort' | 'actualModel' | 'actualReasoningEffort' | 'modelFallbackReason' | 'reasoningEffortFallbackReason' | 'parentId' | 'childId' | 'spawnMode' | 'agent' | 'status' | 'hostEvidence' | 'afterFingerprint' | 'changedPaths' | 'createdAt' | 'preparedMonotonicMs' | 'spawnDispatchMonotonicMs' | 'activationDeadlineMonotonicMs' | 'spawnDispatchedAt' | 'activationDeadlineAt' | 'startEvidenceMonotonicMs' | 'activatedMonotonicMs' | 'activatedAt' | 'sealedAt' | 'consumedAt'> & Readonly<{ requestedModel: string; requestedReasoningEffort: string }>,
|
|
105
|
+
options: { id?: () => string; now?: () => string; monotonicNow?: () => number } = {}
|
|
60
106
|
): DelegationReceipt {
|
|
107
|
+
const monotonic = (options.monotonicNow ?? (() => Number(process.hrtime.bigint() / 1_000_000n)))();
|
|
61
108
|
return Object.freeze({
|
|
62
109
|
...input,
|
|
63
110
|
id: (options.id ?? randomUUID)(),
|
|
@@ -70,15 +117,45 @@ function prepareDelegation(
|
|
|
70
117
|
spawnMode: null,
|
|
71
118
|
agent: null,
|
|
72
119
|
status: 'prepared' as const,
|
|
120
|
+
hostEvidence: null,
|
|
73
121
|
afterFingerprint: null,
|
|
74
122
|
changedPaths: Object.freeze([]),
|
|
75
123
|
createdAt: (options.now ?? (() => new Date().toISOString()))(),
|
|
124
|
+
preparedMonotonicMs: monotonic,
|
|
125
|
+
spawnDispatchMonotonicMs: null,
|
|
126
|
+
activationDeadlineMonotonicMs: null,
|
|
127
|
+
spawnDispatchedAt: null,
|
|
128
|
+
activationDeadlineAt: null,
|
|
129
|
+
startEvidenceMonotonicMs: null,
|
|
130
|
+
activatedMonotonicMs: null,
|
|
76
131
|
activatedAt: null,
|
|
77
132
|
sealedAt: null,
|
|
78
133
|
consumedAt: null
|
|
79
134
|
});
|
|
80
135
|
}
|
|
81
136
|
|
|
137
|
+
function dispatchDelegation(
|
|
138
|
+
receipt: DelegationReceipt,
|
|
139
|
+
options: { now?: () => string; monotonicNow?: () => number; activationWindowMs?: number } = {}
|
|
140
|
+
): ReceiptResult {
|
|
141
|
+
if (receipt.status !== 'prepared') {
|
|
142
|
+
return fail('DELEGATION_STATE_INVALID', `delegation ${receipt.id} is ${receipt.status}, expected prepared`);
|
|
143
|
+
}
|
|
144
|
+
if (receipt.spawnDispatchMonotonicMs != null || receipt.spawnDispatchedAt != null) {
|
|
145
|
+
return fail('DELEGATION_DISPATCH_REPLAY', `delegation ${receipt.id} was already dispatched`);
|
|
146
|
+
}
|
|
147
|
+
const monotonic = (options.monotonicNow ?? (() => Number(process.hrtime.bigint() / 1_000_000n)))();
|
|
148
|
+
const dispatchedAt = (options.now ?? (() => new Date().toISOString()))();
|
|
149
|
+
const window = options.activationWindowMs ?? 15_000;
|
|
150
|
+
return { ok: true, receipt: Object.freeze({
|
|
151
|
+
...receipt,
|
|
152
|
+
spawnDispatchMonotonicMs: monotonic,
|
|
153
|
+
activationDeadlineMonotonicMs: monotonic + window,
|
|
154
|
+
spawnDispatchedAt: dispatchedAt,
|
|
155
|
+
activationDeadlineAt: new Date(Date.parse(dispatchedAt) + window).toISOString()
|
|
156
|
+
}) };
|
|
157
|
+
}
|
|
158
|
+
|
|
82
159
|
function activateDelegation(
|
|
83
160
|
receipt: DelegationReceipt,
|
|
84
161
|
event: Readonly<{
|
|
@@ -90,12 +167,46 @@ function activateDelegation(
|
|
|
90
167
|
actualReasoningEffort?: string;
|
|
91
168
|
modelFallbackReason?: string;
|
|
92
169
|
reasoningEffortFallbackReason?: string;
|
|
170
|
+
hostEvidence?: Readonly<{
|
|
171
|
+
kind: 'codex-lifecycle-v1' | 'codex-lifecycle-v2';
|
|
172
|
+
hookDefinitionHash: string;
|
|
173
|
+
startRevision: number;
|
|
174
|
+
protocolVersion?: number;
|
|
175
|
+
packageVersion?: string;
|
|
176
|
+
internalExecutableBuildHash?: string;
|
|
177
|
+
lifecycleContractHash?: string;
|
|
178
|
+
hookSource?: 'project' | 'managed' | 'isolated-user';
|
|
179
|
+
hookSourcePathDigest?: string;
|
|
180
|
+
hookSourceHash?: string;
|
|
181
|
+
capabilitySessionId?: string;
|
|
182
|
+
capabilityTurnId?: string;
|
|
183
|
+
capabilityToolUseId?: string;
|
|
184
|
+
spawnToolUseId?: string;
|
|
185
|
+
spawnObservedAt?: string;
|
|
186
|
+
controllerInstanceDigest?: string | null;
|
|
187
|
+
controlGeneration?: string | null;
|
|
188
|
+
}>;
|
|
93
189
|
}>,
|
|
94
|
-
options: { now?: () => string } = {}
|
|
190
|
+
options: { now?: () => string; monotonicNow?: () => number } = {}
|
|
95
191
|
): ReceiptResult {
|
|
96
192
|
const managedRole = managedDelegationRole(event.nativeAgent);
|
|
97
193
|
if (!managedRole) return fail('DELEGATION_IGNORED', `subagent '${event.nativeAgent}' is not lifecycle-managed`);
|
|
98
194
|
if (receipt.status !== 'prepared') return fail('DELEGATION_STATE_INVALID', `delegation ${receipt.id} is ${receipt.status}, expected prepared`);
|
|
195
|
+
if (
|
|
196
|
+
receipt.spawnDispatchMonotonicMs == null
|
|
197
|
+
|| receipt.spawnDispatchedAt == null
|
|
198
|
+
|| receipt.activationDeadlineAt == null
|
|
199
|
+
|| receipt.activationDeadlineMonotonicMs == null
|
|
200
|
+
|| !Number.isFinite(Date.parse(receipt.spawnDispatchedAt))
|
|
201
|
+
|| !Number.isFinite(Date.parse(receipt.activationDeadlineAt))
|
|
202
|
+
) {
|
|
203
|
+
return fail('DELEGATION_NOT_DISPATCHED', `delegation ${receipt.id} has not reached the native spawn dispatch boundary`);
|
|
204
|
+
}
|
|
205
|
+
const monotonic = (options.monotonicNow ?? (() => Number(process.hrtime.bigint() / 1_000_000n)))();
|
|
206
|
+
const wallNow = Date.parse((options.now ?? (() => new Date().toISOString()))());
|
|
207
|
+
if (wallNow > Date.parse(receipt.activationDeadlineAt)) {
|
|
208
|
+
return fail('DELEGATION_ACTIVATION_TIMEOUT', `delegation ${receipt.id} activation evidence arrived after its deadline`);
|
|
209
|
+
}
|
|
99
210
|
if (managedRole !== receipt.role) return fail('DELEGATION_ROLE_MISMATCH', `managed role ${managedRole} does not match ${receipt.role}`);
|
|
100
211
|
if (!event.parentId || (receipt.parentId !== null && event.parentId !== receipt.parentId) || !event.childId || event.childId === event.parentId) {
|
|
101
212
|
return fail('DELEGATION_IDENTITY_INVALID', 'native parent/child identity does not match the prepared delegation');
|
|
@@ -112,6 +223,10 @@ function activateDelegation(
|
|
|
112
223
|
if (!actualReasoningEffort || actualReasoningEffort.trim() !== actualReasoningEffort) {
|
|
113
224
|
return fail('DELEGATION_REASONING_EFFORT_MISSING', 'native start event must provide a non-empty actual reasoning effort');
|
|
114
225
|
}
|
|
226
|
+
if (
|
|
227
|
+
receipt.client === 'codex'
|
|
228
|
+
&& event.hostEvidence?.kind !== 'codex-lifecycle-v2'
|
|
229
|
+
) return fail('DELEGATION_HOST_EVIDENCE_REQUIRED', 'Codex activation requires trusted lifecycle-v2 host evidence');
|
|
115
230
|
if (
|
|
116
231
|
actualReasoningEffort !== receipt.requestedReasoningEffort
|
|
117
232
|
&& (!event.reasoningEffortFallbackReason || event.reasoningEffortFallbackReason.trim() === '')
|
|
@@ -124,6 +239,42 @@ function activateDelegation(
|
|
|
124
239
|
if (actualReasoningEffort === receipt.requestedReasoningEffort && event.reasoningEffortFallbackReason) {
|
|
125
240
|
return fail('DELEGATION_REASONING_EFFORT_FALLBACK_INVALID', 'reasoning-effort fallback reason is only valid when actual effort differs');
|
|
126
241
|
}
|
|
242
|
+
if (
|
|
243
|
+
event.hostEvidence
|
|
244
|
+
&& (
|
|
245
|
+
receipt.client !== 'codex'
|
|
246
|
+
|| !event.hostEvidence.hookDefinitionHash.trim()
|
|
247
|
+
|| !Number.isSafeInteger(event.hostEvidence.startRevision)
|
|
248
|
+
|| event.hostEvidence.startRevision < 1
|
|
249
|
+
)
|
|
250
|
+
) return fail('DELEGATION_HOST_EVIDENCE_INVALID', 'Codex start evidence reference is invalid');
|
|
251
|
+
if (event.hostEvidence?.kind === 'codex-lifecycle-v2') {
|
|
252
|
+
const expected = receipt.lifecycleProvenance;
|
|
253
|
+
const spawnObservedAt = Date.parse(event.hostEvidence.spawnObservedAt ?? '');
|
|
254
|
+
const spawnDispatchedAt = Date.parse(receipt.spawnDispatchedAt);
|
|
255
|
+
const activationDeadlineAt = Date.parse(receipt.activationDeadlineAt);
|
|
256
|
+
if (
|
|
257
|
+
!expected
|
|
258
|
+
|| event.hostEvidence.protocolVersion !== expected.protocolVersion
|
|
259
|
+
|| event.hostEvidence.packageVersion !== expected.packageVersion
|
|
260
|
+
|| event.hostEvidence.internalExecutableBuildHash !== expected.internalExecutableBuildHash
|
|
261
|
+
|| event.hostEvidence.lifecycleContractHash !== expected.lifecycleContractHash
|
|
262
|
+
|| event.hostEvidence.hookDefinitionHash !== expected.hookDefinitionHash
|
|
263
|
+
|| event.hostEvidence.hookSource !== expected.hookSource
|
|
264
|
+
|| event.hostEvidence.hookSourcePathDigest !== expected.hookSourcePathDigest
|
|
265
|
+
|| event.hostEvidence.hookSourceHash !== expected.hookSourceHash
|
|
266
|
+
|| event.hostEvidence.capabilitySessionId !== expected.capabilitySessionId
|
|
267
|
+
|| event.hostEvidence.capabilityTurnId !== expected.capabilityTurnId
|
|
268
|
+
|| !event.hostEvidence.spawnToolUseId?.trim()
|
|
269
|
+
|| event.hostEvidence.spawnToolUseId === expected.capabilityToolUseId
|
|
270
|
+
|| !Number.isFinite(spawnObservedAt)
|
|
271
|
+
|| spawnObservedAt < spawnDispatchedAt
|
|
272
|
+
|| spawnObservedAt > activationDeadlineAt
|
|
273
|
+
|| event.parentId !== expected.capabilitySessionId
|
|
274
|
+
|| (event.hostEvidence.controllerInstanceDigest ?? null) !== expected.controllerInstanceDigest
|
|
275
|
+
|| (event.hostEvidence.controlGeneration ?? null) !== expected.controlGeneration
|
|
276
|
+
) return fail('DELEGATION_HOST_EVIDENCE_INVALID', 'Codex lifecycle provenance does not match the prepared receipt');
|
|
277
|
+
}
|
|
127
278
|
return { ok: true, receipt: Object.freeze({
|
|
128
279
|
...receipt,
|
|
129
280
|
status: 'activated',
|
|
@@ -134,10 +285,28 @@ function activateDelegation(
|
|
|
134
285
|
actualReasoningEffort,
|
|
135
286
|
modelFallbackReason: event.modelFallbackReason ?? null,
|
|
136
287
|
reasoningEffortFallbackReason: event.reasoningEffortFallbackReason ?? null,
|
|
137
|
-
|
|
288
|
+
hostEvidence: event.hostEvidence ? Object.freeze({
|
|
289
|
+
...event.hostEvidence,
|
|
290
|
+
stopRevision: null,
|
|
291
|
+
consumer: null,
|
|
292
|
+
consumedAt: null
|
|
293
|
+
}) : receipt.hostEvidence ?? null,
|
|
294
|
+
startEvidenceMonotonicMs: monotonic,
|
|
295
|
+
activatedMonotonicMs: (options.monotonicNow ?? (() => Number(process.hrtime.bigint() / 1_000_000n)))(),
|
|
296
|
+
activatedAt: new Date(wallNow).toISOString()
|
|
138
297
|
}) };
|
|
139
298
|
}
|
|
140
299
|
|
|
300
|
+
function abortPreparedDelegation(receipt: DelegationReceipt): ReceiptResult {
|
|
301
|
+
if (receipt.status !== 'prepared') {
|
|
302
|
+
return fail('DELEGATION_STATE_INVALID', `delegation ${receipt.id} is ${receipt.status}, expected prepared`);
|
|
303
|
+
}
|
|
304
|
+
return {
|
|
305
|
+
ok: true,
|
|
306
|
+
receipt: Object.freeze({ ...receipt, status: 'aborted' as const })
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
141
310
|
function completeDelegationStage(
|
|
142
311
|
receipt: DelegationReceipt,
|
|
143
312
|
event: Readonly<{ stage: DelegationStage; round: number; artifact: string; agent: string }>
|
|
@@ -154,10 +323,19 @@ function completeDelegationStage(
|
|
|
154
323
|
|
|
155
324
|
function sealDelegation(
|
|
156
325
|
receipt: DelegationReceipt,
|
|
157
|
-
event: Readonly<{
|
|
158
|
-
|
|
326
|
+
event: Readonly<{
|
|
327
|
+
childId: string;
|
|
328
|
+
exitCode: number;
|
|
329
|
+
afterFingerprint: string;
|
|
330
|
+
changedPaths: readonly string[];
|
|
331
|
+
hostEvidence?: Readonly<{ stopRevision: number; consumer: string; consumedAt: string }>;
|
|
332
|
+
}>,
|
|
333
|
+
options: { now?: () => string; requireHostEvidence?: boolean } = {}
|
|
159
334
|
): ReceiptResult {
|
|
160
335
|
if (receipt.status !== 'stage-completed') return fail('DELEGATION_STATE_INVALID', `delegation ${receipt.id} is ${receipt.status}, expected stage-completed`);
|
|
336
|
+
if (receipt.client === 'codex' && options.requireHostEvidence !== false && !event.hostEvidence) {
|
|
337
|
+
return fail('DELEGATION_HOST_EVIDENCE_REQUIRED', 'Codex sealing requires consumed lifecycle-v2 host evidence');
|
|
338
|
+
}
|
|
161
339
|
if (event.childId !== receipt.childId || event.exitCode !== 0) return fail('DELEGATION_STOP_INVALID', 'native stop identity or exit status is invalid');
|
|
162
340
|
if (receipt.role === 'reviewer') {
|
|
163
341
|
const taskRoot = `.agents/workspace/active/${receipt.taskId}/`;
|
|
@@ -169,11 +347,24 @@ function sealDelegation(
|
|
|
169
347
|
const disallowed = event.changedPaths.find((entry) => !allowed.has(entry));
|
|
170
348
|
if (disallowed) return fail('DELEGATION_REVIEWER_WRITE_FORBIDDEN', `reviewer changed forbidden path '${disallowed}'`);
|
|
171
349
|
}
|
|
350
|
+
if (event.hostEvidence) {
|
|
351
|
+
if (
|
|
352
|
+
!receipt.hostEvidence
|
|
353
|
+
|| !Number.isSafeInteger(event.hostEvidence.stopRevision)
|
|
354
|
+
|| event.hostEvidence.stopRevision <= receipt.hostEvidence.startRevision
|
|
355
|
+
|| event.hostEvidence.consumer !== receipt.id
|
|
356
|
+
|| !event.hostEvidence.consumedAt.trim()
|
|
357
|
+
) return fail('DELEGATION_HOST_EVIDENCE_INVALID', 'Codex stop evidence reference is invalid');
|
|
358
|
+
}
|
|
172
359
|
return { ok: true, receipt: Object.freeze({
|
|
173
360
|
...receipt,
|
|
174
361
|
status: 'sealed',
|
|
175
362
|
afterFingerprint: event.afterFingerprint,
|
|
176
363
|
changedPaths: Object.freeze([...event.changedPaths]),
|
|
364
|
+
hostEvidence: event.hostEvidence && receipt.hostEvidence ? Object.freeze({
|
|
365
|
+
...receipt.hostEvidence,
|
|
366
|
+
...event.hostEvidence
|
|
367
|
+
}) : receipt.hostEvidence ?? null,
|
|
177
368
|
sealedAt: (options.now ?? (() => new Date().toISOString()))()
|
|
178
369
|
}) };
|
|
179
370
|
}
|
|
@@ -190,10 +381,20 @@ function consumeDelegation(receipt: DelegationReceipt, options: { now?: () => st
|
|
|
190
381
|
|
|
191
382
|
export {
|
|
192
383
|
activateDelegation,
|
|
384
|
+
abortPreparedDelegation,
|
|
193
385
|
completeDelegationStage,
|
|
194
386
|
consumeDelegation,
|
|
387
|
+
dispatchDelegation,
|
|
195
388
|
managedDelegationRole,
|
|
196
389
|
prepareDelegation,
|
|
197
390
|
sealDelegation
|
|
198
391
|
};
|
|
199
|
-
export type {
|
|
392
|
+
export type {
|
|
393
|
+
DelegationHostEvidence,
|
|
394
|
+
DelegationLifecycleProvenance,
|
|
395
|
+
DelegationReceipt,
|
|
396
|
+
DelegationRole,
|
|
397
|
+
DelegationStage,
|
|
398
|
+
DelegationStatus,
|
|
399
|
+
ReceiptResult
|
|
400
|
+
};
|