@fitlab-ai/agent-infra 0.9.7 → 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 +11 -1
- package/dist/lib/agent-clients/adapters/claude-code.js +3 -3
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +60 -22
- package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +8 -1
- package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +175 -11
- 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 +86 -69
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +2 -1
- package/dist/lib/decide.js +68 -14
- package/dist/lib/git/workflow.js +10 -1
- package/dist/lib/init.js +1 -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 +3 -4
- package/dist/lib/internal/sandbox-control.js +79 -2
- package/dist/lib/internal/task-activity.js +57 -7
- package/dist/lib/internal/task-event.js +57 -4
- package/dist/lib/internal/task-finalization.js +54 -0
- package/dist/lib/internal/task-ledger.js +59 -5
- package/dist/lib/internal/task-lifecycle.js +59 -8
- package/dist/lib/internal/task-orchestration.js +161 -288
- package/dist/lib/internal/task-override.js +131 -0
- package/dist/lib/internal/task-review.js +64 -6
- package/dist/lib/internal/task-verify.js +65 -3
- package/dist/lib/internal/task-warning.js +57 -4
- package/dist/lib/platform/pr-summary.js +53 -13
- package/dist/lib/platform/pull-requests.js +205 -41
- package/dist/lib/runtime/agent-runtime.js +56 -0
- package/dist/lib/sandbox/commands/create.js +389 -311
- package/dist/lib/sandbox/commands/rm.js +316 -55
- package/dist/lib/sandbox/control/client.js +190 -6
- package/dist/lib/sandbox/control/container-identity.js +1 -1
- package/dist/lib/sandbox/control/controller-registration.js +273 -0
- package/dist/lib/sandbox/control/executor.js +145 -5
- package/dist/lib/sandbox/control/lifecycle.js +395 -73
- package/dist/lib/sandbox/control/protocol.js +67 -6
- package/dist/lib/sandbox/control/state.js +27 -11
- package/dist/lib/sandbox/engine.js +14 -7
- package/dist/lib/sandbox/index.js +5 -3
- package/dist/lib/sandbox/managed-fs.js +5 -0
- package/dist/lib/sandbox/mounts.js +20 -5
- package/dist/lib/sandbox/recovery.js +78 -10
- package/dist/lib/sandbox/task-resolver.js +50 -6
- package/dist/lib/sandbox/workspace-identity.js +27 -0
- package/dist/lib/sandbox/workspace-view.js +62 -40
- package/dist/lib/sandbox/worktree-safety.js +215 -2
- package/dist/lib/server/process-state.js +51 -1
- package/dist/lib/task/activity-intent.js +4 -1
- 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/decision-intents.js +4 -2
- package/dist/lib/task/delegation-receipts.js +279 -17
- package/dist/lib/task/events.js +9 -5
- package/dist/lib/task/finalization.js +567 -0
- package/dist/lib/task/guard-override.js +31 -0
- package/dist/lib/task/human-override.js +1240 -0
- package/dist/lib/task/ledger-intents.js +6 -3
- package/dist/lib/task/lifecycle.js +69 -14
- package/dist/lib/task/operation-outcome.js +33 -0
- package/dist/lib/task/orchestration.js +199 -719
- package/dist/lib/task/review-finalization.js +3 -2
- 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 +138 -59
- package/dist/lib/task/verification.js +15 -2
- package/dist/lib/task/workflow-warning-intents.js +54 -3
- package/dist/lib/task/write.js +2 -1
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapters/claude-code.ts +3 -3
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +63 -20
- package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +12 -0
- package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +278 -13
- 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 +112 -93
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +2 -1
- package/lib/decide.ts +59 -14
- package/lib/git/workflow.ts +11 -2
- package/lib/init.ts +1 -1
- 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 +3 -4
- package/lib/internal/sandbox-control.ts +92 -2
- package/lib/internal/task-activity.ts +46 -6
- package/lib/internal/task-event.ts +53 -4
- package/lib/internal/task-finalization.ts +68 -0
- package/lib/internal/task-ledger.ts +48 -4
- package/lib/internal/task-lifecycle.ts +56 -12
- package/lib/internal/task-orchestration.ts +32 -107
- package/lib/internal/task-override.ts +138 -0
- package/lib/internal/task-review.ts +51 -6
- package/lib/internal/task-verify.ts +52 -3
- package/lib/internal/task-warning.ts +44 -3
- package/lib/platform/pr-summary.ts +65 -14
- package/lib/platform/pull-requests.ts +211 -41
- package/lib/runtime/agent-runtime.ts +76 -0
- package/lib/sandbox/commands/create.ts +88 -4
- package/lib/sandbox/commands/rm.ts +413 -61
- package/lib/sandbox/config.ts +2 -1
- package/lib/sandbox/control/client.ts +255 -6
- package/lib/sandbox/control/container-identity.ts +3 -3
- package/lib/sandbox/control/controller-registration.ts +347 -0
- package/lib/sandbox/control/executor.ts +172 -6
- package/lib/sandbox/control/lifecycle.ts +465 -77
- package/lib/sandbox/control/protocol.ts +85 -9
- package/lib/sandbox/control/state.ts +35 -18
- package/lib/sandbox/engine.ts +20 -9
- package/lib/sandbox/index.ts +5 -3
- package/lib/sandbox/managed-fs.ts +5 -0
- package/lib/sandbox/mounts.ts +23 -5
- package/lib/sandbox/recovery.ts +90 -9
- package/lib/sandbox/task-resolver.ts +58 -7
- package/lib/sandbox/workspace-identity.ts +50 -2
- package/lib/sandbox/workspace-view.ts +73 -43
- package/lib/sandbox/worktree-safety.ts +236 -2
- package/lib/server/process-state.ts +48 -1
- package/lib/task/activity-intent.ts +4 -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/decision-intents.ts +4 -2
- package/lib/task/delegation-receipts.ts +304 -19
- package/lib/task/events.ts +10 -5
- package/lib/task/finalization.ts +719 -0
- package/lib/task/guard-override.ts +50 -0
- package/lib/task/human-override.ts +1503 -0
- package/lib/task/ledger-intents.ts +6 -3
- package/lib/task/lifecycle.ts +71 -14
- package/lib/task/operation-outcome.ts +48 -0
- package/lib/task/orchestration.ts +239 -922
- package/lib/task/review-finalization.ts +6 -2
- package/lib/task/short-id.ts +65 -1
- package/lib/task/task-execution-lock.ts +10 -1
- package/lib/task/verification-engine.ts +145 -64
- package/lib/task/verification.ts +18 -3
- package/lib/task/workflow-warning-intents.ts +61 -4
- package/lib/task/write.ts +4 -1
- 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 +9 -24
- package/templates/.agents/rules/lifecycle-orchestration.en.md +4 -3
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +4 -3
- 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/run-task/reference/host-validation.en.md +3 -1
- package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +3 -1
- 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
|
@@ -7,6 +7,7 @@ import semver from 'semver';
|
|
|
7
7
|
|
|
8
8
|
import type { ChildProcessWithoutNullStreams } from 'node:child_process';
|
|
9
9
|
import type { CodexLifecycleEvent } from './evidence.ts';
|
|
10
|
+
import { resolveAgentRuntimeStoreRoot } from '../../../runtime/agent-runtime.ts';
|
|
10
11
|
|
|
11
12
|
type JsonObject = Record<string, unknown>;
|
|
12
13
|
type ThreadResolution = Readonly<{
|
|
@@ -35,6 +36,10 @@ type AppServerTransportOptions = Readonly<{
|
|
|
35
36
|
timeoutMs?: number;
|
|
36
37
|
rolloutReadAttempts?: number;
|
|
37
38
|
rolloutRetryMs?: number;
|
|
39
|
+
threadReadAttempts?: number;
|
|
40
|
+
threadRetryMs?: number;
|
|
41
|
+
terminalReadAttempts?: number;
|
|
42
|
+
terminalRetryMs?: number;
|
|
38
43
|
}>;
|
|
39
44
|
|
|
40
45
|
type CodexRuntimeIdentity = Readonly<{
|
|
@@ -51,6 +56,10 @@ const LIFECYCLE_HOOKS = Object.freeze([
|
|
|
51
56
|
]);
|
|
52
57
|
const DEFAULT_ROLLOUT_READ_ATTEMPTS = 81;
|
|
53
58
|
const DEFAULT_ROLLOUT_RETRY_MS = 100;
|
|
59
|
+
const DEFAULT_THREAD_READ_ATTEMPTS = 21;
|
|
60
|
+
const DEFAULT_THREAD_RETRY_MS = 100;
|
|
61
|
+
const DEFAULT_TERMINAL_READ_ATTEMPTS = 21;
|
|
62
|
+
const DEFAULT_TERMINAL_RETRY_MS = 100;
|
|
54
63
|
|
|
55
64
|
function object(value: unknown): JsonObject | null {
|
|
56
65
|
return value && typeof value === 'object' && !Array.isArray(value)
|
|
@@ -66,6 +75,7 @@ function resolveCodexSpawnedChild(
|
|
|
66
75
|
transcriptPath: string,
|
|
67
76
|
expected: Readonly<{
|
|
68
77
|
sessionId: string;
|
|
78
|
+
turnId: string;
|
|
69
79
|
toolUseId: string;
|
|
70
80
|
nativeAgent: string;
|
|
71
81
|
taskName: string;
|
|
@@ -110,15 +120,19 @@ function resolveCodexSpawnedChild(
|
|
|
110
120
|
) throw new Error('Codex parent rollout spawn identity does not match the hook event');
|
|
111
121
|
const activities = records.filter((record) => {
|
|
112
122
|
const payload = object(record?.payload);
|
|
123
|
+
const item = object(payload?.item);
|
|
113
124
|
return record?.type === 'event_msg'
|
|
114
|
-
&& payload?.type === '
|
|
115
|
-
&& payload.
|
|
116
|
-
&& payload.
|
|
117
|
-
&&
|
|
118
|
-
&&
|
|
125
|
+
&& payload?.type === 'item_completed'
|
|
126
|
+
&& payload.thread_id === expected.sessionId
|
|
127
|
+
&& payload.turn_id === expected.turnId
|
|
128
|
+
&& item?.type === 'SubAgentActivity'
|
|
129
|
+
&& item.id === expected.toolUseId
|
|
130
|
+
&& item.kind === 'started'
|
|
131
|
+
&& item.agent_path === `/root/${expected.taskName}`
|
|
132
|
+
&& nonEmpty(item.agent_thread_id);
|
|
119
133
|
});
|
|
120
134
|
if (activities.length !== 1) throw new Error('Codex parent rollout child activity was not found uniquely');
|
|
121
|
-
return nonEmpty(object(activities[0]?.payload)?.agent_thread_id)!;
|
|
135
|
+
return nonEmpty(object(object(activities[0]?.payload)?.item)?.agent_thread_id)!;
|
|
122
136
|
}
|
|
123
137
|
|
|
124
138
|
function validateCodexLifecycleHookConfig(value: unknown): void {
|
|
@@ -132,7 +146,7 @@ function validateCodexLifecycleHookConfig(value: unknown): void {
|
|
|
132
146
|
return entry.hooks.some((hookValue) => {
|
|
133
147
|
const hook = object(hookValue);
|
|
134
148
|
return hook?.type === 'command'
|
|
135
|
-
&& hook.timeout ===
|
|
149
|
+
&& hook.timeout === 30
|
|
136
150
|
&& hook.command === `node "$(git rev-parse --show-toplevel)/.agents/hooks/lifecycle-delegation.js" --client codex --event ${phase}`;
|
|
137
151
|
});
|
|
138
152
|
});
|
|
@@ -477,18 +491,30 @@ async function resolveCodexThread(
|
|
|
477
491
|
const readResult = object(await transport.request('thread/read', { threadId: childThreadId, includeTurns: false }));
|
|
478
492
|
const readThread = object(readResult?.thread);
|
|
479
493
|
if (nonEmpty(readThread?.id) !== childThreadId) throw new Error('Codex App Server returned the wrong child thread');
|
|
480
|
-
let
|
|
494
|
+
let rolloutRecords: readonly unknown[];
|
|
481
495
|
try {
|
|
482
|
-
|
|
496
|
+
rolloutRecords = await resolveCodexRolloutRecords(
|
|
483
497
|
readResult,
|
|
484
498
|
childThreadId,
|
|
485
499
|
options.rolloutReadAttempts ?? DEFAULT_ROLLOUT_READ_ATTEMPTS,
|
|
486
500
|
options.rolloutRetryMs ?? DEFAULT_ROLLOUT_RETRY_MS
|
|
487
501
|
);
|
|
488
|
-
resolution = parseCodexThreadResolution(readResult, rolloutRecords);
|
|
489
502
|
} catch {
|
|
490
503
|
throw new Error('Codex App Server rollout metadata is unavailable');
|
|
491
504
|
}
|
|
505
|
+
let resolution = parseCodexThreadResolution(readResult, rolloutRecords);
|
|
506
|
+
const threadReadAttempts = options.threadReadAttempts ?? DEFAULT_THREAD_READ_ATTEMPTS;
|
|
507
|
+
for (let attempt = 1; resolution.thread.forkedFromId !== null && attempt < threadReadAttempts; attempt += 1) {
|
|
508
|
+
await new Promise((resolve) => setTimeout(resolve, options.threadRetryMs ?? DEFAULT_THREAD_RETRY_MS));
|
|
509
|
+
const refreshed = object(await transport.request('thread/read', { threadId: childThreadId, includeTurns: false }));
|
|
510
|
+
if (nonEmpty(object(refreshed?.thread)?.id) !== childThreadId) {
|
|
511
|
+
throw new Error('Codex App Server returned the wrong child thread');
|
|
512
|
+
}
|
|
513
|
+
resolution = parseCodexThreadResolution(refreshed, rolloutRecords);
|
|
514
|
+
}
|
|
515
|
+
if (resolution.thread.forkedFromId !== null) {
|
|
516
|
+
throw new Error('Codex App Server fresh thread metadata is unavailable');
|
|
517
|
+
}
|
|
492
518
|
const reroutes = transport.notifications
|
|
493
519
|
.filter((entry) => entry.method === 'model/rerouted')
|
|
494
520
|
.map((entry) => parseCodexModelReroute(entry.params))
|
|
@@ -506,20 +532,37 @@ async function resolveCodexThread(
|
|
|
506
532
|
|
|
507
533
|
async function resolveCodexTerminal(
|
|
508
534
|
childThreadId: string,
|
|
509
|
-
|
|
535
|
+
expectedTurnIdOrOptions: string | AppServerTransportOptions = {},
|
|
536
|
+
terminalOptions: AppServerTransportOptions = {}
|
|
510
537
|
): Promise<Extract<CodexLifecycleEvent, { type: 'app-terminal' }>> {
|
|
538
|
+
const expectedTurnId = typeof expectedTurnIdOrOptions === 'string' ? expectedTurnIdOrOptions : null;
|
|
539
|
+
const options = typeof expectedTurnIdOrOptions === 'string' ? terminalOptions : expectedTurnIdOrOptions;
|
|
511
540
|
const transport = new CodexAppServerTransport(options);
|
|
512
541
|
transport.start();
|
|
513
542
|
try {
|
|
514
543
|
await transport.initialize();
|
|
515
|
-
const
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
544
|
+
const attempts = expectedTurnId ? options.terminalReadAttempts ?? DEFAULT_TERMINAL_READ_ATTEMPTS : 1;
|
|
545
|
+
let abnormal: Extract<CodexLifecycleEvent, { type: 'app-terminal' }> | null = null;
|
|
546
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
547
|
+
const result = object(await transport.request('thread/read', { threadId: childThreadId, includeTurns: true }));
|
|
548
|
+
const thread = object(result?.thread);
|
|
549
|
+
if (nonEmpty(thread?.id) !== childThreadId) throw new Error('Codex App Server returned the wrong child thread');
|
|
550
|
+
const turns = Array.isArray(thread?.turns) ? thread.turns : [];
|
|
551
|
+
const turn = expectedTurnId
|
|
552
|
+
? turns.find((candidate) => object(candidate)?.id === expectedTurnId)
|
|
553
|
+
: turns.at(-1);
|
|
554
|
+
const turnValue = object(turn);
|
|
555
|
+
if (turnValue && turnValue.status !== 'inProgress') {
|
|
556
|
+
const terminal = parseCodexTurnCompleted({ threadId: childThreadId, turn });
|
|
557
|
+
if (terminal.status === 'completed') return terminal;
|
|
558
|
+
abnormal = terminal;
|
|
559
|
+
}
|
|
560
|
+
if (attempt + 1 < attempts) {
|
|
561
|
+
await new Promise((resolve) => setTimeout(resolve, options.terminalRetryMs ?? DEFAULT_TERMINAL_RETRY_MS));
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
if (abnormal) return abnormal;
|
|
565
|
+
throw new Error('CODEX_TURN_NOT_TERMINAL');
|
|
523
566
|
} finally {
|
|
524
567
|
transport.close();
|
|
525
568
|
}
|
|
@@ -558,7 +601,7 @@ async function preflightCodexLifecycleEvidence(
|
|
|
558
601
|
}
|
|
559
602
|
|
|
560
603
|
const hookDefinitionHash = crypto.createHash('sha256').update(hooksRaw).digest('hex');
|
|
561
|
-
const runtimeRoot =
|
|
604
|
+
const runtimeRoot = resolveAgentRuntimeStoreRoot({ repoRoot, store: 'lifecycle' });
|
|
562
605
|
const runtimeLiveness = hasCodexRuntimeLiveness(runtimeRoot, hookDefinitionHash, runtimeIdentity);
|
|
563
606
|
|
|
564
607
|
const transport = new CodexAppServerTransport();
|
|
@@ -3,6 +3,8 @@ import fs from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
|
|
6
|
+
import semver from 'semver';
|
|
7
|
+
|
|
6
8
|
const LIFECYCLE_PROTOCOL_VERSION = 3;
|
|
7
9
|
|
|
8
10
|
type LifecycleBuildIdentity = Readonly<{
|
|
@@ -32,6 +34,14 @@ function exactText(value: unknown): value is string {
|
|
|
32
34
|
return typeof value === 'string' && value.trim() === value && value.length > 0;
|
|
33
35
|
}
|
|
34
36
|
|
|
37
|
+
function isCanonicalLifecyclePackageVersion(value: unknown): value is string {
|
|
38
|
+
return typeof value === 'string' && semver.valid(value) === value;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isLifecycleProtocolVersion(value: unknown): value is typeof LIFECYCLE_PROTOCOL_VERSION {
|
|
42
|
+
return value === LIFECYCLE_PROTOCOL_VERSION;
|
|
43
|
+
}
|
|
44
|
+
|
|
35
45
|
function normalizeRelativePath(value: string): string {
|
|
36
46
|
return value.replaceAll('\\', '/');
|
|
37
47
|
}
|
|
@@ -215,6 +225,8 @@ function verifyLifecycleBuildIdentity(
|
|
|
215
225
|
export {
|
|
216
226
|
LIFECYCLE_PROTOCOL_VERSION,
|
|
217
227
|
computeLifecycleBuildIdentity,
|
|
228
|
+
isCanonicalLifecyclePackageVersion,
|
|
229
|
+
isLifecycleProtocolVersion,
|
|
218
230
|
readLifecycleManifestFiles,
|
|
219
231
|
resolveLifecycleExecutableFiles,
|
|
220
232
|
verifyLifecycleBuildIdentity
|
|
@@ -2,14 +2,50 @@ import crypto from 'node:crypto';
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
isCanonicalLifecyclePackageVersion,
|
|
7
|
+
isLifecycleProtocolVersion,
|
|
8
|
+
LIFECYCLE_PROTOCOL_VERSION,
|
|
9
|
+
verifyLifecycleBuildIdentity
|
|
10
|
+
} from './build-identity.ts';
|
|
6
11
|
import type { LifecycleBuildIdentity } from './build-identity.ts';
|
|
12
|
+
import { resolveAgentRuntimeStoreRoot } from '../../../runtime/agent-runtime.ts';
|
|
7
13
|
|
|
8
14
|
type CapabilityStatus = 'armed' | 'attested' | 'consumed' | 'expired';
|
|
9
15
|
type CodexControllerBinding = Readonly<{
|
|
10
16
|
instanceDigest: string;
|
|
11
17
|
controlGeneration: string;
|
|
12
18
|
}>;
|
|
19
|
+
type CodexCapabilityProjection = Readonly<
|
|
20
|
+
| { kind: 'absent' }
|
|
21
|
+
| { kind: 'protocol-version'; value: typeof LIFECYCLE_PROTOCOL_VERSION }
|
|
22
|
+
| { kind: 'semver'; value: string }
|
|
23
|
+
| { kind: 'digest-prefix'; value: string }
|
|
24
|
+
| { kind: 'presence'; present: boolean }
|
|
25
|
+
>;
|
|
26
|
+
type CodexCapabilityProvenanceField = Readonly<{
|
|
27
|
+
matches: boolean;
|
|
28
|
+
expected: CodexCapabilityProjection;
|
|
29
|
+
actual: CodexCapabilityProjection;
|
|
30
|
+
}>;
|
|
31
|
+
type CodexCapabilityProvenanceDetail = Readonly<{
|
|
32
|
+
kind: 'codex-capability-provenance-mismatch';
|
|
33
|
+
version: 1;
|
|
34
|
+
fields: Readonly<{
|
|
35
|
+
taskId: CodexCapabilityProvenanceField;
|
|
36
|
+
hookDefinitionHash: CodexCapabilityProvenanceField;
|
|
37
|
+
buildIdentity: Readonly<{
|
|
38
|
+
protocolVersion: CodexCapabilityProvenanceField;
|
|
39
|
+
packageVersion: CodexCapabilityProvenanceField;
|
|
40
|
+
internalExecutableBuildHash: CodexCapabilityProvenanceField;
|
|
41
|
+
lifecycleContractHash: CodexCapabilityProvenanceField;
|
|
42
|
+
}>;
|
|
43
|
+
controller: Readonly<{
|
|
44
|
+
instanceDigest: CodexCapabilityProvenanceField;
|
|
45
|
+
controlGeneration: CodexCapabilityProvenanceField;
|
|
46
|
+
}>;
|
|
47
|
+
}>;
|
|
48
|
+
}>;
|
|
13
49
|
type CodexCapabilityRecord = Readonly<{
|
|
14
50
|
schemaVersion: 1;
|
|
15
51
|
revision: number;
|
|
@@ -44,28 +80,227 @@ const DEFAULT_MAX_RECORDS = 128;
|
|
|
44
80
|
const LOCK_RETRY_MS = 10;
|
|
45
81
|
const LOCK_TIMEOUT_MS = 1_000;
|
|
46
82
|
const LOCK_STALE_MS = 30_000;
|
|
83
|
+
const DETAIL_DOMAIN = 'codex-capability-provenance/v1';
|
|
84
|
+
const TASK_ID_PATTERN = /^TASK-[0-9]{8}-[0-9]{6}$/u;
|
|
85
|
+
const SHA256_PATTERN = /^[0-9a-f]{64}$/u;
|
|
86
|
+
const DIGEST_PREFIX_PATTERN = /^sha256:[0-9a-f]{16}$/u;
|
|
47
87
|
|
|
48
88
|
function digest(value: string): string {
|
|
49
89
|
return crypto.createHash('sha256').update(value).digest('hex');
|
|
50
90
|
}
|
|
51
91
|
|
|
52
|
-
function capabilityError(code: string, message: string): Error {
|
|
92
|
+
function capabilityError(code: string, message: string, detail?: CodexCapabilityProvenanceDetail): Error {
|
|
53
93
|
const error = new Error(`${code}: ${message}`);
|
|
54
94
|
error.name = code;
|
|
95
|
+
if (detail) (error as Error & { detail: CodexCapabilityProvenanceDetail }).detail = detail;
|
|
55
96
|
return error;
|
|
56
97
|
}
|
|
57
98
|
|
|
99
|
+
function objectValue(value: unknown, key: string): unknown {
|
|
100
|
+
if (!value || typeof value !== 'object') return undefined;
|
|
101
|
+
return (value as Record<string, unknown>)[key];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function digestPrefix(fieldName: string, value: string): CodexCapabilityProjection {
|
|
105
|
+
return {
|
|
106
|
+
kind: 'digest-prefix',
|
|
107
|
+
value: `sha256:${crypto.createHash('sha256')
|
|
108
|
+
.update(`${DETAIL_DOMAIN}\0${fieldName}\0${value}`)
|
|
109
|
+
.digest('hex')
|
|
110
|
+
.slice(0, 16)}`
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function projectDigest(fieldName: string, value: unknown, pattern: RegExp): CodexCapabilityProjection {
|
|
115
|
+
return typeof value === 'string' && pattern.test(value) ? digestPrefix(fieldName, value) : { kind: 'absent' };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function projectTaskId(value: unknown): CodexCapabilityProjection {
|
|
119
|
+
return projectDigest('taskId', value, TASK_ID_PATTERN);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function projectHash(fieldName: string, value: unknown): CodexCapabilityProjection {
|
|
123
|
+
return projectDigest(fieldName, value, SHA256_PATTERN);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function projectProtocol(value: unknown): CodexCapabilityProjection {
|
|
127
|
+
return isLifecycleProtocolVersion(value)
|
|
128
|
+
? { kind: 'protocol-version', value: LIFECYCLE_PROTOCOL_VERSION }
|
|
129
|
+
: { kind: 'absent' };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function projectPackageVersion(value: unknown): CodexCapabilityProjection {
|
|
133
|
+
return isCanonicalLifecyclePackageVersion(value)
|
|
134
|
+
? { kind: 'semver', value }
|
|
135
|
+
: { kind: 'absent' };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function projectPresence(value: unknown): CodexCapabilityProjection {
|
|
139
|
+
return { kind: 'presence', present: Boolean(value && typeof value === 'object') };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function field(matches: boolean, expected: CodexCapabilityProjection, actual: CodexCapabilityProjection): CodexCapabilityProvenanceField {
|
|
143
|
+
return { matches, expected, actual };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function provenanceDetail(
|
|
147
|
+
record: CodexCapabilityRecord,
|
|
148
|
+
expected: Readonly<{
|
|
149
|
+
taskId: string;
|
|
150
|
+
hookDefinitionHash: string;
|
|
151
|
+
buildIdentity: LifecycleBuildIdentity;
|
|
152
|
+
controller?: CodexControllerBinding;
|
|
153
|
+
}>
|
|
154
|
+
): CodexCapabilityProvenanceDetail {
|
|
155
|
+
const actualBuild = record.buildIdentity;
|
|
156
|
+
const expectedBuild = expected.buildIdentity;
|
|
157
|
+
const actualController = record.controller;
|
|
158
|
+
const expectedController = expected.controller ?? null;
|
|
159
|
+
return {
|
|
160
|
+
kind: 'codex-capability-provenance-mismatch',
|
|
161
|
+
version: 1,
|
|
162
|
+
fields: {
|
|
163
|
+
taskId: field(
|
|
164
|
+
record.taskId === expected.taskId,
|
|
165
|
+
projectTaskId(expected.taskId),
|
|
166
|
+
projectTaskId(record.taskId)
|
|
167
|
+
),
|
|
168
|
+
hookDefinitionHash: field(
|
|
169
|
+
record.hookDefinitionHash === expected.hookDefinitionHash,
|
|
170
|
+
projectHash('hookDefinitionHash', expected.hookDefinitionHash),
|
|
171
|
+
projectHash('hookDefinitionHash', record.hookDefinitionHash)
|
|
172
|
+
),
|
|
173
|
+
buildIdentity: {
|
|
174
|
+
protocolVersion: field(
|
|
175
|
+
objectValue(actualBuild, 'protocolVersion') === objectValue(expectedBuild, 'protocolVersion'),
|
|
176
|
+
projectProtocol(objectValue(expectedBuild, 'protocolVersion')),
|
|
177
|
+
projectProtocol(objectValue(actualBuild, 'protocolVersion'))
|
|
178
|
+
),
|
|
179
|
+
packageVersion: field(
|
|
180
|
+
objectValue(actualBuild, 'packageVersion') === objectValue(expectedBuild, 'packageVersion'),
|
|
181
|
+
projectPackageVersion(objectValue(expectedBuild, 'packageVersion')),
|
|
182
|
+
projectPackageVersion(objectValue(actualBuild, 'packageVersion'))
|
|
183
|
+
),
|
|
184
|
+
internalExecutableBuildHash: field(
|
|
185
|
+
objectValue(actualBuild, 'internalExecutableBuildHash') === objectValue(expectedBuild, 'internalExecutableBuildHash'),
|
|
186
|
+
projectHash('buildIdentity.internalExecutableBuildHash', objectValue(expectedBuild, 'internalExecutableBuildHash')),
|
|
187
|
+
projectHash('buildIdentity.internalExecutableBuildHash', objectValue(actualBuild, 'internalExecutableBuildHash'))
|
|
188
|
+
),
|
|
189
|
+
lifecycleContractHash: field(
|
|
190
|
+
objectValue(actualBuild, 'lifecycleContractHash') === objectValue(expectedBuild, 'lifecycleContractHash'),
|
|
191
|
+
projectHash('buildIdentity.lifecycleContractHash', objectValue(expectedBuild, 'lifecycleContractHash')),
|
|
192
|
+
projectHash('buildIdentity.lifecycleContractHash', objectValue(actualBuild, 'lifecycleContractHash'))
|
|
193
|
+
)
|
|
194
|
+
},
|
|
195
|
+
controller: {
|
|
196
|
+
instanceDigest: field(
|
|
197
|
+
sameControllerField(actualController, expectedController, 'instanceDigest'),
|
|
198
|
+
projectPresence(objectValue(expectedController, 'instanceDigest') === undefined ? null : expectedController),
|
|
199
|
+
projectPresence(objectValue(actualController, 'instanceDigest') === undefined ? null : actualController)
|
|
200
|
+
),
|
|
201
|
+
controlGeneration: field(
|
|
202
|
+
sameControllerField(actualController, expectedController, 'controlGeneration'),
|
|
203
|
+
projectPresence(objectValue(expectedController, 'controlGeneration') === undefined ? null : expectedController),
|
|
204
|
+
projectPresence(objectValue(actualController, 'controlGeneration') === undefined ? null : actualController)
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function exactKeys(value: Record<string, unknown>, keys: readonly string[]): boolean {
|
|
212
|
+
return Object.keys(value).length === keys.length && keys.every((key) => Object.hasOwn(value, key));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function isCodexControllerBinding(value: unknown): value is CodexControllerBinding {
|
|
216
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
217
|
+
const record = value as Record<string, unknown>;
|
|
218
|
+
return exactKeys(record, ['instanceDigest', 'controlGeneration'])
|
|
219
|
+
&& typeof record.instanceDigest === 'string'
|
|
220
|
+
&& typeof record.controlGeneration === 'string';
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function isProjection(value: unknown): value is CodexCapabilityProjection {
|
|
224
|
+
if (!value || typeof value !== 'object') return false;
|
|
225
|
+
const record = value as Record<string, unknown>;
|
|
226
|
+
if (record.kind === 'absent') return exactKeys(record, ['kind']);
|
|
227
|
+
if (record.kind === 'protocol-version') {
|
|
228
|
+
return exactKeys(record, ['kind', 'value']) && isLifecycleProtocolVersion(record.value);
|
|
229
|
+
}
|
|
230
|
+
if (record.kind === 'semver') {
|
|
231
|
+
return exactKeys(record, ['kind', 'value']) && isCanonicalLifecyclePackageVersion(record.value);
|
|
232
|
+
}
|
|
233
|
+
if (record.kind === 'digest-prefix') {
|
|
234
|
+
return exactKeys(record, ['kind', 'value'])
|
|
235
|
+
&& typeof record.value === 'string'
|
|
236
|
+
&& DIGEST_PREFIX_PATTERN.test(record.value);
|
|
237
|
+
}
|
|
238
|
+
if (record.kind === 'presence') {
|
|
239
|
+
return exactKeys(record, ['kind', 'present']) && typeof record.present === 'boolean';
|
|
240
|
+
}
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function isProvenanceField(value: unknown): value is CodexCapabilityProvenanceField {
|
|
245
|
+
if (!value || typeof value !== 'object') return false;
|
|
246
|
+
const record = value as Record<string, unknown>;
|
|
247
|
+
return exactKeys(record, ['matches', 'expected', 'actual'])
|
|
248
|
+
&& typeof record.matches === 'boolean'
|
|
249
|
+
&& isProjection(record.expected)
|
|
250
|
+
&& isProjection(record.actual);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function isCodexCapabilityProvenanceDetail(value: unknown): value is CodexCapabilityProvenanceDetail {
|
|
254
|
+
if (!value || typeof value !== 'object') return false;
|
|
255
|
+
const record = value as Record<string, unknown>;
|
|
256
|
+
if (!exactKeys(record, ['kind', 'version', 'fields'])
|
|
257
|
+
|| record.kind !== 'codex-capability-provenance-mismatch'
|
|
258
|
+
|| record.version !== 1
|
|
259
|
+
|| !record.fields || typeof record.fields !== 'object') return false;
|
|
260
|
+
const fields = record.fields as Record<string, unknown>;
|
|
261
|
+
if (!exactKeys(fields, ['taskId', 'hookDefinitionHash', 'buildIdentity', 'controller'])
|
|
262
|
+
|| !isProvenanceField(fields.taskId)
|
|
263
|
+
|| !isProvenanceField(fields.hookDefinitionHash)) return false;
|
|
264
|
+
const buildIdentity = fields.buildIdentity;
|
|
265
|
+
const controller = fields.controller;
|
|
266
|
+
if (!buildIdentity || typeof buildIdentity !== 'object' || !controller || typeof controller !== 'object') return false;
|
|
267
|
+
const build = buildIdentity as Record<string, unknown>;
|
|
268
|
+
const binding = controller as Record<string, unknown>;
|
|
269
|
+
return exactKeys(build, ['protocolVersion', 'packageVersion', 'internalExecutableBuildHash', 'lifecycleContractHash'])
|
|
270
|
+
&& exactKeys(binding, ['instanceDigest', 'controlGeneration'])
|
|
271
|
+
&& isProvenanceField(build.protocolVersion)
|
|
272
|
+
&& isProvenanceField(build.packageVersion)
|
|
273
|
+
&& isProvenanceField(build.internalExecutableBuildHash)
|
|
274
|
+
&& isProvenanceField(build.lifecycleContractHash)
|
|
275
|
+
&& isProvenanceField(binding.instanceDigest)
|
|
276
|
+
&& isProvenanceField(binding.controlGeneration);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function sameControllerField(
|
|
280
|
+
left: unknown,
|
|
281
|
+
right: unknown,
|
|
282
|
+
key: keyof CodexControllerBinding
|
|
283
|
+
): boolean {
|
|
284
|
+
if (left === null || right === null) return left === null && right === null;
|
|
285
|
+
return isCodexControllerBinding(left)
|
|
286
|
+
&& isCodexControllerBinding(right)
|
|
287
|
+
&& left[key] === right[key];
|
|
288
|
+
}
|
|
289
|
+
|
|
58
290
|
function sameController(
|
|
59
|
-
left:
|
|
60
|
-
right:
|
|
291
|
+
left: unknown,
|
|
292
|
+
right: unknown
|
|
61
293
|
): boolean {
|
|
62
|
-
return left
|
|
63
|
-
|
|
294
|
+
if (left === null || right === null) return left === null && right === null;
|
|
295
|
+
return isCodexControllerBinding(left)
|
|
296
|
+
&& isCodexControllerBinding(right)
|
|
297
|
+
&& left.instanceDigest === right.instanceDigest
|
|
298
|
+
&& left.controlGeneration === right.controlGeneration;
|
|
64
299
|
}
|
|
65
300
|
|
|
66
301
|
function createCodexCapabilityStore(options: CodexCapabilityStoreOptions = {}) {
|
|
67
302
|
const root = options.root
|
|
68
|
-
??
|
|
303
|
+
?? resolveAgentRuntimeStoreRoot({ store: 'capabilities' });
|
|
69
304
|
const now = options.now ?? Date.now;
|
|
70
305
|
const createToken = options.token ?? (() => crypto.randomBytes(32).toString('base64url'));
|
|
71
306
|
const ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
|
|
@@ -274,27 +509,54 @@ function createCodexCapabilityStore(options: CodexCapabilityStoreOptions = {}) {
|
|
|
274
509
|
return next;
|
|
275
510
|
}
|
|
276
511
|
|
|
277
|
-
function
|
|
512
|
+
function validateRecord(record: CodexCapabilityRecord, expected: Readonly<{
|
|
278
513
|
taskId: string;
|
|
279
514
|
hookDefinitionHash: string;
|
|
280
515
|
buildIdentity: LifecycleBuildIdentity;
|
|
281
516
|
controller?: CodexControllerBinding;
|
|
282
517
|
}>): CodexCapabilityRecord {
|
|
283
|
-
sweep();
|
|
284
|
-
const { file, record } = loadActive(token);
|
|
285
518
|
if (record.status === 'consumed') {
|
|
286
519
|
throw capabilityError('CODEX_CAPABILITY_REPLAY', 'capability token was already consumed');
|
|
287
520
|
}
|
|
288
521
|
if (record.status !== 'attested') {
|
|
289
522
|
throw capabilityError('CODEX_CAPABILITY_NOT_ATTESTED', 'capability token has no current-session hook attestation');
|
|
290
523
|
}
|
|
291
|
-
const identity =
|
|
524
|
+
const identity = record.buildIdentity && typeof record.buildIdentity === 'object'
|
|
525
|
+
? verifyLifecycleBuildIdentity(record.buildIdentity, expected.buildIdentity)
|
|
526
|
+
: { ok: false };
|
|
292
527
|
if (!identity.ok
|
|
293
528
|
|| record.taskId !== expected.taskId
|
|
294
529
|
|| record.hookDefinitionHash !== expected.hookDefinitionHash
|
|
295
530
|
|| !sameController(record.controller, expected.controller ?? null)) {
|
|
296
|
-
throw capabilityError(
|
|
531
|
+
throw capabilityError(
|
|
532
|
+
'CODEX_CAPABILITY_PROVENANCE_MISMATCH',
|
|
533
|
+
'capability consumption identity does not match',
|
|
534
|
+
provenanceDetail(record, expected)
|
|
535
|
+
);
|
|
297
536
|
}
|
|
537
|
+
return record;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function validate(token: string, expected: Readonly<{
|
|
541
|
+
taskId: string;
|
|
542
|
+
hookDefinitionHash: string;
|
|
543
|
+
buildIdentity: LifecycleBuildIdentity;
|
|
544
|
+
controller?: CodexControllerBinding;
|
|
545
|
+
}>): CodexCapabilityRecord {
|
|
546
|
+
sweep();
|
|
547
|
+
const { record } = loadActive(token);
|
|
548
|
+
return validateRecord(record, expected);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function consume(token: string, expected: Readonly<{
|
|
552
|
+
taskId: string;
|
|
553
|
+
hookDefinitionHash: string;
|
|
554
|
+
buildIdentity: LifecycleBuildIdentity;
|
|
555
|
+
controller?: CodexControllerBinding;
|
|
556
|
+
}>): CodexCapabilityRecord {
|
|
557
|
+
sweep();
|
|
558
|
+
const { file, record } = loadActive(token);
|
|
559
|
+
validateRecord(record, expected);
|
|
298
560
|
const next: CodexCapabilityRecord = Object.freeze({
|
|
299
561
|
...record,
|
|
300
562
|
revision: record.revision + 1,
|
|
@@ -309,13 +571,16 @@ function createCodexCapabilityStore(options: CodexCapabilityStoreOptions = {}) {
|
|
|
309
571
|
return loadActive(token).record;
|
|
310
572
|
}
|
|
311
573
|
|
|
312
|
-
return Object.freeze({ arm, attest, consume, inspect, sweep });
|
|
574
|
+
return Object.freeze({ arm, attest, consume, inspect, validate, sweep });
|
|
313
575
|
}
|
|
314
576
|
|
|
315
577
|
export { createCodexCapabilityStore };
|
|
316
578
|
export type {
|
|
317
579
|
CapabilityStatus,
|
|
580
|
+
CodexCapabilityProjection,
|
|
581
|
+
CodexCapabilityProvenanceDetail,
|
|
318
582
|
CodexCapabilityRecord,
|
|
319
583
|
CodexCapabilityStoreOptions,
|
|
320
584
|
CodexControllerBinding
|
|
321
585
|
};
|
|
586
|
+
export { isCodexCapabilityProvenanceDetail };
|