@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.
Files changed (195) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/bin/internal-cli.js +11 -1
  4. package/dist/lib/agent-clients/adapters/claude-code.js +3 -3
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +60 -22
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +8 -1
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +175 -11
  8. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-context.js +147 -0
  9. package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -0
  10. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +86 -69
  11. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +2 -1
  12. package/dist/lib/decide.js +68 -14
  13. package/dist/lib/git/workflow.js +10 -1
  14. package/dist/lib/init.js +1 -1
  15. package/dist/lib/internal/codex-lifecycle.js +19 -5
  16. package/dist/lib/internal/codex-sandbox-controller.js +6 -13
  17. package/dist/lib/internal/git-workflow.js +5 -7
  18. package/dist/lib/internal/platform-pr.js +20 -7
  19. package/dist/lib/internal/release-workflow.js +3 -4
  20. package/dist/lib/internal/sandbox-control.js +79 -2
  21. package/dist/lib/internal/task-activity.js +57 -7
  22. package/dist/lib/internal/task-event.js +57 -4
  23. package/dist/lib/internal/task-finalization.js +54 -0
  24. package/dist/lib/internal/task-ledger.js +59 -5
  25. package/dist/lib/internal/task-lifecycle.js +59 -8
  26. package/dist/lib/internal/task-orchestration.js +161 -288
  27. package/dist/lib/internal/task-override.js +131 -0
  28. package/dist/lib/internal/task-review.js +64 -6
  29. package/dist/lib/internal/task-verify.js +65 -3
  30. package/dist/lib/internal/task-warning.js +57 -4
  31. package/dist/lib/platform/pr-summary.js +53 -13
  32. package/dist/lib/platform/pull-requests.js +205 -41
  33. package/dist/lib/runtime/agent-runtime.js +56 -0
  34. package/dist/lib/sandbox/commands/create.js +389 -311
  35. package/dist/lib/sandbox/commands/rm.js +316 -55
  36. package/dist/lib/sandbox/control/client.js +190 -6
  37. package/dist/lib/sandbox/control/container-identity.js +1 -1
  38. package/dist/lib/sandbox/control/controller-registration.js +273 -0
  39. package/dist/lib/sandbox/control/executor.js +145 -5
  40. package/dist/lib/sandbox/control/lifecycle.js +395 -73
  41. package/dist/lib/sandbox/control/protocol.js +67 -6
  42. package/dist/lib/sandbox/control/state.js +27 -11
  43. package/dist/lib/sandbox/engine.js +14 -7
  44. package/dist/lib/sandbox/index.js +5 -3
  45. package/dist/lib/sandbox/managed-fs.js +5 -0
  46. package/dist/lib/sandbox/mounts.js +20 -5
  47. package/dist/lib/sandbox/recovery.js +78 -10
  48. package/dist/lib/sandbox/task-resolver.js +50 -6
  49. package/dist/lib/sandbox/workspace-identity.js +27 -0
  50. package/dist/lib/sandbox/workspace-view.js +62 -40
  51. package/dist/lib/sandbox/worktree-safety.js +215 -2
  52. package/dist/lib/server/process-state.js +51 -1
  53. package/dist/lib/task/activity-intent.js +4 -1
  54. package/dist/lib/task/codex-orchestration.js +158 -33
  55. package/dist/lib/task/commands/status.js +59 -1
  56. package/dist/lib/task/commit-operation.js +335 -0
  57. package/dist/lib/task/commit-policy.js +18 -0
  58. package/dist/lib/task/decision-intents.js +4 -2
  59. package/dist/lib/task/delegation-receipts.js +279 -17
  60. package/dist/lib/task/events.js +9 -5
  61. package/dist/lib/task/finalization.js +567 -0
  62. package/dist/lib/task/guard-override.js +31 -0
  63. package/dist/lib/task/human-override.js +1240 -0
  64. package/dist/lib/task/ledger-intents.js +6 -3
  65. package/dist/lib/task/lifecycle.js +69 -14
  66. package/dist/lib/task/operation-outcome.js +33 -0
  67. package/dist/lib/task/orchestration.js +199 -719
  68. package/dist/lib/task/review-finalization.js +3 -2
  69. package/dist/lib/task/short-id.js +59 -1
  70. package/dist/lib/task/task-execution-lock.js +6 -1
  71. package/dist/lib/task/verification-engine.js +138 -59
  72. package/dist/lib/task/verification.js +15 -2
  73. package/dist/lib/task/workflow-warning-intents.js +54 -3
  74. package/dist/lib/task/write.js +2 -1
  75. package/dist/lifecycle-build-manifest.json +3 -3
  76. package/lib/agent-clients/adapters/claude-code.ts +3 -3
  77. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +63 -20
  78. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +12 -0
  79. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +278 -13
  80. package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +180 -0
  81. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -0
  82. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +9 -1
  83. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +112 -93
  84. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +2 -1
  85. package/lib/decide.ts +59 -14
  86. package/lib/git/workflow.ts +11 -2
  87. package/lib/init.ts +1 -1
  88. package/lib/internal/codex-lifecycle.ts +18 -5
  89. package/lib/internal/codex-sandbox-controller.ts +6 -12
  90. package/lib/internal/git-workflow.ts +5 -6
  91. package/lib/internal/platform-pr.ts +14 -7
  92. package/lib/internal/release-workflow.ts +3 -4
  93. package/lib/internal/sandbox-control.ts +92 -2
  94. package/lib/internal/task-activity.ts +46 -6
  95. package/lib/internal/task-event.ts +53 -4
  96. package/lib/internal/task-finalization.ts +68 -0
  97. package/lib/internal/task-ledger.ts +48 -4
  98. package/lib/internal/task-lifecycle.ts +56 -12
  99. package/lib/internal/task-orchestration.ts +32 -107
  100. package/lib/internal/task-override.ts +138 -0
  101. package/lib/internal/task-review.ts +51 -6
  102. package/lib/internal/task-verify.ts +52 -3
  103. package/lib/internal/task-warning.ts +44 -3
  104. package/lib/platform/pr-summary.ts +65 -14
  105. package/lib/platform/pull-requests.ts +211 -41
  106. package/lib/runtime/agent-runtime.ts +76 -0
  107. package/lib/sandbox/commands/create.ts +88 -4
  108. package/lib/sandbox/commands/rm.ts +413 -61
  109. package/lib/sandbox/config.ts +2 -1
  110. package/lib/sandbox/control/client.ts +255 -6
  111. package/lib/sandbox/control/container-identity.ts +3 -3
  112. package/lib/sandbox/control/controller-registration.ts +347 -0
  113. package/lib/sandbox/control/executor.ts +172 -6
  114. package/lib/sandbox/control/lifecycle.ts +465 -77
  115. package/lib/sandbox/control/protocol.ts +85 -9
  116. package/lib/sandbox/control/state.ts +35 -18
  117. package/lib/sandbox/engine.ts +20 -9
  118. package/lib/sandbox/index.ts +5 -3
  119. package/lib/sandbox/managed-fs.ts +5 -0
  120. package/lib/sandbox/mounts.ts +23 -5
  121. package/lib/sandbox/recovery.ts +90 -9
  122. package/lib/sandbox/task-resolver.ts +58 -7
  123. package/lib/sandbox/workspace-identity.ts +50 -2
  124. package/lib/sandbox/workspace-view.ts +73 -43
  125. package/lib/sandbox/worktree-safety.ts +236 -2
  126. package/lib/server/process-state.ts +48 -1
  127. package/lib/task/activity-intent.ts +4 -2
  128. package/lib/task/codex-orchestration.ts +167 -33
  129. package/lib/task/commands/status.ts +78 -1
  130. package/lib/task/commit-operation.ts +430 -0
  131. package/lib/task/commit-policy.ts +30 -0
  132. package/lib/task/decision-intents.ts +4 -2
  133. package/lib/task/delegation-receipts.ts +304 -19
  134. package/lib/task/events.ts +10 -5
  135. package/lib/task/finalization.ts +719 -0
  136. package/lib/task/guard-override.ts +50 -0
  137. package/lib/task/human-override.ts +1503 -0
  138. package/lib/task/ledger-intents.ts +6 -3
  139. package/lib/task/lifecycle.ts +71 -14
  140. package/lib/task/operation-outcome.ts +48 -0
  141. package/lib/task/orchestration.ts +239 -922
  142. package/lib/task/review-finalization.ts +6 -2
  143. package/lib/task/short-id.ts +65 -1
  144. package/lib/task/task-execution-lock.ts +10 -1
  145. package/lib/task/verification-engine.ts +145 -64
  146. package/lib/task/verification.ts +18 -3
  147. package/lib/task/workflow-warning-intents.ts +61 -4
  148. package/lib/task/write.ts +4 -1
  149. package/package.json +2 -2
  150. package/templates/.agents/QUICKSTART.en.md +2 -2
  151. package/templates/.agents/QUICKSTART.zh-CN.md +2 -2
  152. package/templates/.agents/hooks/lifecycle-delegation.js +9 -24
  153. package/templates/.agents/rules/lifecycle-orchestration.en.md +4 -3
  154. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +4 -3
  155. package/templates/.agents/rules/task-management.en.md +2 -2
  156. package/templates/.agents/rules/task-management.zh-CN.md +2 -2
  157. package/templates/.agents/skills/block-task/SKILL.en.md +2 -4
  158. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -4
  159. package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -3
  160. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +3 -3
  161. package/templates/.agents/skills/close-codescan/SKILL.en.md +6 -2
  162. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +6 -2
  163. package/templates/.agents/skills/close-dependabot/SKILL.en.md +6 -2
  164. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +6 -2
  165. package/templates/.agents/skills/commit/SKILL.en.md +41 -125
  166. package/templates/.agents/skills/commit/SKILL.zh-CN.md +52 -123
  167. package/templates/.agents/skills/commit/reference/commit-message.en.md +1 -1
  168. package/templates/.agents/skills/commit/reference/commit-message.zh-CN.md +1 -1
  169. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +14 -43
  170. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +14 -49
  171. package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +1 -1
  172. package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +1 -1
  173. package/templates/.agents/skills/commit/reference/task-status-update.en.md +2 -35
  174. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +2 -35
  175. package/templates/.agents/skills/complete-task/SKILL.en.md +31 -38
  176. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +31 -38
  177. package/templates/.agents/skills/complete-task/config/verify.en.json +1 -0
  178. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +1 -0
  179. package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +1 -1
  180. package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +1 -1
  181. package/templates/.agents/skills/create-pr/SKILL.en.md +6 -4
  182. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +6 -4
  183. package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +2 -2
  184. package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +2 -2
  185. package/templates/.agents/skills/run-task/SKILL.en.md +5 -5
  186. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +5 -5
  187. package/templates/.agents/skills/run-task/reference/host-validation.en.md +3 -1
  188. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +3 -1
  189. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +1 -1
  190. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +1 -1
  191. package/templates/.codex/hooks.json +4 -4
  192. package/dist/lib/task/commit-finalization.js +0 -305
  193. package/dist/lib/task/commit-intent.js +0 -165
  194. package/lib/task/commit-finalization.ts +0 -419
  195. 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 === 'sub_agent_activity'
115
- && payload.event_id === expected.toolUseId
116
- && payload.kind === 'started'
117
- && payload.agent_path === `/root/${expected.taskName}`
118
- && nonEmpty(payload.agent_thread_id);
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 === 15
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 resolution: ThreadResolution;
494
+ let rolloutRecords: readonly unknown[];
481
495
  try {
482
- const rolloutRecords = await resolveCodexRolloutRecords(
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
- options: AppServerTransportOptions = {}
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 result = object(await transport.request('thread/read', { threadId: childThreadId, includeTurns: true }));
516
- const thread = object(result?.thread);
517
- if (nonEmpty(thread?.id) !== childThreadId) throw new Error('Codex App Server returned the wrong child thread');
518
- const turns = Array.isArray(thread?.turns) ? thread.turns : [];
519
- const turn = turns.at(-1);
520
- const turnValue = object(turn);
521
- if (!turnValue || turnValue.status === 'inProgress') throw new Error('CODEX_TURN_NOT_TERMINAL');
522
- return parseCodexTurnCompleted({ threadId: childThreadId, turn });
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 = path.join(repoRoot, '.agents', 'workspace', '.runtime', 'codex-lifecycle');
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 { verifyLifecycleBuildIdentity } from './build-identity.ts';
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: CodexControllerBinding | null,
60
- right: CodexControllerBinding | null
291
+ left: unknown,
292
+ right: unknown
61
293
  ): boolean {
62
- return left?.instanceDigest === right?.instanceDigest
63
- && left?.controlGeneration === right?.controlGeneration;
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
- ?? path.join(process.cwd(), '.agents', 'workspace', '.runtime', 'codex-capabilities');
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 consume(token: string, expected: Readonly<{
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 = verifyLifecycleBuildIdentity(record.buildIdentity, expected.buildIdentity);
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('CODEX_CAPABILITY_PROVENANCE_MISMATCH', 'capability consumption identity does not match');
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 };