@fitlab-ai/agent-infra 0.9.6 → 0.9.8

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 (178) hide show
  1. package/dist/bin/internal-cli.js +10 -0
  2. package/dist/lib/agent-clients/adapters/claude-code.js +3 -3
  3. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +47 -5
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +162 -0
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +245 -0
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +398 -0
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +23 -6
  8. package/dist/lib/decide.js +68 -14
  9. package/dist/lib/defaults.json +10 -2
  10. package/dist/lib/git/workflow.js +30 -3
  11. package/dist/lib/init.js +1 -1
  12. package/dist/lib/internal/codex-lifecycle.js +69 -1
  13. package/dist/lib/internal/codex-sandbox-controller.js +106 -0
  14. package/dist/lib/internal/release-workflow.js +1 -3
  15. package/dist/lib/internal/task-activity.js +57 -7
  16. package/dist/lib/internal/task-event.js +57 -4
  17. package/dist/lib/internal/task-ledger.js +59 -5
  18. package/dist/lib/internal/task-lifecycle.js +59 -8
  19. package/dist/lib/internal/task-orchestration.js +48 -8
  20. package/dist/lib/internal/task-override.js +131 -0
  21. package/dist/lib/internal/task-review.js +64 -6
  22. package/dist/lib/internal/task-verify.js +65 -3
  23. package/dist/lib/internal/task-warning.js +57 -4
  24. package/dist/lib/platform/context.js +6 -4
  25. package/dist/lib/platform/github-client.js +55 -2
  26. package/dist/lib/platform/pr-summary.js +5 -3
  27. package/dist/lib/process-data/checkpoint.js +319 -0
  28. package/dist/lib/process-data/index.js +128 -60
  29. package/dist/lib/process-data/materialize.js +84 -0
  30. package/dist/lib/process-data/normalize.js +34 -1
  31. package/dist/lib/process-data/reconcile.js +55 -1
  32. package/dist/lib/process-data/sources.js +422 -1
  33. package/dist/lib/process-data/store.js +341 -11
  34. package/dist/lib/runtime/agent-runtime.js +56 -0
  35. package/dist/lib/sandbox/commands/create.js +394 -264
  36. package/dist/lib/sandbox/commands/rm.js +64 -15
  37. package/dist/lib/sandbox/commands/start.js +4 -3
  38. package/dist/lib/sandbox/control/container-identity.js +64 -0
  39. package/dist/lib/sandbox/control/executor.js +23 -9
  40. package/dist/lib/sandbox/control/lifecycle.js +590 -61
  41. package/dist/lib/sandbox/control/protocol.js +7 -0
  42. package/dist/lib/sandbox/control/server.js +202 -26
  43. package/dist/lib/sandbox/control/state.js +51 -25
  44. package/dist/lib/sandbox/control/timing.js +8 -0
  45. package/dist/lib/sandbox/engine.js +14 -7
  46. package/dist/lib/sandbox/mounts.js +7 -0
  47. package/dist/lib/sandbox/recovery.js +135 -50
  48. package/dist/lib/sandbox/workspace-view.js +59 -20
  49. package/dist/lib/server/daemon.js +1 -1
  50. package/dist/lib/server/process-control.js +11 -1
  51. package/dist/lib/server/process-state.js +107 -20
  52. package/dist/lib/task/activity-intent.js +4 -1
  53. package/dist/lib/task/artifact-lifecycle.js +82 -11
  54. package/dist/lib/task/artifact-receipts.js +128 -0
  55. package/dist/lib/task/codex-orchestration.js +81 -6
  56. package/dist/lib/task/commands/validate.js +1 -1
  57. package/dist/lib/task/create.js +10 -2
  58. package/dist/lib/task/decision-intents.js +4 -2
  59. package/dist/lib/task/delegation-receipts.js +120 -17
  60. package/dist/lib/task/events.js +148 -7
  61. package/dist/lib/task/guard-override.js +31 -0
  62. package/dist/lib/task/human-override.js +1240 -0
  63. package/dist/lib/task/ledger-intents.js +6 -3
  64. package/dist/lib/task/ledger.js +13 -1
  65. package/dist/lib/task/lifecycle.js +69 -14
  66. package/dist/lib/task/orchestration.js +293 -18
  67. package/dist/lib/task/review-finalization.js +3 -2
  68. package/dist/lib/task/sections.js +5 -1
  69. package/dist/lib/task/task-execution-lock.js +5 -4
  70. package/dist/lib/task/verification-engine.js +80 -22
  71. package/dist/lib/task/verification.js +13 -1
  72. package/dist/lib/task/workflow-warning-intents.js +4 -2
  73. package/dist/lib/task/write.js +2 -1
  74. package/dist/lifecycle-build-manifest.json +6 -0
  75. package/lib/agent-clients/adapters/claude-code.ts +3 -3
  76. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +62 -4
  77. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +227 -0
  78. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +322 -0
  79. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +26 -0
  80. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +480 -0
  81. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +35 -5
  82. package/lib/decide.ts +59 -14
  83. package/lib/defaults.json +10 -2
  84. package/lib/git/workflow.ts +25 -2
  85. package/lib/init.ts +1 -1
  86. package/lib/internal/codex-lifecycle.ts +68 -1
  87. package/lib/internal/codex-sandbox-controller.ts +109 -0
  88. package/lib/internal/release-workflow.ts +1 -3
  89. package/lib/internal/task-activity.ts +46 -6
  90. package/lib/internal/task-event.ts +53 -4
  91. package/lib/internal/task-ledger.ts +48 -4
  92. package/lib/internal/task-lifecycle.ts +56 -12
  93. package/lib/internal/task-orchestration.ts +43 -8
  94. package/lib/internal/task-override.ts +138 -0
  95. package/lib/internal/task-review.ts +51 -6
  96. package/lib/internal/task-verify.ts +52 -3
  97. package/lib/internal/task-warning.ts +44 -3
  98. package/lib/platform/context.ts +8 -5
  99. package/lib/platform/github-client.ts +61 -3
  100. package/lib/platform/pr-summary.ts +10 -3
  101. package/lib/process-data/checkpoint.ts +335 -0
  102. package/lib/process-data/index.ts +112 -60
  103. package/lib/process-data/materialize.ts +83 -0
  104. package/lib/process-data/normalize.ts +29 -1
  105. package/lib/process-data/reconcile.ts +64 -2
  106. package/lib/process-data/sources.ts +449 -2
  107. package/lib/process-data/store.ts +304 -11
  108. package/lib/process-data/types.ts +124 -2
  109. package/lib/runtime/agent-runtime.ts +76 -0
  110. package/lib/sandbox/commands/create.ts +139 -6
  111. package/lib/sandbox/commands/rm.ts +64 -14
  112. package/lib/sandbox/commands/start.ts +4 -3
  113. package/lib/sandbox/config.ts +2 -1
  114. package/lib/sandbox/control/container-identity.ts +93 -0
  115. package/lib/sandbox/control/executor.ts +24 -12
  116. package/lib/sandbox/control/lifecycle.ts +685 -67
  117. package/lib/sandbox/control/protocol.ts +35 -6
  118. package/lib/sandbox/control/server.ts +184 -24
  119. package/lib/sandbox/control/state.ts +67 -33
  120. package/lib/sandbox/control/timing.ts +12 -0
  121. package/lib/sandbox/engine.ts +20 -9
  122. package/lib/sandbox/mounts.ts +8 -0
  123. package/lib/sandbox/recovery.ts +159 -49
  124. package/lib/sandbox/workspace-view.ts +73 -22
  125. package/lib/server/daemon.ts +1 -1
  126. package/lib/server/process-control.ts +13 -3
  127. package/lib/server/process-state.ts +129 -27
  128. package/lib/task/activity-intent.ts +4 -2
  129. package/lib/task/artifact-lifecycle.ts +73 -10
  130. package/lib/task/artifact-receipts.ts +161 -0
  131. package/lib/task/codex-orchestration.ts +91 -5
  132. package/lib/task/commands/validate.ts +1 -1
  133. package/lib/task/create.ts +11 -2
  134. package/lib/task/decision-intents.ts +4 -2
  135. package/lib/task/delegation-receipts.ts +205 -25
  136. package/lib/task/events.ts +132 -6
  137. package/lib/task/guard-override.ts +50 -0
  138. package/lib/task/human-override.ts +1503 -0
  139. package/lib/task/ledger-intents.ts +6 -3
  140. package/lib/task/ledger.ts +16 -1
  141. package/lib/task/lifecycle.ts +71 -14
  142. package/lib/task/orchestration.ts +443 -22
  143. package/lib/task/review-finalization.ts +6 -2
  144. package/lib/task/sections.ts +6 -2
  145. package/lib/task/task-execution-lock.ts +11 -9
  146. package/lib/task/verification-engine.ts +93 -32
  147. package/lib/task/verification.ts +15 -1
  148. package/lib/task/workflow-warning-intents.ts +4 -2
  149. package/lib/task/write.ts +4 -1
  150. package/package.json +1 -1
  151. package/templates/.agents/README.en.md +7 -0
  152. package/templates/.agents/README.zh-CN.md +5 -0
  153. package/templates/.agents/hooks/lifecycle-delegation.js +33 -4
  154. package/templates/.agents/rules/lifecycle-orchestration.en.md +10 -3
  155. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +10 -3
  156. package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
  157. package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +1 -1
  158. package/templates/.agents/rules/review-handshake.en.md +3 -1
  159. package/templates/.agents/rules/review-handshake.zh-CN.md +3 -1
  160. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +3 -1
  161. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +3 -1
  162. package/templates/.agents/skills/complete-task/SKILL.en.md +4 -0
  163. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +4 -0
  164. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +6 -6
  165. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +6 -6
  166. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +14 -3
  167. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +14 -3
  168. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +67 -0
  169. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +67 -0
  170. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +25 -0
  171. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +25 -0
  172. package/templates/.agents/skills/run-task/SKILL.en.md +9 -7
  173. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +9 -7
  174. package/templates/.agents/skills/run-task/reference/host-validation.en.md +5 -1
  175. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +5 -1
  176. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +75 -3
  177. package/templates/.agents/workspace/README.en.md +0 -26
  178. package/templates/.agents/workspace/README.zh-CN.md +0 -26
@@ -32,6 +32,11 @@ else
32
32
  await codexLifecycle(process.argv.slice(3));
33
33
  break;
34
34
  }
35
+ case 'codex-sandbox-controller': {
36
+ const { codexSandboxController } = await import('../lib/internal/codex-sandbox-controller.js');
37
+ await codexSandboxController(process.argv.slice(3));
38
+ break;
39
+ }
35
40
  case 'git-workflow': {
36
41
  const { gitWorkflow } = await import('../lib/internal/git-workflow.js');
37
42
  gitWorkflow(process.argv.slice(3));
@@ -127,6 +132,11 @@ else
127
132
  taskLifecycle(process.argv.slice(3));
128
133
  break;
129
134
  }
135
+ case 'task-override': {
136
+ const { taskOverride } = await import('../lib/internal/task-override.js');
137
+ taskOverride(process.argv.slice(3));
138
+ break;
139
+ }
130
140
  case 'task-short-id': {
131
141
  const { taskShortId } = await import('../lib/internal/task-short-id.js');
132
142
  taskShortId(process.argv.slice(3));
@@ -11,7 +11,7 @@ const claudeCodeAdapter = defineAgentClientAdapter({
11
11
  commands: { level: 'integrated' },
12
12
  hooks: { level: 'integrated' },
13
13
  subagents: { level: 'experimental' },
14
- orchestration: { level: 'unsupported' },
14
+ orchestration: { level: 'experimental' },
15
15
  sandbox: { level: 'integrated' },
16
16
  verification: { level: 'compatible' }
17
17
  },
@@ -21,8 +21,8 @@ const claudeCodeAdapter = defineAgentClientAdapter({
21
21
  guidance: 'Use the host model picker for the complete model and reasoning-effort catalog.'
22
22
  },
23
23
  delegationEvidence: {
24
- actualModel: 'unavailable',
25
- actualReasoningEffort: 'unavailable'
24
+ actualModel: 'host-event',
25
+ actualReasoningEffort: 'spawn-ack'
26
26
  },
27
27
  project: {
28
28
  ownedPathPrefixes: ['.claude/'],
@@ -4,6 +4,7 @@ import fs from 'node:fs';
4
4
  import os from 'node:os';
5
5
  import path from 'node:path';
6
6
  import semver from 'semver';
7
+ import { resolveAgentRuntimeStoreRoot } from '../../../runtime/agent-runtime.js';
7
8
  const LIFECYCLE_HOOKS = Object.freeze([
8
9
  Object.freeze({ event: 'PreToolUse', matcher: '^collaborationspawn_agent$', phase: 'pre-tool' }),
9
10
  Object.freeze({ event: 'PostToolUse', matcher: '', phase: 'post-tool' }),
@@ -93,7 +94,7 @@ function validateCodexLifecycleHookConfig(value) {
93
94
  if (!valid)
94
95
  throw new Error('CODEX_PREFLIGHT_HOOKS_INVALID: lifecycle hooks are invalid');
95
96
  }
96
- function parseCodexHooksList(value, repoRoot) {
97
+ function parseCodexHooksList(value, repoRoot, expectedSource = 'direct-host') {
97
98
  const root = object(value);
98
99
  const entries = Array.isArray(root?.data) ? root.data : [];
99
100
  const entry = entries.map(object).find((candidate) => candidate?.cwd === repoRoot);
@@ -107,12 +108,27 @@ function parseCodexHooksList(value, repoRoot) {
107
108
  eventName: String(hook.eventName ?? ''),
108
109
  matcher: typeof hook.matcher === 'string' ? hook.matcher : null,
109
110
  command: String(hook.command ?? ''),
110
- enabled: hook.enabled === true
111
+ enabled: hook.enabled === true,
112
+ source: String(hook.source ?? ''),
113
+ sourcePathDigest: typeof hook.sourcePath === 'string' && path.isAbsolute(hook.sourcePath)
114
+ ? crypto.createHash('sha256').update(hook.sourcePath).digest('hex')
115
+ : '',
116
+ trustStatus: String(hook.trustStatus ?? ''),
117
+ currentHash: String(hook.currentHash ?? ''),
118
+ isManaged: hook.isManaged === true,
119
+ pluginId: typeof hook.pluginId === 'string' ? hook.pluginId : null
111
120
  }));
112
121
  const valid = LIFECYCLE_HOOKS.every(({ event, matcher, phase }) => hooks.some((hook) => hook.eventName === event.replace(/^./, (character) => character.toLowerCase())
113
122
  && hook.matcher === matcher
114
123
  && hook.command === `node "$(git rev-parse --show-toplevel)/.agents/hooks/lifecycle-delegation.js" --client codex --event ${phase}`
115
- && hook.enabled));
124
+ && hook.enabled
125
+ && hook.currentHash.length > 0
126
+ && hook.sourcePathDigest.length === 64
127
+ && hook.pluginId === null
128
+ && (expectedSource === 'isolated-user'
129
+ ? hook.source === 'user' && hook.trustStatus !== 'modified'
130
+ : (hook.source === 'project' && hook.trustStatus === 'trusted')
131
+ || hook.isManaged && hook.trustStatus === 'managed')));
116
132
  if (!valid)
117
133
  throw new Error('CODEX_PREFLIGHT_HOOKS_NOT_LOADED: lifecycle hooks are not loaded for this workspace');
118
134
  return Object.freeze(hooks);
@@ -467,23 +483,49 @@ async function preflightCodexLifecycleEvidence(repoRoot = process.cwd(), runtime
467
483
  fs.rmSync(schemaDir, { recursive: true, force: true });
468
484
  }
469
485
  const hookDefinitionHash = crypto.createHash('sha256').update(hooksRaw).digest('hex');
470
- const runtimeRoot = path.join(repoRoot, '.agents', 'workspace', '.runtime', 'codex-lifecycle');
486
+ const runtimeRoot = resolveAgentRuntimeStoreRoot({ repoRoot, store: 'lifecycle' });
471
487
  const runtimeLiveness = hasCodexRuntimeLiveness(runtimeRoot, hookDefinitionHash, runtimeIdentity);
472
488
  const transport = new CodexAppServerTransport();
473
489
  transport.start();
474
490
  let discoveredHooks;
475
491
  try {
476
492
  await transport.initialize();
477
- discoveredHooks = parseCodexHooksList(await transport.request('hooks/list', { cwds: [repoRoot] }), repoRoot);
493
+ discoveredHooks = parseCodexHooksList(await transport.request('hooks/list', { cwds: [repoRoot] }), repoRoot, process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT ? 'isolated-user' : 'direct-host');
478
494
  }
479
495
  finally {
480
496
  transport.close();
481
497
  }
498
+ const lifecycleHooks = LIFECYCLE_HOOKS.map(({ event, matcher, phase }) => discoveredHooks.filter((hook) => hook.eventName === event.replace(/^./, (character) => character.toLowerCase())
499
+ && hook.matcher === matcher
500
+ && hook.command === `node "$(git rev-parse --show-toplevel)/.agents/hooks/lifecycle-delegation.js" --client codex --event ${phase}`));
501
+ if (lifecycleHooks.some((matches) => matches.length !== 1)) {
502
+ throw new Error('CODEX_PREFLIGHT_HOOK_SOURCE_AMBIGUOUS: lifecycle hook source is not unique');
503
+ }
504
+ const matchedHooks = lifecycleHooks.flat();
505
+ const sources = new Set(matchedHooks.map((hook) => hook.isManaged ? 'managed' : hook.source));
506
+ const sourcePathDigests = new Set(matchedHooks.map((hook) => hook.sourcePathDigest));
507
+ if (sources.size !== 1 || sourcePathDigests.size !== 1) {
508
+ throw new Error('CODEX_PREFLIGHT_HOOK_SOURCE_AMBIGUOUS: lifecycle hooks do not share one exact source');
509
+ }
510
+ const source = [...sources][0];
511
+ const hookSource = process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT
512
+ ? 'isolated-user'
513
+ : source === 'managed' ? 'managed' : 'project';
482
514
  return Object.freeze({
483
515
  cliVersion: match[1],
484
516
  hookDefinitionHash,
485
517
  staticReady: true,
486
518
  discoveredHooks,
519
+ hookProvenance: Object.freeze({
520
+ hookSource,
521
+ hookSourcePathDigest: [...sourcePathDigests][0],
522
+ hookSourceHash: crypto.createHash('sha256').update(JSON.stringify(matchedHooks.map((hook) => ({
523
+ eventName: hook.eventName,
524
+ matcher: hook.matcher,
525
+ command: hook.command,
526
+ currentHash: hook.currentHash
527
+ })))).digest('hex')
528
+ }),
487
529
  runtimeLiveness,
488
530
  diagnostics: transport.diagnostics
489
531
  });
@@ -0,0 +1,162 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ const LIFECYCLE_PROTOCOL_VERSION = 3;
6
+ function exactText(value) {
7
+ return typeof value === 'string' && value.trim() === value && value.length > 0;
8
+ }
9
+ function normalizeRelativePath(value) {
10
+ return value.replaceAll('\\', '/');
11
+ }
12
+ function readLifecycleManifestFiles(packageRoot) {
13
+ const file = path.join(packageRoot, 'lib', 'agent-clients', 'adapters', 'codex-lifecycle', 'manifest-files.json');
14
+ const value = JSON.parse(fs.readFileSync(file, 'utf8'));
15
+ if (!Array.isArray(value.executableFiles)
16
+ || !Array.isArray(value.contractFiles)
17
+ || !value.executableFiles.every(exactText)
18
+ || !value.contractFiles.every(exactText)
19
+ || new Set(value.executableFiles).size !== value.executableFiles.length
20
+ || new Set(value.contractFiles).size !== value.contractFiles.length) {
21
+ throw new Error('Lifecycle manifest file list is invalid');
22
+ }
23
+ return Object.freeze({
24
+ executableFiles: Object.freeze([...value.executableFiles]),
25
+ contractFiles: Object.freeze([...value.contractFiles])
26
+ });
27
+ }
28
+ function executablePackageRoot() {
29
+ let current = path.dirname(fileURLToPath(import.meta.url));
30
+ while (true) {
31
+ const candidate = path.join(current, 'package.json');
32
+ if (fs.existsSync(candidate)) {
33
+ const value = JSON.parse(fs.readFileSync(candidate, 'utf8'));
34
+ if (value.name === '@fitlab-ai/agent-infra')
35
+ return current;
36
+ }
37
+ const parent = path.dirname(current);
38
+ if (parent === current)
39
+ throw new Error('Lifecycle executable package root is unavailable');
40
+ current = parent;
41
+ }
42
+ }
43
+ function hashFiles(root, files) {
44
+ const hash = crypto.createHash('sha256');
45
+ for (const relative of [...new Set(files.map(normalizeRelativePath))].sort()) {
46
+ if (path.isAbsolute(relative) || relative.split(/[\\/]/u).includes('..')) {
47
+ throw new Error(`Lifecycle manifest path '${relative}' is unsafe`);
48
+ }
49
+ const file = path.join(root, relative);
50
+ const stat = fs.lstatSync(file);
51
+ if (!stat.isFile() || stat.isSymbolicLink()) {
52
+ throw new Error(`Lifecycle manifest entry '${relative}' must be a regular file`);
53
+ }
54
+ hash.update(relative);
55
+ hash.update('\0');
56
+ hash.update(fs.readFileSync(file));
57
+ hash.update('\0');
58
+ }
59
+ return hash.digest('hex');
60
+ }
61
+ function resolveLifecycleExecutableFiles(root, entries) {
62
+ const pending = [...entries];
63
+ const resolved = new Set();
64
+ while (pending.length > 0) {
65
+ const relative = normalizeRelativePath(pending.pop());
66
+ if (resolved.has(relative))
67
+ continue;
68
+ resolved.add(relative);
69
+ if (!/\.[cm]?[jt]s$/u.test(relative))
70
+ continue;
71
+ const file = path.join(root, relative);
72
+ const source = fs.readFileSync(file, 'utf8');
73
+ const imports = source.matchAll(/(?:\bfrom\s*|\bimport\s*(?:\(\s*)?)["'](\.[^"']+)["']/gu);
74
+ for (const match of imports) {
75
+ const requested = match[1];
76
+ const base = path.resolve(path.dirname(file), requested);
77
+ const candidates = [
78
+ base,
79
+ `${base}.ts`, `${base}.js`,
80
+ path.join(base, 'index.ts'), path.join(base, 'index.js')
81
+ ];
82
+ const dependency = candidates.find((candidate) => fs.existsSync(candidate) && fs.lstatSync(candidate).isFile());
83
+ if (!dependency)
84
+ throw new Error(`Lifecycle executable dependency '${requested}' from '${relative}' is unavailable`);
85
+ const dependencyRelative = path.relative(root, dependency);
86
+ if (dependencyRelative.startsWith('..') || path.isAbsolute(dependencyRelative)) {
87
+ throw new Error(`Lifecycle executable dependency '${requested}' escapes the package root`);
88
+ }
89
+ pending.push(normalizeRelativePath(dependencyRelative));
90
+ }
91
+ }
92
+ return [...resolved].sort();
93
+ }
94
+ function computeLifecycleBuildIdentity(repoRoot, options = {}) {
95
+ const packageRoot = executablePackageRoot();
96
+ const executableRoot = options.executableFiles ? repoRoot : packageRoot;
97
+ const defaults = readLifecycleManifestFiles(packageRoot);
98
+ const packageJson = JSON.parse(fs.readFileSync(path.join(executableRoot, 'package.json'), 'utf8'));
99
+ if (!exactText(packageJson.version))
100
+ throw new Error('Lifecycle package version is invalid');
101
+ let internalExecutableBuildHash;
102
+ if (options.executableFiles) {
103
+ internalExecutableBuildHash = hashFiles(repoRoot, options.executableFiles);
104
+ }
105
+ else {
106
+ const manifestFile = path.join(executableRoot, 'dist', 'lifecycle-build-manifest.json');
107
+ if (fs.existsSync(manifestFile)) {
108
+ const manifest = JSON.parse(fs.readFileSync(manifestFile, 'utf8'));
109
+ if (manifest.protocolVersion !== LIFECYCLE_PROTOCOL_VERSION
110
+ || manifest.packageVersion !== packageJson.version
111
+ || !exactText(manifest.sourceInputHash)
112
+ || !exactText(manifest.internalExecutableBuildHash)) {
113
+ throw new Error('Lifecycle executable build manifest is stale or invalid');
114
+ }
115
+ if (fileURLToPath(import.meta.url).split(path.sep).includes('dist')) {
116
+ const compiledFiles = defaults.executableFiles.map((file) => file.endsWith('.ts') ? path.join('dist', file.replace(/\.ts$/u, '.js')) : file);
117
+ if (hashFiles(executableRoot, resolveLifecycleExecutableFiles(executableRoot, compiledFiles)) !== manifest.internalExecutableBuildHash) {
118
+ throw new Error('Lifecycle compiled executable build does not match its manifest');
119
+ }
120
+ }
121
+ else if (hashFiles(executableRoot, resolveLifecycleExecutableFiles(executableRoot, defaults.executableFiles)) !== manifest.sourceInputHash) {
122
+ throw new Error('Lifecycle executable build manifest is stale or invalid');
123
+ }
124
+ internalExecutableBuildHash = manifest.internalExecutableBuildHash;
125
+ }
126
+ else
127
+ internalExecutableBuildHash = hashFiles(executableRoot, resolveLifecycleExecutableFiles(executableRoot, defaults.executableFiles));
128
+ }
129
+ return Object.freeze({
130
+ protocolVersion: LIFECYCLE_PROTOCOL_VERSION,
131
+ packageVersion: packageJson.version,
132
+ internalExecutableBuildHash,
133
+ lifecycleContractHash: hashFiles(repoRoot, options.contractFiles ?? defaults.contractFiles)
134
+ });
135
+ }
136
+ function verifyLifecycleBuildIdentity(expected, actual) {
137
+ if (actual.protocolVersion !== expected.protocolVersion) {
138
+ return {
139
+ ok: false,
140
+ code: 'CODEX_LIFECYCLE_PROTOCOL_MISMATCH',
141
+ message: 'Codex lifecycle protocol version does not match'
142
+ };
143
+ }
144
+ if (actual.packageVersion !== expected.packageVersion
145
+ || actual.internalExecutableBuildHash !== expected.internalExecutableBuildHash) {
146
+ return {
147
+ ok: false,
148
+ code: 'CODEX_LIFECYCLE_BUILD_MISMATCH',
149
+ message: 'Codex lifecycle executable build identity does not match'
150
+ };
151
+ }
152
+ if (actual.lifecycleContractHash !== expected.lifecycleContractHash) {
153
+ return {
154
+ ok: false,
155
+ code: 'CODEX_LIFECYCLE_CONTRACT_MISMATCH',
156
+ message: 'Codex lifecycle contract identity does not match'
157
+ };
158
+ }
159
+ return { ok: true, code: null, message: null };
160
+ }
161
+ export { LIFECYCLE_PROTOCOL_VERSION, computeLifecycleBuildIdentity, readLifecycleManifestFiles, resolveLifecycleExecutableFiles, verifyLifecycleBuildIdentity };
162
+ //# sourceMappingURL=build-identity.js.map
@@ -0,0 +1,245 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { verifyLifecycleBuildIdentity } from './build-identity.js';
5
+ import { resolveAgentRuntimeStoreRoot } from '../../../runtime/agent-runtime.js';
6
+ const DEFAULT_TTL_MS = 30_000;
7
+ const DEFAULT_TOMBSTONE_MS = 86_400_000;
8
+ const DEFAULT_MAX_RECORDS = 128;
9
+ const LOCK_RETRY_MS = 10;
10
+ const LOCK_TIMEOUT_MS = 1_000;
11
+ const LOCK_STALE_MS = 30_000;
12
+ function digest(value) {
13
+ return crypto.createHash('sha256').update(value).digest('hex');
14
+ }
15
+ function capabilityError(code, message) {
16
+ const error = new Error(`${code}: ${message}`);
17
+ error.name = code;
18
+ return error;
19
+ }
20
+ function sameController(left, right) {
21
+ return left?.instanceDigest === right?.instanceDigest
22
+ && left?.controlGeneration === right?.controlGeneration;
23
+ }
24
+ function createCodexCapabilityStore(options = {}) {
25
+ const root = options.root
26
+ ?? resolveAgentRuntimeStoreRoot({ store: 'capabilities' });
27
+ const now = options.now ?? Date.now;
28
+ const createToken = options.token ?? (() => crypto.randomBytes(32).toString('base64url'));
29
+ const ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
30
+ const tombstoneMs = options.tombstoneMs ?? DEFAULT_TOMBSTONE_MS;
31
+ const maxRecords = options.maxRecords ?? DEFAULT_MAX_RECORDS;
32
+ fs.mkdirSync(root, { recursive: true, mode: 0o700 });
33
+ fs.chmodSync(root, 0o700);
34
+ function fileFor(token) {
35
+ return path.join(root, `${digest(token)}.json`);
36
+ }
37
+ function read(file) {
38
+ const value = JSON.parse(fs.readFileSync(file, 'utf8'));
39
+ if (value.schemaVersion !== 1 || !Number.isSafeInteger(value.revision) || value.revision < 1) {
40
+ throw capabilityError('CODEX_CAPABILITY_STATE_INVALID', 'capability record schema is invalid');
41
+ }
42
+ return value;
43
+ }
44
+ function withWriteLock(operation) {
45
+ const lock = path.join(root, '.write.lock');
46
+ const deadline = Date.now() + LOCK_TIMEOUT_MS;
47
+ let descriptor = null;
48
+ while (descriptor === null) {
49
+ try {
50
+ descriptor = fs.openSync(lock, 'wx', 0o600);
51
+ }
52
+ catch (error) {
53
+ if (error.code !== 'EEXIST')
54
+ throw error;
55
+ try {
56
+ if (Date.now() - fs.statSync(lock).mtimeMs > LOCK_STALE_MS) {
57
+ fs.unlinkSync(lock);
58
+ continue;
59
+ }
60
+ }
61
+ catch (lockError) {
62
+ if (lockError.code === 'ENOENT')
63
+ continue;
64
+ throw lockError;
65
+ }
66
+ if (Date.now() >= deadline) {
67
+ throw capabilityError('CODEX_CAPABILITY_STORE_BUSY', 'capability store is busy');
68
+ }
69
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, LOCK_RETRY_MS);
70
+ }
71
+ }
72
+ try {
73
+ return operation();
74
+ }
75
+ finally {
76
+ fs.closeSync(descriptor);
77
+ fs.unlinkSync(lock);
78
+ }
79
+ }
80
+ function write(file, value, expectedRevision) {
81
+ const temporary = `${file}.${process.pid}.${crypto.randomUUID()}.tmp`;
82
+ fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, {
83
+ encoding: 'utf8',
84
+ flag: 'wx',
85
+ mode: 0o600
86
+ });
87
+ try {
88
+ withWriteLock(() => {
89
+ options.beforeCompareAndSwap?.({ path: file, expectedRevision });
90
+ const actualRevision = fs.existsSync(file) ? read(file).revision : 0;
91
+ if (actualRevision !== expectedRevision) {
92
+ throw capabilityError('CODEX_CAPABILITY_REVISION_CONFLICT', `capability revision changed from ${expectedRevision} to ${actualRevision}`);
93
+ }
94
+ fs.renameSync(temporary, file);
95
+ fs.chmodSync(file, 0o600);
96
+ });
97
+ }
98
+ finally {
99
+ if (fs.existsSync(temporary))
100
+ fs.unlinkSync(temporary);
101
+ }
102
+ }
103
+ function files() {
104
+ return fs.readdirSync(root)
105
+ .filter((name) => /^[a-f0-9]{64}\.json$/u.test(name))
106
+ .map((name) => path.join(root, name));
107
+ }
108
+ function expire(record, file, timestamp) {
109
+ if ((record.status === 'armed' || record.status === 'attested') && timestamp > record.expiresAt) {
110
+ const expired = Object.freeze({
111
+ ...record,
112
+ revision: record.revision + 1,
113
+ status: 'expired'
114
+ });
115
+ write(file, expired, record.revision);
116
+ return expired;
117
+ }
118
+ return record;
119
+ }
120
+ function sweep() {
121
+ const timestamp = now();
122
+ const records = files().map((file) => ({ file, record: expire(read(file), file, timestamp) }));
123
+ for (const entry of records) {
124
+ const terminalAt = entry.record.consumedAt ?? entry.record.expiresAt;
125
+ if (['consumed', 'expired'].includes(entry.record.status)
126
+ && timestamp - terminalAt > tombstoneMs) {
127
+ fs.unlinkSync(entry.file);
128
+ }
129
+ }
130
+ const remaining = files().map((file) => ({ file, record: read(file) }));
131
+ if (remaining.length <= maxRecords)
132
+ return;
133
+ const removable = remaining
134
+ .filter(({ record }) => ['consumed', 'expired'].includes(record.status))
135
+ .sort((left, right) => (left.record.consumedAt ?? left.record.expiresAt)
136
+ - (right.record.consumedAt ?? right.record.expiresAt));
137
+ while (files().length > maxRecords && removable.length > 0) {
138
+ fs.unlinkSync(removable.shift().file);
139
+ }
140
+ if (files().length > maxRecords) {
141
+ throw capabilityError('CODEX_CAPABILITY_CAPACITY_EXCEEDED', 'capability store has too many active records');
142
+ }
143
+ }
144
+ function arm(input) {
145
+ sweep();
146
+ const token = createToken();
147
+ const file = fileFor(token);
148
+ if (fs.existsSync(file))
149
+ throw capabilityError('CODEX_CAPABILITY_TOKEN_COLLISION', 'capability token already exists');
150
+ const timestamp = now();
151
+ const record = Object.freeze({
152
+ schemaVersion: 1,
153
+ revision: 1,
154
+ tokenDigest: digest(token),
155
+ taskId: input.taskId,
156
+ status: 'armed',
157
+ buildIdentity: input.buildIdentity,
158
+ controller: input.controller ?? null,
159
+ sessionId: null,
160
+ turnId: null,
161
+ toolUseId: null,
162
+ hookDefinitionHash: null,
163
+ armedAt: timestamp,
164
+ expiresAt: timestamp + ttlMs,
165
+ attestedAt: null,
166
+ consumedAt: null
167
+ });
168
+ write(file, record, 0);
169
+ return Object.freeze({
170
+ ...record,
171
+ token,
172
+ path: file,
173
+ marker: `agent-infra-codex-capability:${token}`
174
+ });
175
+ }
176
+ function loadActive(token) {
177
+ const file = fileFor(token);
178
+ if (!fs.existsSync(file)) {
179
+ throw capabilityError('CODEX_CAPABILITY_MISSING', 'capability token was not found');
180
+ }
181
+ const record = expire(read(file), file, now());
182
+ if (record.status === 'expired') {
183
+ throw capabilityError('CODEX_CAPABILITY_EXPIRED', 'capability token expired');
184
+ }
185
+ return { file, record };
186
+ }
187
+ function attest(input) {
188
+ const { file, record } = loadActive(input.token);
189
+ if (record.status !== 'armed') {
190
+ throw capabilityError(record.status === 'consumed' ? 'CODEX_CAPABILITY_REPLAY' : 'CODEX_CAPABILITY_STATE_INVALID', `capability is ${record.status}, expected armed`);
191
+ }
192
+ const identity = verifyLifecycleBuildIdentity(record.buildIdentity, input.buildIdentity);
193
+ if (!identity.ok || !sameController(record.controller, input.controller ?? null)) {
194
+ throw capabilityError('CODEX_CAPABILITY_PROVENANCE_MISMATCH', 'capability build or controller identity does not match');
195
+ }
196
+ if (![input.sessionId, input.turnId, input.toolUseId, input.hookDefinitionHash]
197
+ .every((value) => value.trim().length > 0)) {
198
+ throw capabilityError('CODEX_CAPABILITY_IDENTITY_INVALID', 'hook identity is incomplete');
199
+ }
200
+ const timestamp = now();
201
+ const next = Object.freeze({
202
+ ...record,
203
+ revision: record.revision + 1,
204
+ status: 'attested',
205
+ sessionId: input.sessionId,
206
+ turnId: input.turnId,
207
+ toolUseId: input.toolUseId,
208
+ hookDefinitionHash: input.hookDefinitionHash,
209
+ attestedAt: timestamp
210
+ });
211
+ write(file, next, record.revision);
212
+ return next;
213
+ }
214
+ function consume(token, expected) {
215
+ sweep();
216
+ const { file, record } = loadActive(token);
217
+ if (record.status === 'consumed') {
218
+ throw capabilityError('CODEX_CAPABILITY_REPLAY', 'capability token was already consumed');
219
+ }
220
+ if (record.status !== 'attested') {
221
+ throw capabilityError('CODEX_CAPABILITY_NOT_ATTESTED', 'capability token has no current-session hook attestation');
222
+ }
223
+ const identity = verifyLifecycleBuildIdentity(record.buildIdentity, expected.buildIdentity);
224
+ if (!identity.ok
225
+ || record.taskId !== expected.taskId
226
+ || record.hookDefinitionHash !== expected.hookDefinitionHash
227
+ || !sameController(record.controller, expected.controller ?? null)) {
228
+ throw capabilityError('CODEX_CAPABILITY_PROVENANCE_MISMATCH', 'capability consumption identity does not match');
229
+ }
230
+ const next = Object.freeze({
231
+ ...record,
232
+ revision: record.revision + 1,
233
+ status: 'consumed',
234
+ consumedAt: now()
235
+ });
236
+ write(file, next, record.revision);
237
+ return next;
238
+ }
239
+ function inspect(token) {
240
+ return loadActive(token).record;
241
+ }
242
+ return Object.freeze({ arm, attest, consume, inspect, sweep });
243
+ }
244
+ export { createCodexCapabilityStore };
245
+ //# sourceMappingURL=capability-store.js.map