@fitlab-ai/agent-infra 0.9.5 → 0.9.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/dist/bin/internal-cli.js +8 -3
  2. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +171 -20
  3. package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +162 -0
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +244 -0
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -2
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +370 -0
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +68 -8
  8. package/dist/lib/agent-clients/adapters/codex.js +1 -1
  9. package/dist/lib/defaults.json +10 -2
  10. package/dist/lib/git/workflow.js +30 -4
  11. package/dist/lib/internal/codex-lifecycle.js +169 -3
  12. package/dist/lib/internal/codex-sandbox-controller.js +106 -0
  13. package/dist/lib/internal/release-workflow.js +108 -11
  14. package/dist/lib/internal/sandbox-control.js +43 -6
  15. package/dist/lib/internal/task-create.js +12 -4
  16. package/dist/lib/internal/task-orchestration.js +40 -7
  17. package/dist/lib/platform/context.js +6 -4
  18. package/dist/lib/platform/github-client.js +55 -2
  19. package/dist/lib/platform/issue-comments.js +2 -1
  20. package/dist/lib/process-data/checkpoint.js +319 -0
  21. package/dist/lib/process-data/index.js +128 -60
  22. package/dist/lib/process-data/materialize.js +84 -0
  23. package/dist/lib/process-data/normalize.js +34 -1
  24. package/dist/lib/process-data/reconcile.js +55 -1
  25. package/dist/lib/process-data/sources.js +422 -1
  26. package/dist/lib/process-data/store.js +341 -11
  27. package/dist/lib/release/workflow.js +29 -21
  28. package/dist/lib/sandbox/commands/create.js +111 -60
  29. package/dist/lib/sandbox/commands/prune.js +40 -16
  30. package/dist/lib/sandbox/commands/rm.js +372 -94
  31. package/dist/lib/sandbox/commands/start.js +4 -3
  32. package/dist/lib/sandbox/control/client.js +138 -22
  33. package/dist/lib/sandbox/control/container-identity.js +64 -0
  34. package/dist/lib/sandbox/control/executor.js +142 -0
  35. package/dist/lib/sandbox/control/lifecycle.js +557 -0
  36. package/dist/lib/sandbox/control/protocol.js +69 -26
  37. package/dist/lib/sandbox/control/server.js +395 -99
  38. package/dist/lib/sandbox/control/state.js +191 -0
  39. package/dist/lib/sandbox/control/timing.js +8 -0
  40. package/dist/lib/sandbox/index.js +2 -2
  41. package/dist/lib/sandbox/managed-fs.js +14 -2
  42. package/dist/lib/sandbox/mounts.js +22 -19
  43. package/dist/lib/sandbox/recovery.js +284 -50
  44. package/dist/lib/sandbox/workspace-view.js +90 -10
  45. package/dist/lib/sandbox/worktree-safety.js +196 -0
  46. package/dist/lib/server/daemon.js +1 -1
  47. package/dist/lib/server/process-control.js +17 -1
  48. package/dist/lib/server/process-state.js +57 -20
  49. package/dist/lib/task/artifact-lifecycle.js +87 -15
  50. package/dist/lib/task/artifact-receipts.js +128 -0
  51. package/dist/lib/task/codex-orchestration.js +236 -0
  52. package/dist/lib/task/commands/status.js +3 -0
  53. package/dist/lib/task/commands/validate.js +289 -0
  54. package/dist/lib/task/commit-finalization.js +36 -9
  55. package/dist/lib/task/create.js +10 -2
  56. package/dist/lib/task/delegation-receipts.js +109 -2
  57. package/dist/lib/task/events.js +149 -6
  58. package/dist/lib/task/finalization-artifacts.js +2 -1
  59. package/dist/lib/task/implementation-inputs.js +1 -7
  60. package/dist/lib/task/index.js +9 -0
  61. package/dist/lib/task/ledger.js +13 -1
  62. package/dist/lib/task/orchestration.js +365 -15
  63. package/dist/lib/task/sections.js +5 -1
  64. package/dist/lib/task/task-execution-lock.js +5 -4
  65. package/dist/lib/task/verification-engine.js +78 -14
  66. package/dist/lib/task/verification.js +1 -0
  67. package/dist/lifecycle-build-manifest.json +6 -0
  68. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +202 -18
  69. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +227 -0
  70. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +321 -0
  71. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -3
  72. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +26 -0
  73. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +453 -0
  74. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +83 -6
  75. package/lib/agent-clients/adapters/codex.ts +1 -1
  76. package/lib/defaults.json +10 -2
  77. package/lib/git/workflow.ts +25 -3
  78. package/lib/internal/codex-lifecycle.ts +175 -4
  79. package/lib/internal/codex-sandbox-controller.ts +109 -0
  80. package/lib/internal/release-workflow.ts +107 -11
  81. package/lib/internal/sandbox-control.ts +38 -6
  82. package/lib/internal/task-create.ts +16 -4
  83. package/lib/internal/task-orchestration.ts +34 -7
  84. package/lib/platform/context.ts +8 -5
  85. package/lib/platform/github-client.ts +61 -3
  86. package/lib/platform/issue-comments.ts +2 -1
  87. package/lib/process-data/checkpoint.ts +335 -0
  88. package/lib/process-data/index.ts +112 -60
  89. package/lib/process-data/materialize.ts +83 -0
  90. package/lib/process-data/normalize.ts +29 -1
  91. package/lib/process-data/reconcile.ts +64 -2
  92. package/lib/process-data/sources.ts +449 -2
  93. package/lib/process-data/store.ts +304 -11
  94. package/lib/process-data/types.ts +124 -2
  95. package/lib/release/workflow.ts +69 -20
  96. package/lib/sandbox/commands/create.ts +70 -25
  97. package/lib/sandbox/commands/prune.ts +53 -15
  98. package/lib/sandbox/commands/rm.ts +423 -108
  99. package/lib/sandbox/commands/start.ts +4 -3
  100. package/lib/sandbox/control/client.ts +152 -28
  101. package/lib/sandbox/control/container-identity.ts +93 -0
  102. package/lib/sandbox/control/executor.ts +167 -0
  103. package/lib/sandbox/control/lifecycle.ts +573 -0
  104. package/lib/sandbox/control/protocol.ts +121 -68
  105. package/lib/sandbox/control/server.ts +398 -109
  106. package/lib/sandbox/control/state.ts +213 -0
  107. package/lib/sandbox/control/timing.ts +12 -0
  108. package/lib/sandbox/index.ts +2 -2
  109. package/lib/sandbox/managed-fs.ts +17 -2
  110. package/lib/sandbox/mounts.ts +16 -8
  111. package/lib/sandbox/recovery.ts +335 -65
  112. package/lib/sandbox/workspace-view.ts +102 -11
  113. package/lib/sandbox/worktree-safety.ts +234 -0
  114. package/lib/server/daemon.ts +1 -1
  115. package/lib/server/process-control.ts +24 -3
  116. package/lib/server/process-state.ts +82 -27
  117. package/lib/task/artifact-lifecycle.ts +78 -14
  118. package/lib/task/artifact-receipts.ts +161 -0
  119. package/lib/task/codex-orchestration.ts +319 -0
  120. package/lib/task/commands/status.ts +2 -0
  121. package/lib/task/commands/validate.ts +282 -0
  122. package/lib/task/commit-finalization.ts +52 -9
  123. package/lib/task/create.ts +11 -2
  124. package/lib/task/delegation-receipts.ts +208 -7
  125. package/lib/task/events.ts +131 -5
  126. package/lib/task/finalization-artifacts.ts +2 -1
  127. package/lib/task/implementation-inputs.ts +1 -5
  128. package/lib/task/index.ts +8 -0
  129. package/lib/task/ledger.ts +16 -1
  130. package/lib/task/orchestration.ts +552 -18
  131. package/lib/task/sections.ts +6 -2
  132. package/lib/task/task-execution-lock.ts +11 -9
  133. package/lib/task/verification-engine.ts +93 -24
  134. package/lib/task/verification.ts +2 -1
  135. package/package.json +1 -1
  136. package/templates/.agents/README.en.md +7 -0
  137. package/templates/.agents/README.zh-CN.md +5 -0
  138. package/templates/.agents/hooks/lifecycle-delegation.js +72 -12
  139. package/templates/.agents/rules/lifecycle-orchestration.en.md +8 -2
  140. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +8 -2
  141. package/templates/.agents/rules/no-mid-flow-questions.en.md +1 -0
  142. package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +1 -0
  143. package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
  144. package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +1 -1
  145. package/templates/.agents/rules/review-handshake.en.md +3 -1
  146. package/templates/.agents/rules/review-handshake.zh-CN.md +3 -1
  147. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +3 -1
  148. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +3 -1
  149. package/templates/.agents/skills/complete-task/SKILL.en.md +4 -0
  150. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +4 -0
  151. package/templates/.agents/skills/post-release/SKILL.en.md +32 -6
  152. package/templates/.agents/skills/post-release/SKILL.zh-CN.md +32 -6
  153. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +42 -0
  154. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +42 -0
  155. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +25 -0
  156. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +25 -0
  157. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +67 -0
  158. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +67 -0
  159. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +69 -0
  160. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +69 -0
  161. package/templates/.agents/skills/run-task/SKILL.en.md +9 -7
  162. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +9 -7
  163. package/templates/.agents/skills/run-task/reference/host-validation.en.md +5 -3
  164. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +5 -3
  165. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +75 -3
  166. package/templates/.claude/commands/run-manual-validation.en.md +9 -0
  167. package/templates/.claude/commands/run-manual-validation.zh-CN.md +9 -0
  168. package/templates/.codex/hooks.json +8 -8
  169. package/templates/.opencode/commands/run-manual-validation.en.md +11 -0
  170. package/templates/.opencode/commands/run-manual-validation.zh-CN.md +11 -0
  171. package/templates/.agents/workspace/README.en.md +0 -26
  172. package/templates/.agents/workspace/README.zh-CN.md +0 -26
@@ -0,0 +1,227 @@
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
+
6
+ const LIFECYCLE_PROTOCOL_VERSION = 3;
7
+
8
+ type LifecycleBuildIdentity = Readonly<{
9
+ protocolVersion: 3;
10
+ packageVersion: string;
11
+ internalExecutableBuildHash: string;
12
+ lifecycleContractHash: string;
13
+ }>;
14
+
15
+ type LifecycleBuildIdentityOptions = Readonly<{
16
+ executableFiles?: readonly string[];
17
+ contractFiles?: readonly string[];
18
+ }>;
19
+
20
+ type LifecycleManifestFiles = Readonly<{
21
+ executableFiles: readonly string[];
22
+ contractFiles: readonly string[];
23
+ }>;
24
+
25
+ type IdentityVerification = Readonly<{
26
+ ok: boolean;
27
+ code: string | null;
28
+ message: string | null;
29
+ }>;
30
+
31
+ function exactText(value: unknown): value is string {
32
+ return typeof value === 'string' && value.trim() === value && value.length > 0;
33
+ }
34
+
35
+ function normalizeRelativePath(value: string): string {
36
+ return value.replaceAll('\\', '/');
37
+ }
38
+
39
+ function readLifecycleManifestFiles(packageRoot: string): LifecycleManifestFiles {
40
+ const file = path.join(
41
+ packageRoot,
42
+ 'lib',
43
+ 'agent-clients',
44
+ 'adapters',
45
+ 'codex-lifecycle',
46
+ 'manifest-files.json'
47
+ );
48
+ const value = JSON.parse(fs.readFileSync(file, 'utf8')) as {
49
+ executableFiles?: unknown;
50
+ contractFiles?: unknown;
51
+ };
52
+ if (!Array.isArray(value.executableFiles)
53
+ || !Array.isArray(value.contractFiles)
54
+ || !value.executableFiles.every(exactText)
55
+ || !value.contractFiles.every(exactText)
56
+ || new Set(value.executableFiles).size !== value.executableFiles.length
57
+ || new Set(value.contractFiles).size !== value.contractFiles.length) {
58
+ throw new Error('Lifecycle manifest file list is invalid');
59
+ }
60
+ return Object.freeze({
61
+ executableFiles: Object.freeze([...value.executableFiles] as string[]),
62
+ contractFiles: Object.freeze([...value.contractFiles] as string[])
63
+ });
64
+ }
65
+
66
+ function executablePackageRoot(): string {
67
+ let current = path.dirname(fileURLToPath(import.meta.url));
68
+ while (true) {
69
+ const candidate = path.join(current, 'package.json');
70
+ if (fs.existsSync(candidate)) {
71
+ const value = JSON.parse(fs.readFileSync(candidate, 'utf8')) as { name?: unknown };
72
+ if (value.name === '@fitlab-ai/agent-infra') return current;
73
+ }
74
+ const parent = path.dirname(current);
75
+ if (parent === current) throw new Error('Lifecycle executable package root is unavailable');
76
+ current = parent;
77
+ }
78
+ }
79
+
80
+ function hashFiles(root: string, files: readonly string[]): string {
81
+ const hash = crypto.createHash('sha256');
82
+ for (const relative of [...new Set(files.map(normalizeRelativePath))].sort()) {
83
+ if (path.isAbsolute(relative) || relative.split(/[\\/]/u).includes('..')) {
84
+ throw new Error(`Lifecycle manifest path '${relative}' is unsafe`);
85
+ }
86
+ const file = path.join(root, relative);
87
+ const stat = fs.lstatSync(file);
88
+ if (!stat.isFile() || stat.isSymbolicLink()) {
89
+ throw new Error(`Lifecycle manifest entry '${relative}' must be a regular file`);
90
+ }
91
+ hash.update(relative);
92
+ hash.update('\0');
93
+ hash.update(fs.readFileSync(file));
94
+ hash.update('\0');
95
+ }
96
+ return hash.digest('hex');
97
+ }
98
+
99
+ function resolveLifecycleExecutableFiles(root: string, entries: readonly string[]): string[] {
100
+ const pending = [...entries];
101
+ const resolved = new Set<string>();
102
+ while (pending.length > 0) {
103
+ const relative = normalizeRelativePath(pending.pop()!);
104
+ if (resolved.has(relative)) continue;
105
+ resolved.add(relative);
106
+ if (!/\.[cm]?[jt]s$/u.test(relative)) continue;
107
+ const file = path.join(root, relative);
108
+ const source = fs.readFileSync(file, 'utf8');
109
+ const imports = source.matchAll(/(?:\bfrom\s*|\bimport\s*(?:\(\s*)?)["'](\.[^"']+)["']/gu);
110
+ for (const match of imports) {
111
+ const requested = match[1]!;
112
+ const base = path.resolve(path.dirname(file), requested);
113
+ const candidates = [
114
+ base,
115
+ `${base}.ts`, `${base}.js`,
116
+ path.join(base, 'index.ts'), path.join(base, 'index.js')
117
+ ];
118
+ const dependency = candidates.find((candidate) => fs.existsSync(candidate) && fs.lstatSync(candidate).isFile());
119
+ if (!dependency) throw new Error(`Lifecycle executable dependency '${requested}' from '${relative}' is unavailable`);
120
+ const dependencyRelative = path.relative(root, dependency);
121
+ if (dependencyRelative.startsWith('..') || path.isAbsolute(dependencyRelative)) {
122
+ throw new Error(`Lifecycle executable dependency '${requested}' escapes the package root`);
123
+ }
124
+ pending.push(normalizeRelativePath(dependencyRelative));
125
+ }
126
+ }
127
+ return [...resolved].sort();
128
+ }
129
+
130
+ function computeLifecycleBuildIdentity(
131
+ repoRoot: string,
132
+ options: LifecycleBuildIdentityOptions = {}
133
+ ): LifecycleBuildIdentity {
134
+ const packageRoot = executablePackageRoot();
135
+ const executableRoot = options.executableFiles ? repoRoot : packageRoot;
136
+ const defaults = readLifecycleManifestFiles(packageRoot);
137
+ const packageJson = JSON.parse(fs.readFileSync(path.join(executableRoot, 'package.json'), 'utf8')) as {
138
+ version?: unknown;
139
+ };
140
+ if (!exactText(packageJson.version)) throw new Error('Lifecycle package version is invalid');
141
+ let internalExecutableBuildHash: string;
142
+ if (options.executableFiles) {
143
+ internalExecutableBuildHash = hashFiles(repoRoot, options.executableFiles);
144
+ } else {
145
+ const manifestFile = path.join(executableRoot, 'dist', 'lifecycle-build-manifest.json');
146
+ if (fs.existsSync(manifestFile)) {
147
+ const manifest = JSON.parse(fs.readFileSync(manifestFile, 'utf8')) as {
148
+ protocolVersion?: unknown;
149
+ packageVersion?: unknown;
150
+ sourceInputHash?: unknown;
151
+ internalExecutableBuildHash?: unknown;
152
+ };
153
+ if (manifest.protocolVersion !== LIFECYCLE_PROTOCOL_VERSION
154
+ || manifest.packageVersion !== packageJson.version
155
+ || !exactText(manifest.sourceInputHash)
156
+ || !exactText(manifest.internalExecutableBuildHash)) {
157
+ throw new Error('Lifecycle executable build manifest is stale or invalid');
158
+ }
159
+ if (fileURLToPath(import.meta.url).split(path.sep).includes('dist')) {
160
+ const compiledFiles = defaults.executableFiles.map((file) =>
161
+ file.endsWith('.ts') ? path.join('dist', file.replace(/\.ts$/u, '.js')) : file
162
+ );
163
+ if (hashFiles(executableRoot, resolveLifecycleExecutableFiles(executableRoot, compiledFiles)) !== manifest.internalExecutableBuildHash) {
164
+ throw new Error('Lifecycle compiled executable build does not match its manifest');
165
+ }
166
+ } else if (hashFiles(executableRoot, resolveLifecycleExecutableFiles(executableRoot, defaults.executableFiles)) !== manifest.sourceInputHash) {
167
+ throw new Error('Lifecycle executable build manifest is stale or invalid');
168
+ }
169
+ internalExecutableBuildHash = manifest.internalExecutableBuildHash;
170
+ } else internalExecutableBuildHash = hashFiles(
171
+ executableRoot,
172
+ resolveLifecycleExecutableFiles(executableRoot, defaults.executableFiles)
173
+ );
174
+ }
175
+ return Object.freeze({
176
+ protocolVersion: LIFECYCLE_PROTOCOL_VERSION,
177
+ packageVersion: packageJson.version,
178
+ internalExecutableBuildHash,
179
+ lifecycleContractHash: hashFiles(
180
+ repoRoot,
181
+ options.contractFiles ?? defaults.contractFiles
182
+ )
183
+ });
184
+ }
185
+
186
+ function verifyLifecycleBuildIdentity(
187
+ expected: LifecycleBuildIdentity,
188
+ actual: LifecycleBuildIdentity
189
+ ): IdentityVerification {
190
+ if (actual.protocolVersion !== expected.protocolVersion) {
191
+ return {
192
+ ok: false,
193
+ code: 'CODEX_LIFECYCLE_PROTOCOL_MISMATCH',
194
+ message: 'Codex lifecycle protocol version does not match'
195
+ };
196
+ }
197
+ if (actual.packageVersion !== expected.packageVersion
198
+ || actual.internalExecutableBuildHash !== expected.internalExecutableBuildHash) {
199
+ return {
200
+ ok: false,
201
+ code: 'CODEX_LIFECYCLE_BUILD_MISMATCH',
202
+ message: 'Codex lifecycle executable build identity does not match'
203
+ };
204
+ }
205
+ if (actual.lifecycleContractHash !== expected.lifecycleContractHash) {
206
+ return {
207
+ ok: false,
208
+ code: 'CODEX_LIFECYCLE_CONTRACT_MISMATCH',
209
+ message: 'Codex lifecycle contract identity does not match'
210
+ };
211
+ }
212
+ return { ok: true, code: null, message: null };
213
+ }
214
+
215
+ export {
216
+ LIFECYCLE_PROTOCOL_VERSION,
217
+ computeLifecycleBuildIdentity,
218
+ readLifecycleManifestFiles,
219
+ resolveLifecycleExecutableFiles,
220
+ verifyLifecycleBuildIdentity
221
+ };
222
+ export type {
223
+ IdentityVerification,
224
+ LifecycleBuildIdentity,
225
+ LifecycleManifestFiles,
226
+ LifecycleBuildIdentityOptions
227
+ };
@@ -0,0 +1,321 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+
5
+ import { verifyLifecycleBuildIdentity } from './build-identity.ts';
6
+ import type { LifecycleBuildIdentity } from './build-identity.ts';
7
+
8
+ type CapabilityStatus = 'armed' | 'attested' | 'consumed' | 'expired';
9
+ type CodexControllerBinding = Readonly<{
10
+ instanceDigest: string;
11
+ controlGeneration: string;
12
+ }>;
13
+ type CodexCapabilityRecord = Readonly<{
14
+ schemaVersion: 1;
15
+ revision: number;
16
+ tokenDigest: string;
17
+ taskId: string;
18
+ status: CapabilityStatus;
19
+ buildIdentity: LifecycleBuildIdentity;
20
+ controller: CodexControllerBinding | null;
21
+ sessionId: string | null;
22
+ turnId: string | null;
23
+ toolUseId: string | null;
24
+ hookDefinitionHash: string | null;
25
+ armedAt: number;
26
+ expiresAt: number;
27
+ attestedAt: number | null;
28
+ consumedAt: number | null;
29
+ }>;
30
+
31
+ type CodexCapabilityStoreOptions = Readonly<{
32
+ root?: string;
33
+ now?: () => number;
34
+ token?: () => string;
35
+ ttlMs?: number;
36
+ tombstoneMs?: number;
37
+ maxRecords?: number;
38
+ beforeCompareAndSwap?: (input: Readonly<{ path: string; expectedRevision: number }>) => void;
39
+ }>;
40
+
41
+ const DEFAULT_TTL_MS = 30_000;
42
+ const DEFAULT_TOMBSTONE_MS = 86_400_000;
43
+ const DEFAULT_MAX_RECORDS = 128;
44
+ const LOCK_RETRY_MS = 10;
45
+ const LOCK_TIMEOUT_MS = 1_000;
46
+ const LOCK_STALE_MS = 30_000;
47
+
48
+ function digest(value: string): string {
49
+ return crypto.createHash('sha256').update(value).digest('hex');
50
+ }
51
+
52
+ function capabilityError(code: string, message: string): Error {
53
+ const error = new Error(`${code}: ${message}`);
54
+ error.name = code;
55
+ return error;
56
+ }
57
+
58
+ function sameController(
59
+ left: CodexControllerBinding | null,
60
+ right: CodexControllerBinding | null
61
+ ): boolean {
62
+ return left?.instanceDigest === right?.instanceDigest
63
+ && left?.controlGeneration === right?.controlGeneration;
64
+ }
65
+
66
+ function createCodexCapabilityStore(options: CodexCapabilityStoreOptions = {}) {
67
+ const root = options.root
68
+ ?? path.join(process.cwd(), '.agents', 'workspace', '.runtime', 'codex-capabilities');
69
+ const now = options.now ?? Date.now;
70
+ const createToken = options.token ?? (() => crypto.randomBytes(32).toString('base64url'));
71
+ const ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
72
+ const tombstoneMs = options.tombstoneMs ?? DEFAULT_TOMBSTONE_MS;
73
+ const maxRecords = options.maxRecords ?? DEFAULT_MAX_RECORDS;
74
+ fs.mkdirSync(root, { recursive: true, mode: 0o700 });
75
+ fs.chmodSync(root, 0o700);
76
+
77
+ function fileFor(token: string): string {
78
+ return path.join(root, `${digest(token)}.json`);
79
+ }
80
+
81
+ function read(file: string): CodexCapabilityRecord {
82
+ const value = JSON.parse(fs.readFileSync(file, 'utf8')) as CodexCapabilityRecord;
83
+ if (value.schemaVersion !== 1 || !Number.isSafeInteger(value.revision) || value.revision < 1) {
84
+ throw capabilityError('CODEX_CAPABILITY_STATE_INVALID', 'capability record schema is invalid');
85
+ }
86
+ return value;
87
+ }
88
+
89
+ function withWriteLock<T>(operation: () => T): T {
90
+ const lock = path.join(root, '.write.lock');
91
+ const deadline = Date.now() + LOCK_TIMEOUT_MS;
92
+ let descriptor: number | null = null;
93
+ while (descriptor === null) {
94
+ try {
95
+ descriptor = fs.openSync(lock, 'wx', 0o600);
96
+ } catch (error) {
97
+ if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;
98
+ try {
99
+ if (Date.now() - fs.statSync(lock).mtimeMs > LOCK_STALE_MS) {
100
+ fs.unlinkSync(lock);
101
+ continue;
102
+ }
103
+ } catch (lockError) {
104
+ if ((lockError as NodeJS.ErrnoException).code === 'ENOENT') continue;
105
+ throw lockError;
106
+ }
107
+ if (Date.now() >= deadline) {
108
+ throw capabilityError('CODEX_CAPABILITY_STORE_BUSY', 'capability store is busy');
109
+ }
110
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, LOCK_RETRY_MS);
111
+ }
112
+ }
113
+ try {
114
+ return operation();
115
+ } finally {
116
+ fs.closeSync(descriptor);
117
+ fs.unlinkSync(lock);
118
+ }
119
+ }
120
+
121
+ function write(file: string, value: CodexCapabilityRecord, expectedRevision: number): void {
122
+ const temporary = `${file}.${process.pid}.${crypto.randomUUID()}.tmp`;
123
+ fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, {
124
+ encoding: 'utf8',
125
+ flag: 'wx',
126
+ mode: 0o600
127
+ });
128
+ try {
129
+ withWriteLock(() => {
130
+ options.beforeCompareAndSwap?.({ path: file, expectedRevision });
131
+ const actualRevision = fs.existsSync(file) ? read(file).revision : 0;
132
+ if (actualRevision !== expectedRevision) {
133
+ throw capabilityError(
134
+ 'CODEX_CAPABILITY_REVISION_CONFLICT',
135
+ `capability revision changed from ${expectedRevision} to ${actualRevision}`
136
+ );
137
+ }
138
+ fs.renameSync(temporary, file);
139
+ fs.chmodSync(file, 0o600);
140
+ });
141
+ } finally {
142
+ if (fs.existsSync(temporary)) fs.unlinkSync(temporary);
143
+ }
144
+ }
145
+
146
+ function files(): string[] {
147
+ return fs.readdirSync(root)
148
+ .filter((name) => /^[a-f0-9]{64}\.json$/u.test(name))
149
+ .map((name) => path.join(root, name));
150
+ }
151
+
152
+ function expire(record: CodexCapabilityRecord, file: string, timestamp: number): CodexCapabilityRecord {
153
+ if ((record.status === 'armed' || record.status === 'attested') && timestamp > record.expiresAt) {
154
+ const expired = Object.freeze({
155
+ ...record,
156
+ revision: record.revision + 1,
157
+ status: 'expired' as const
158
+ });
159
+ write(file, expired, record.revision);
160
+ return expired;
161
+ }
162
+ return record;
163
+ }
164
+
165
+ function sweep(): void {
166
+ const timestamp = now();
167
+ const records = files().map((file) => ({ file, record: expire(read(file), file, timestamp) }));
168
+ for (const entry of records) {
169
+ const terminalAt = entry.record.consumedAt ?? entry.record.expiresAt;
170
+ if (['consumed', 'expired'].includes(entry.record.status)
171
+ && timestamp - terminalAt > tombstoneMs) {
172
+ fs.unlinkSync(entry.file);
173
+ }
174
+ }
175
+ const remaining = files().map((file) => ({ file, record: read(file) }));
176
+ if (remaining.length <= maxRecords) return;
177
+ const removable = remaining
178
+ .filter(({ record }) => ['consumed', 'expired'].includes(record.status))
179
+ .sort((left, right) =>
180
+ (left.record.consumedAt ?? left.record.expiresAt)
181
+ - (right.record.consumedAt ?? right.record.expiresAt));
182
+ while (files().length > maxRecords && removable.length > 0) {
183
+ fs.unlinkSync(removable.shift()!.file);
184
+ }
185
+ if (files().length > maxRecords) {
186
+ throw capabilityError('CODEX_CAPABILITY_CAPACITY_EXCEEDED', 'capability store has too many active records');
187
+ }
188
+ }
189
+
190
+ function arm(input: Readonly<{
191
+ taskId: string;
192
+ buildIdentity: LifecycleBuildIdentity;
193
+ controller?: CodexControllerBinding;
194
+ }>): CodexCapabilityRecord & Readonly<{ token: string; path: string; marker: string }> {
195
+ sweep();
196
+ const token = createToken();
197
+ const file = fileFor(token);
198
+ if (fs.existsSync(file)) throw capabilityError('CODEX_CAPABILITY_TOKEN_COLLISION', 'capability token already exists');
199
+ const timestamp = now();
200
+ const record: CodexCapabilityRecord = Object.freeze({
201
+ schemaVersion: 1,
202
+ revision: 1,
203
+ tokenDigest: digest(token),
204
+ taskId: input.taskId,
205
+ status: 'armed',
206
+ buildIdentity: input.buildIdentity,
207
+ controller: input.controller ?? null,
208
+ sessionId: null,
209
+ turnId: null,
210
+ toolUseId: null,
211
+ hookDefinitionHash: null,
212
+ armedAt: timestamp,
213
+ expiresAt: timestamp + ttlMs,
214
+ attestedAt: null,
215
+ consumedAt: null
216
+ });
217
+ write(file, record, 0);
218
+ return Object.freeze({
219
+ ...record,
220
+ token,
221
+ path: file,
222
+ marker: `agent-infra-codex-capability:${token}`
223
+ });
224
+ }
225
+
226
+ function loadActive(token: string): Readonly<{ file: string; record: CodexCapabilityRecord }> {
227
+ const file = fileFor(token);
228
+ if (!fs.existsSync(file)) {
229
+ throw capabilityError('CODEX_CAPABILITY_MISSING', 'capability token was not found');
230
+ }
231
+ const record = expire(read(file), file, now());
232
+ if (record.status === 'expired') {
233
+ throw capabilityError('CODEX_CAPABILITY_EXPIRED', 'capability token expired');
234
+ }
235
+ return { file, record };
236
+ }
237
+
238
+ function attest(input: Readonly<{
239
+ token: string;
240
+ sessionId: string;
241
+ turnId: string;
242
+ toolUseId: string;
243
+ hookDefinitionHash: string;
244
+ buildIdentity: LifecycleBuildIdentity;
245
+ controller?: CodexControllerBinding;
246
+ }>): CodexCapabilityRecord {
247
+ const { file, record } = loadActive(input.token);
248
+ if (record.status !== 'armed') {
249
+ throw capabilityError(
250
+ record.status === 'consumed' ? 'CODEX_CAPABILITY_REPLAY' : 'CODEX_CAPABILITY_STATE_INVALID',
251
+ `capability is ${record.status}, expected armed`
252
+ );
253
+ }
254
+ const identity = verifyLifecycleBuildIdentity(record.buildIdentity, input.buildIdentity);
255
+ if (!identity.ok || !sameController(record.controller, input.controller ?? null)) {
256
+ throw capabilityError('CODEX_CAPABILITY_PROVENANCE_MISMATCH', 'capability build or controller identity does not match');
257
+ }
258
+ if (![input.sessionId, input.turnId, input.toolUseId, input.hookDefinitionHash]
259
+ .every((value) => value.trim().length > 0)) {
260
+ throw capabilityError('CODEX_CAPABILITY_IDENTITY_INVALID', 'hook identity is incomplete');
261
+ }
262
+ const timestamp = now();
263
+ const next: CodexCapabilityRecord = Object.freeze({
264
+ ...record,
265
+ revision: record.revision + 1,
266
+ status: 'attested',
267
+ sessionId: input.sessionId,
268
+ turnId: input.turnId,
269
+ toolUseId: input.toolUseId,
270
+ hookDefinitionHash: input.hookDefinitionHash,
271
+ attestedAt: timestamp
272
+ });
273
+ write(file, next, record.revision);
274
+ return next;
275
+ }
276
+
277
+ function consume(token: string, expected: Readonly<{
278
+ taskId: string;
279
+ hookDefinitionHash: string;
280
+ buildIdentity: LifecycleBuildIdentity;
281
+ controller?: CodexControllerBinding;
282
+ }>): CodexCapabilityRecord {
283
+ sweep();
284
+ const { file, record } = loadActive(token);
285
+ if (record.status === 'consumed') {
286
+ throw capabilityError('CODEX_CAPABILITY_REPLAY', 'capability token was already consumed');
287
+ }
288
+ if (record.status !== 'attested') {
289
+ throw capabilityError('CODEX_CAPABILITY_NOT_ATTESTED', 'capability token has no current-session hook attestation');
290
+ }
291
+ const identity = verifyLifecycleBuildIdentity(record.buildIdentity, expected.buildIdentity);
292
+ if (!identity.ok
293
+ || record.taskId !== expected.taskId
294
+ || record.hookDefinitionHash !== expected.hookDefinitionHash
295
+ || !sameController(record.controller, expected.controller ?? null)) {
296
+ throw capabilityError('CODEX_CAPABILITY_PROVENANCE_MISMATCH', 'capability consumption identity does not match');
297
+ }
298
+ const next: CodexCapabilityRecord = Object.freeze({
299
+ ...record,
300
+ revision: record.revision + 1,
301
+ status: 'consumed',
302
+ consumedAt: now()
303
+ });
304
+ write(file, next, record.revision);
305
+ return next;
306
+ }
307
+
308
+ function inspect(token: string): CodexCapabilityRecord {
309
+ return loadActive(token).record;
310
+ }
311
+
312
+ return Object.freeze({ arm, attest, consume, inspect, sweep });
313
+ }
314
+
315
+ export { createCodexCapabilityStore };
316
+ export type {
317
+ CapabilityStatus,
318
+ CodexCapabilityRecord,
319
+ CodexCapabilityStoreOptions,
320
+ CodexControllerBinding
321
+ };
@@ -21,7 +21,9 @@ type CodexLifecycleEvent =
21
21
  sessionId: string;
22
22
  turnId: string;
23
23
  childThreadId: string;
24
+ parentThreadId: string;
24
25
  nativeAgent: string;
26
+ source?: 'hook' | 'parent-rollout';
25
27
  }>
26
28
  | Readonly<{
27
29
  type: 'app-thread';
@@ -29,6 +31,7 @@ type CodexLifecycleEvent =
29
31
  parentThreadId: string;
30
32
  forkedFromId: string | null;
31
33
  sourceParentThreadId: string;
34
+ nativeAgent: string;
32
35
  }>
33
36
  | Readonly<{
34
37
  type: 'app-settings';
@@ -56,6 +59,7 @@ type CodexLifecycleEvent =
56
59
  turnId: string;
57
60
  childThreadId: string;
58
61
  nativeAgent: string;
62
+ source?: 'hook' | 'parent-rollout';
59
63
  }>;
60
64
 
61
65
  type CodexStartEvidence = Readonly<{
@@ -79,7 +83,7 @@ type CodexStopEvidence = Readonly<{
79
83
  childThreadId: string;
80
84
  turnId: string;
81
85
  terminalStatus: 'completed';
82
- hookStopObserved: true;
86
+ hookStopObserved: boolean;
83
87
  }>;
84
88
 
85
89
  type CodexLifecycleStatus =
@@ -153,20 +157,32 @@ function requiredStrings(event: CodexLifecycleEvent): boolean {
153
157
  function derive(state: CodexLifecycleState): CodexLifecycleState {
154
158
  const { spawn, child, thread, settings, reroute, terminal, stop } = state;
155
159
  if (spawn && child && (
156
- spawn.sessionId !== child.sessionId
160
+ spawn.sessionId !== child.parentThreadId
157
161
  || spawn.nativeAgent !== child.nativeAgent
158
162
  )) {
159
163
  return invalid(state, 'CODEX_EVIDENCE_IDENTITY_MISMATCH', 'hook spawn and child identity do not match');
160
164
  }
165
+ if (child && child.sessionId !== child.parentThreadId) {
166
+ return invalid(state, 'CODEX_EVIDENCE_IDENTITY_MISMATCH', 'child event session does not match the parent thread');
167
+ }
161
168
  if (child && thread && child.childThreadId !== thread.childThreadId) {
162
169
  return invalid(state, 'CODEX_EVIDENCE_IDENTITY_MISMATCH', 'hook and App Server child identity do not match');
163
170
  }
171
+ if (child && thread && child.nativeAgent !== thread.nativeAgent) {
172
+ return invalid(state, 'CODEX_EVIDENCE_IDENTITY_MISMATCH', 'hook and App Server agent roles do not match');
173
+ }
174
+ if (child && thread && child.parentThreadId !== thread.parentThreadId) {
175
+ return invalid(state, 'CODEX_EVIDENCE_PARENT_MISMATCH', 'hook and App Server parent identity do not match');
176
+ }
164
177
  if (spawn && thread && (
165
178
  thread.parentThreadId !== spawn.sessionId
166
179
  || thread.sourceParentThreadId !== spawn.sessionId
167
180
  )) {
168
181
  return invalid(state, 'CODEX_EVIDENCE_PARENT_MISMATCH', 'App Server parent identity does not match the spawning session');
169
182
  }
183
+ if (spawn && thread && spawn.nativeAgent !== thread.nativeAgent) {
184
+ return invalid(state, 'CODEX_EVIDENCE_IDENTITY_MISMATCH', 'spawn and App Server agent roles do not match');
185
+ }
170
186
  if (thread?.forkedFromId !== null && thread?.forkedFromId !== undefined) {
171
187
  return invalid(state, 'CODEX_EVIDENCE_FORK_FORBIDDEN', 'forked child threads are not fresh lifecycle evidence');
172
188
  }
@@ -233,7 +249,7 @@ function derive(state: CodexLifecycleState): CodexLifecycleState {
233
249
  childThreadId: startEvidence.childThreadId,
234
250
  turnId: terminal.turnId,
235
251
  terminalStatus: 'completed',
236
- hookStopObserved: true
252
+ hookStopObserved: stop.source !== 'parent-rollout'
237
253
  });
238
254
  }
239
255
  const status: CodexLifecycleStatus = stopEvidence
@@ -0,0 +1,26 @@
1
+ {
2
+ "executableFiles": [
3
+ "bin/internal-cli.ts",
4
+ "lib/internal/codex-lifecycle.ts",
5
+ "lib/internal/codex-sandbox-controller.ts",
6
+ "lib/internal/task-orchestration.ts",
7
+ "lib/task/codex-orchestration.ts",
8
+ "lib/task/delegation-receipts.ts",
9
+ "lib/task/orchestration.ts",
10
+ "lib/agent-clients/adapters/codex-lifecycle/build-identity.ts",
11
+ "lib/agent-clients/adapters/codex-lifecycle/capability-store.ts",
12
+ "lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts",
13
+ "lib/agent-clients/adapters/codex-lifecycle/app-server.ts",
14
+ "lib/agent-clients/adapters/codex-lifecycle/evidence.ts",
15
+ "lib/agent-clients/adapters/codex-lifecycle/store.ts",
16
+ "lib/agent-clients/adapters/codex-lifecycle/manifest-files.json"
17
+ ],
18
+ "contractFiles": [
19
+ ".codex/hooks.json",
20
+ ".codex/agents/agent-infra-lifecycle-executor.toml",
21
+ ".codex/agents/agent-infra-lifecycle-reviewer.toml",
22
+ ".agents/hooks/lifecycle-delegation.js",
23
+ ".agents/skills/run-task/SKILL.md",
24
+ ".agents/rules/lifecycle-orchestration.md"
25
+ ]
26
+ }