@fitlab-ai/agent-infra 0.9.8 → 0.9.10

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 (182) 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/codex-lifecycle/app-server.js +58 -21
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +25 -11
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +173 -10
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-context.js +127 -0
  8. package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -0
  9. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +63 -92
  10. package/dist/lib/agent-clients/adapters/codex-sandbox.js +72 -11
  11. package/dist/lib/agent-clients/adapters/index.js +3 -1
  12. package/dist/lib/agent-clients/adapters/traecli.js +66 -0
  13. package/dist/lib/agent-clients/tokens.js +2 -2
  14. package/dist/lib/agent-clients/types.js +2 -1
  15. package/dist/lib/defaults.json +5 -0
  16. package/dist/lib/git/workflow.js +10 -1
  17. package/dist/lib/internal/codex-lifecycle.js +20 -6
  18. package/dist/lib/internal/codex-sandbox-controller.js +9 -15
  19. package/dist/lib/internal/git-workflow.js +5 -7
  20. package/dist/lib/internal/platform-pr.js +20 -7
  21. package/dist/lib/internal/release-workflow.js +2 -1
  22. package/dist/lib/internal/sandbox-control.js +79 -2
  23. package/dist/lib/internal/task-finalization.js +54 -0
  24. package/dist/lib/internal/task-orchestration.js +156 -291
  25. package/dist/lib/{task/commands/validate.js → internal/task-validate.js} +75 -42
  26. package/dist/lib/platform/pr-summary.js +48 -10
  27. package/dist/lib/platform/pull-requests.js +205 -41
  28. package/dist/lib/run/tui.js +5 -2
  29. package/dist/lib/sandbox/commands/rm.js +316 -55
  30. package/dist/lib/sandbox/control/client.js +190 -6
  31. package/dist/lib/sandbox/control/controller-registration.js +275 -0
  32. package/dist/lib/sandbox/control/executor.js +140 -5
  33. package/dist/lib/sandbox/control/lifecycle.js +3 -1
  34. package/dist/lib/sandbox/control/protocol.js +67 -6
  35. package/dist/lib/sandbox/index.js +5 -3
  36. package/dist/lib/sandbox/managed-fs.js +5 -0
  37. package/dist/lib/sandbox/task-resolver.js +50 -6
  38. package/dist/lib/sandbox/workspace-identity.js +27 -0
  39. package/dist/lib/sandbox/worktree-safety.js +215 -2
  40. package/dist/lib/task/codex-orchestration.js +161 -39
  41. package/dist/lib/task/commands/status.js +59 -1
  42. package/dist/lib/task/commit-operation.js +335 -0
  43. package/dist/lib/task/commit-policy.js +18 -0
  44. package/dist/lib/task/delegation-receipts.js +248 -6
  45. package/dist/lib/task/finalization.js +593 -0
  46. package/dist/lib/task/index.js +0 -9
  47. package/dist/lib/task/operation-outcome.js +33 -0
  48. package/dist/lib/task/orchestration.js +187 -761
  49. package/dist/lib/task/short-id.js +59 -1
  50. package/dist/lib/task/task-execution-lock.js +6 -1
  51. package/dist/lib/task/verification-engine.js +110 -60
  52. package/dist/lib/task/verification.js +2 -1
  53. package/dist/lib/task/workflow-warning-intents.js +50 -1
  54. package/dist/lifecycle-build-manifest.json +3 -3
  55. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +61 -19
  56. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +37 -10
  57. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +276 -12
  58. package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +163 -0
  59. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -0
  60. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +9 -1
  61. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +91 -118
  62. package/lib/agent-clients/adapters/codex-sandbox.ts +89 -14
  63. package/lib/agent-clients/adapters/index.ts +3 -1
  64. package/lib/agent-clients/adapters/traecli.ts +67 -0
  65. package/lib/agent-clients/tokens.ts +2 -2
  66. package/lib/agent-clients/types.ts +2 -1
  67. package/lib/defaults.json +5 -0
  68. package/lib/git/workflow.ts +11 -2
  69. package/lib/internal/codex-lifecycle.ts +19 -6
  70. package/lib/internal/codex-sandbox-controller.ts +9 -14
  71. package/lib/internal/git-workflow.ts +5 -6
  72. package/lib/internal/platform-pr.ts +14 -7
  73. package/lib/internal/release-workflow.ts +2 -1
  74. package/lib/internal/sandbox-control.ts +92 -2
  75. package/lib/internal/task-finalization.ts +68 -0
  76. package/lib/internal/task-orchestration.ts +20 -103
  77. package/lib/{task/commands/validate.ts → internal/task-validate.ts} +75 -40
  78. package/lib/platform/pr-summary.ts +55 -11
  79. package/lib/platform/pull-requests.ts +211 -41
  80. package/lib/run/tui.ts +5 -3
  81. package/lib/sandbox/commands/rm.ts +413 -61
  82. package/lib/sandbox/control/client.ts +255 -6
  83. package/lib/sandbox/control/controller-registration.ts +352 -0
  84. package/lib/sandbox/control/executor.ts +169 -5
  85. package/lib/sandbox/control/lifecycle.ts +2 -1
  86. package/lib/sandbox/control/protocol.ts +80 -7
  87. package/lib/sandbox/index.ts +5 -3
  88. package/lib/sandbox/managed-fs.ts +5 -0
  89. package/lib/sandbox/task-resolver.ts +58 -7
  90. package/lib/sandbox/workspace-identity.ts +50 -2
  91. package/lib/sandbox/workspace-view.ts +3 -3
  92. package/lib/sandbox/worktree-safety.ts +236 -2
  93. package/lib/task/codex-orchestration.ts +168 -40
  94. package/lib/task/commands/status.ts +78 -1
  95. package/lib/task/commit-operation.ts +430 -0
  96. package/lib/task/commit-policy.ts +30 -0
  97. package/lib/task/delegation-receipts.ts +263 -4
  98. package/lib/task/finalization.ts +761 -0
  99. package/lib/task/index.ts +0 -8
  100. package/lib/task/operation-outcome.ts +48 -0
  101. package/lib/task/orchestration.ts +218 -982
  102. package/lib/task/short-id.ts +65 -1
  103. package/lib/task/task-execution-lock.ts +10 -1
  104. package/lib/task/verification-engine.ts +115 -63
  105. package/lib/task/verification.ts +3 -2
  106. package/lib/task/workflow-warning-intents.ts +57 -2
  107. package/package.json +2 -2
  108. package/templates/.agents/QUICKSTART.en.md +3 -4
  109. package/templates/.agents/QUICKSTART.zh-CN.md +3 -4
  110. package/templates/.agents/hooks/lifecycle-delegation.js +1 -21
  111. package/templates/.agents/rules/README.en.md +1 -0
  112. package/templates/.agents/rules/README.zh-CN.md +1 -0
  113. package/templates/.agents/rules/compatibility-policy.en.md +39 -0
  114. package/templates/.agents/rules/compatibility-policy.zh-CN.md +39 -0
  115. package/templates/.agents/rules/lifecycle-orchestration.en.md +4 -4
  116. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +4 -4
  117. package/templates/.agents/rules/pr-sync.en.md +1 -1
  118. package/templates/.agents/rules/pr-sync.github.en.md +2 -1
  119. package/templates/.agents/rules/pr-sync.github.zh-CN.md +2 -0
  120. package/templates/.agents/rules/pr-sync.zh-CN.md +1 -0
  121. package/templates/.agents/rules/review-method.en.md +1 -0
  122. package/templates/.agents/rules/review-method.zh-CN.md +1 -0
  123. package/templates/.agents/rules/task-management.en.md +2 -2
  124. package/templates/.agents/rules/task-management.zh-CN.md +2 -2
  125. package/templates/.agents/skills/analyze-task/SKILL.en.md +1 -0
  126. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +1 -0
  127. package/templates/.agents/skills/block-task/SKILL.en.md +2 -4
  128. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -4
  129. package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -3
  130. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +3 -3
  131. package/templates/.agents/skills/close-codescan/SKILL.en.md +6 -2
  132. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +6 -2
  133. package/templates/.agents/skills/close-dependabot/SKILL.en.md +6 -2
  134. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +6 -2
  135. package/templates/.agents/skills/code-task/SKILL.en.md +1 -0
  136. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +2 -0
  137. package/templates/.agents/skills/commit/SKILL.en.md +41 -125
  138. package/templates/.agents/skills/commit/SKILL.zh-CN.md +52 -123
  139. package/templates/.agents/skills/commit/reference/commit-message.en.md +1 -1
  140. package/templates/.agents/skills/commit/reference/commit-message.zh-CN.md +1 -1
  141. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +14 -43
  142. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +14 -49
  143. package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +1 -1
  144. package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +1 -1
  145. package/templates/.agents/skills/commit/reference/task-status-update.en.md +2 -35
  146. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +2 -35
  147. package/templates/.agents/skills/complete-task/SKILL.en.md +31 -38
  148. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +31 -38
  149. package/templates/.agents/skills/complete-task/config/verify.en.json +1 -0
  150. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +1 -0
  151. package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +1 -1
  152. package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +1 -1
  153. package/templates/.agents/skills/create-pr/SKILL.en.md +17 -9
  154. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +17 -9
  155. package/templates/.agents/skills/create-pr/reference/change-report.en.md +41 -0
  156. package/templates/.agents/skills/create-pr/reference/change-report.zh-CN.md +41 -0
  157. package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +2 -2
  158. package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +2 -2
  159. package/templates/.agents/skills/plan-task/SKILL.en.md +2 -1
  160. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +2 -1
  161. package/templates/.agents/skills/review-code/reference/migration-risks.en.md +8 -6
  162. package/templates/.agents/skills/review-code/reference/migration-risks.zh-CN.md +8 -6
  163. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +3 -3
  164. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +3 -3
  165. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +1 -1
  166. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +1 -1
  167. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -2
  168. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -2
  169. package/templates/.agents/skills/run-task/SKILL.en.md +6 -6
  170. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +6 -6
  171. package/templates/.agents/skills/run-task/reference/host-validation.en.md +2 -2
  172. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +2 -2
  173. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +24 -0
  174. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +1 -1
  175. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +1 -1
  176. package/templates/.codex/hooks.json +4 -4
  177. package/templates/.trae/skills/update-agent-infra.en.md +8 -0
  178. package/templates/.trae/skills/update-agent-infra.zh-CN.md +8 -0
  179. package/dist/lib/task/commit-finalization.js +0 -305
  180. package/dist/lib/task/commit-intent.js +0 -165
  181. package/lib/task/commit-finalization.ts +0 -419
  182. package/lib/task/commit-intent.ts +0 -230
@@ -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<{
@@ -26,12 +28,27 @@ type IdentityVerification = Readonly<{
26
28
  ok: boolean;
27
29
  code: string | null;
28
30
  message: string | null;
31
+ warnings: readonly LifecycleIdentityWarning[];
32
+ }>;
33
+
34
+ type LifecycleIdentityWarning = Readonly<{
35
+ code: 'CODEX_LIFECYCLE_BUILD_MISMATCH' | 'CODEX_LIFECYCLE_CONTRACT_MISMATCH';
36
+ message: string;
37
+ action: 'rebuild-sandbox';
29
38
  }>;
30
39
 
31
40
  function exactText(value: unknown): value is string {
32
41
  return typeof value === 'string' && value.trim() === value && value.length > 0;
33
42
  }
34
43
 
44
+ function isCanonicalLifecyclePackageVersion(value: unknown): value is string {
45
+ return typeof value === 'string' && semver.valid(value) === value;
46
+ }
47
+
48
+ function isLifecycleProtocolVersion(value: unknown): value is typeof LIFECYCLE_PROTOCOL_VERSION {
49
+ return value === LIFECYCLE_PROTOCOL_VERSION;
50
+ }
51
+
35
52
  function normalizeRelativePath(value: string): string {
36
53
  return value.replaceAll('\\', '/');
37
54
  }
@@ -191,36 +208,46 @@ function verifyLifecycleBuildIdentity(
191
208
  return {
192
209
  ok: false,
193
210
  code: 'CODEX_LIFECYCLE_PROTOCOL_MISMATCH',
194
- message: 'Codex lifecycle protocol version does not match'
211
+ message: 'Codex lifecycle protocol version does not match',
212
+ warnings: []
195
213
  };
196
214
  }
215
+ const warnings: LifecycleIdentityWarning[] = [];
197
216
  if (actual.packageVersion !== expected.packageVersion
198
217
  || actual.internalExecutableBuildHash !== expected.internalExecutableBuildHash) {
199
- return {
200
- ok: false,
218
+ warnings.push({
201
219
  code: 'CODEX_LIFECYCLE_BUILD_MISMATCH',
202
- message: 'Codex lifecycle executable build identity does not match'
203
- };
220
+ message: 'Codex lifecycle executable build identity differs; rebuild the sandbox if the runtime is stale',
221
+ action: 'rebuild-sandbox'
222
+ });
204
223
  }
205
224
  if (actual.lifecycleContractHash !== expected.lifecycleContractHash) {
206
- return {
207
- ok: false,
225
+ warnings.push({
208
226
  code: 'CODEX_LIFECYCLE_CONTRACT_MISMATCH',
209
- message: 'Codex lifecycle contract identity does not match'
210
- };
227
+ message: 'Codex lifecycle contract identity differs; rebuild the sandbox if the runtime is stale',
228
+ action: 'rebuild-sandbox'
229
+ });
211
230
  }
212
- return { ok: true, code: null, message: null };
231
+ return {
232
+ ok: true,
233
+ code: null,
234
+ message: null,
235
+ warnings: Object.freeze(warnings)
236
+ };
213
237
  }
214
238
 
215
239
  export {
216
240
  LIFECYCLE_PROTOCOL_VERSION,
217
241
  computeLifecycleBuildIdentity,
242
+ isCanonicalLifecyclePackageVersion,
243
+ isLifecycleProtocolVersion,
218
244
  readLifecycleManifestFiles,
219
245
  resolveLifecycleExecutableFiles,
220
246
  verifyLifecycleBuildIdentity
221
247
  };
222
248
  export type {
223
249
  IdentityVerification,
250
+ LifecycleIdentityWarning,
224
251
  LifecycleBuildIdentity,
225
252
  LifecycleManifestFiles,
226
253
  LifecycleBuildIdentityOptions
@@ -2,7 +2,12 @@ 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';
7
12
  import { resolveAgentRuntimeStoreRoot } from '../../../runtime/agent-runtime.ts';
8
13
 
@@ -11,6 +16,36 @@ type CodexControllerBinding = Readonly<{
11
16
  instanceDigest: string;
12
17
  controlGeneration: string;
13
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
+ }>;
14
49
  type CodexCapabilityRecord = Readonly<{
15
50
  schemaVersion: 1;
16
51
  revision: number;
@@ -45,23 +80,222 @@ const DEFAULT_MAX_RECORDS = 128;
45
80
  const LOCK_RETRY_MS = 10;
46
81
  const LOCK_TIMEOUT_MS = 1_000;
47
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;
48
87
 
49
88
  function digest(value: string): string {
50
89
  return crypto.createHash('sha256').update(value).digest('hex');
51
90
  }
52
91
 
53
- function capabilityError(code: string, message: string): Error {
92
+ function capabilityError(code: string, message: string, detail?: CodexCapabilityProvenanceDetail): Error {
54
93
  const error = new Error(`${code}: ${message}`);
55
94
  error.name = code;
95
+ if (detail) (error as Error & { detail: CodexCapabilityProvenanceDetail }).detail = detail;
56
96
  return error;
57
97
  }
58
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
+
59
290
  function sameController(
60
- left: CodexControllerBinding | null,
61
- right: CodexControllerBinding | null
291
+ left: unknown,
292
+ right: unknown
62
293
  ): boolean {
63
- return left?.instanceDigest === right?.instanceDigest
64
- && 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;
65
299
  }
66
300
 
67
301
  function createCodexCapabilityStore(options: CodexCapabilityStoreOptions = {}) {
@@ -275,27 +509,54 @@ function createCodexCapabilityStore(options: CodexCapabilityStoreOptions = {}) {
275
509
  return next;
276
510
  }
277
511
 
278
- function consume(token: string, expected: Readonly<{
512
+ function validateRecord(record: CodexCapabilityRecord, expected: Readonly<{
279
513
  taskId: string;
280
514
  hookDefinitionHash: string;
281
515
  buildIdentity: LifecycleBuildIdentity;
282
516
  controller?: CodexControllerBinding;
283
517
  }>): CodexCapabilityRecord {
284
- sweep();
285
- const { file, record } = loadActive(token);
286
518
  if (record.status === 'consumed') {
287
519
  throw capabilityError('CODEX_CAPABILITY_REPLAY', 'capability token was already consumed');
288
520
  }
289
521
  if (record.status !== 'attested') {
290
522
  throw capabilityError('CODEX_CAPABILITY_NOT_ATTESTED', 'capability token has no current-session hook attestation');
291
523
  }
292
- 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 };
293
527
  if (!identity.ok
294
528
  || record.taskId !== expected.taskId
295
529
  || record.hookDefinitionHash !== expected.hookDefinitionHash
296
530
  || !sameController(record.controller, expected.controller ?? null)) {
297
- 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
+ );
298
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);
299
560
  const next: CodexCapabilityRecord = Object.freeze({
300
561
  ...record,
301
562
  revision: record.revision + 1,
@@ -310,13 +571,16 @@ function createCodexCapabilityStore(options: CodexCapabilityStoreOptions = {}) {
310
571
  return loadActive(token).record;
311
572
  }
312
573
 
313
- return Object.freeze({ arm, attest, consume, inspect, sweep });
574
+ return Object.freeze({ arm, attest, consume, inspect, validate, sweep });
314
575
  }
315
576
 
316
577
  export { createCodexCapabilityStore };
317
578
  export type {
318
579
  CapabilityStatus,
580
+ CodexCapabilityProjection,
581
+ CodexCapabilityProvenanceDetail,
319
582
  CodexCapabilityRecord,
320
583
  CodexCapabilityStoreOptions,
321
584
  CodexControllerBinding
322
585
  };
586
+ export { isCodexCapabilityProvenanceDetail };
@@ -0,0 +1,163 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+
5
+ import { getProcessIdentityState, type ProcessIdentity } from '../../../server/process-state.ts';
6
+ import type { CodexControllerLeaseProofV1, CodexControllerLeaseV1 } from '../../../sandbox/control/controller-registration.ts';
7
+ import {
8
+ computeLifecycleBuildIdentity,
9
+ verifyLifecycleBuildIdentity,
10
+ type LifecycleBuildIdentity,
11
+ type LifecycleIdentityWarning
12
+ } from './build-identity.ts';
13
+
14
+ const HEX_256 = /^[a-f0-9]{64}$/u;
15
+
16
+ export type CodexSandboxControllerContextV2 = Readonly<{
17
+ version: 2;
18
+ taskId: string;
19
+ controlGeneration: string;
20
+ controllerInstanceDigest: string;
21
+ controllerProcess: ProcessIdentity;
22
+ controllerLease: Readonly<{ version: 1; leaseId: string; leaseSecret: string }>;
23
+ issuedAt: number;
24
+ expiresAt: number;
25
+ buildIdentity: LifecycleBuildIdentity;
26
+ hookDefinitionHash: string;
27
+ }>;
28
+
29
+ function exactKeys(value: Record<string, unknown>, keys: readonly string[]): boolean {
30
+ return Object.keys(value).sort().join(',') === [...keys].sort().join(',');
31
+ }
32
+
33
+ function validProcess(value: unknown): value is ProcessIdentity {
34
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
35
+ const identity = value as Record<string, unknown>;
36
+ return exactKeys(identity, ['pid', 'startTime'])
37
+ && Number.isSafeInteger(identity.pid) && (identity.pid as number) > 0
38
+ && Number.isSafeInteger(identity.startTime) && (identity.startTime as number) >= 0;
39
+ }
40
+
41
+ function validBuildIdentity(value: unknown): value is LifecycleBuildIdentity {
42
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
43
+ const identity = value as Record<string, unknown>;
44
+ return exactKeys(identity, [
45
+ 'internalExecutableBuildHash', 'lifecycleContractHash', 'packageVersion', 'protocolVersion'
46
+ ])
47
+ && identity.protocolVersion === 3
48
+ && typeof identity.packageVersion === 'string' && identity.packageVersion.length > 0
49
+ && typeof identity.internalExecutableBuildHash === 'string' && HEX_256.test(identity.internalExecutableBuildHash)
50
+ && typeof identity.lifecycleContractHash === 'string' && HEX_256.test(identity.lifecycleContractHash);
51
+ }
52
+
53
+ function validateContext(value: unknown): CodexSandboxControllerContextV2 {
54
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
55
+ throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
56
+ }
57
+ const context = value as Record<string, unknown>;
58
+ const lease = context.controllerLease as Record<string, unknown> | null;
59
+ if (!exactKeys(context, [
60
+ 'buildIdentity', 'controlGeneration', 'controllerInstanceDigest', 'controllerLease',
61
+ 'controllerProcess', 'expiresAt', 'hookDefinitionHash', 'issuedAt',
62
+ 'taskId', 'version'
63
+ ])
64
+ || context.version !== 2
65
+ || typeof context.taskId !== 'string' || context.taskId.length === 0
66
+ || typeof context.controlGeneration !== 'string' || context.controlGeneration.length === 0
67
+ || typeof context.controllerInstanceDigest !== 'string' || !HEX_256.test(context.controllerInstanceDigest)
68
+ || !validProcess(context.controllerProcess)
69
+ || !lease || !exactKeys(lease, ['leaseId', 'leaseSecret', 'version'])
70
+ || lease.version !== 1
71
+ || typeof lease.leaseId !== 'string' || !HEX_256.test(lease.leaseId)
72
+ || typeof lease.leaseSecret !== 'string' || !HEX_256.test(lease.leaseSecret)
73
+ || !Number.isSafeInteger(context.issuedAt)
74
+ || !Number.isSafeInteger(context.expiresAt)
75
+ || (context.expiresAt as number) <= (context.issuedAt as number)
76
+ || !validBuildIdentity(context.buildIdentity)
77
+ || typeof context.hookDefinitionHash !== 'string' || !HEX_256.test(context.hookDefinitionHash)) {
78
+ throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
79
+ }
80
+ return context as unknown as CodexSandboxControllerContextV2;
81
+ }
82
+
83
+ export function contextFromControllerLease(
84
+ lease: CodexControllerLeaseV1,
85
+ input: Readonly<{ hookDefinitionHash: string }>
86
+ ): CodexSandboxControllerContextV2 {
87
+ return Object.freeze({
88
+ version: 2,
89
+ taskId: lease.taskId,
90
+ controlGeneration: lease.controlGeneration,
91
+ controllerInstanceDigest: lease.controllerInstanceDigest,
92
+ controllerProcess: lease.controllerProcess,
93
+ controllerLease: Object.freeze({ version: 1, leaseId: lease.leaseId, leaseSecret: lease.leaseSecret }),
94
+ issuedAt: lease.issuedAt,
95
+ expiresAt: lease.expiresAt,
96
+ buildIdentity: lease.buildIdentity,
97
+ hookDefinitionHash: input.hookDefinitionHash
98
+ });
99
+ }
100
+
101
+ export function writeCodexSandboxControllerContext(
102
+ contextPath: string,
103
+ context: CodexSandboxControllerContextV2
104
+ ): void {
105
+ validateContext(context);
106
+ fs.mkdirSync(path.dirname(contextPath), { recursive: true, mode: 0o700 });
107
+ const temporary = path.join(path.dirname(contextPath), `.${path.basename(contextPath)}.${process.pid}.${crypto.randomUUID()}.tmp`);
108
+ try {
109
+ fs.writeFileSync(temporary, `${JSON.stringify(context, null, 2)}\n`, { mode: 0o600, flag: 'wx' });
110
+ fs.chmodSync(temporary, 0o600);
111
+ fs.renameSync(temporary, contextPath);
112
+ } finally {
113
+ fs.rmSync(temporary, { force: true });
114
+ }
115
+ }
116
+
117
+ export function verifyCodexSandboxControllerContextWithWarnings(
118
+ contextPath: string,
119
+ options: Readonly<{
120
+ repoRoot?: string;
121
+ now?: number;
122
+ generation?: string;
123
+ probeProcess?: (identity: ProcessIdentity) => 'alive' | 'dead' | 'unknown';
124
+ }> = {}
125
+ ): Readonly<{ context: CodexSandboxControllerContextV2; warnings: readonly LifecycleIdentityWarning[] }> {
126
+ let stat: fs.Stats;
127
+ try {
128
+ stat = fs.lstatSync(contextPath);
129
+ } catch {
130
+ throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
131
+ }
132
+ if (!stat.isFile() || stat.isSymbolicLink()
133
+ || (process.platform !== 'win32' && (stat.mode & 0o077) !== 0)) {
134
+ throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
135
+ }
136
+ let context: CodexSandboxControllerContextV2;
137
+ try {
138
+ context = validateContext(JSON.parse(fs.readFileSync(contextPath, 'utf8')));
139
+ } catch {
140
+ throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
141
+ }
142
+ const repoRoot = options.repoRoot ?? process.cwd();
143
+ const generation = options.generation ?? process.env.AGENT_INFRA_CONTROL_GENERATION;
144
+ const state = (options.probeProcess ?? getProcessIdentityState)(context.controllerProcess);
145
+ if (context.controlGeneration !== generation
146
+ || context.expiresAt <= (options.now ?? Date.now())
147
+ || state !== 'alive') {
148
+ throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
149
+ }
150
+ const currentBuildIdentity = computeLifecycleBuildIdentity(repoRoot);
151
+ const identity = verifyLifecycleBuildIdentity(context.buildIdentity, currentBuildIdentity);
152
+ if (!identity.ok) throw new Error(`${identity.code}: ${identity.message}`);
153
+ return Object.freeze({ context: Object.freeze(context), warnings: identity.warnings });
154
+ }
155
+
156
+ export function controllerProofFromContext(context: CodexSandboxControllerContextV2): CodexControllerLeaseProofV1 {
157
+ return Object.freeze({
158
+ version: 1,
159
+ leaseId: context.controllerLease.leaseId,
160
+ leaseSecret: context.controllerLease.leaseSecret,
161
+ controllerProcess: context.controllerProcess
162
+ });
163
+ }
@@ -147,6 +147,16 @@ function same(left: unknown, right: unknown): boolean {
147
147
  return JSON.stringify(left) === JSON.stringify(right);
148
148
  }
149
149
 
150
+ function sameChildCorrelation(
151
+ left: Extract<CodexLifecycleEvent, { type: 'hook-child' }>,
152
+ right: Extract<CodexLifecycleEvent, { type: 'hook-child' }>
153
+ ): boolean {
154
+ return left.sessionId === right.sessionId
155
+ && left.childThreadId === right.childThreadId
156
+ && left.parentThreadId === right.parentThreadId
157
+ && left.nativeAgent === right.nativeAgent;
158
+ }
159
+
150
160
  function requiredStrings(event: CodexLifecycleEvent): boolean {
151
161
  return Object.entries(event).every(([key, value]) =>
152
162
  key === 'forkedFromId'
@@ -286,6 +296,15 @@ function reduceCodexLifecycleEvent(
286
296
  const existing = state[key];
287
297
  if (existing) {
288
298
  if (same(existing, event)) return state;
299
+ if (
300
+ existing.type === 'hook-child'
301
+ && event.type === 'hook-child'
302
+ && sameChildCorrelation(existing, event)
303
+ && (existing.source === 'parent-rollout') !== (event.source === 'parent-rollout')
304
+ ) {
305
+ if (existing.source !== 'parent-rollout') return state;
306
+ return derive(Object.freeze({ ...state, child: Object.freeze({ ...event }) }));
307
+ }
289
308
  return invalid(state, 'CODEX_EVIDENCE_REPLAY_CONFLICT', `conflicting '${event.type}' event`);
290
309
  }
291
310
  return derive(Object.freeze({ ...state, [key]: Object.freeze({ ...event }) }));
@@ -3,6 +3,7 @@
3
3
  "bin/internal-cli.ts",
4
4
  "lib/internal/codex-lifecycle.ts",
5
5
  "lib/internal/codex-sandbox-controller.ts",
6
+ "lib/internal/sandbox-control.ts",
6
7
  "lib/internal/task-orchestration.ts",
7
8
  "lib/task/codex-orchestration.ts",
8
9
  "lib/task/delegation-receipts.ts",
@@ -10,10 +11,17 @@
10
11
  "lib/agent-clients/adapters/codex-lifecycle/build-identity.ts",
11
12
  "lib/agent-clients/adapters/codex-lifecycle/capability-store.ts",
12
13
  "lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts",
14
+ "lib/agent-clients/adapters/codex-lifecycle/controller-context.ts",
13
15
  "lib/agent-clients/adapters/codex-lifecycle/app-server.ts",
14
16
  "lib/agent-clients/adapters/codex-lifecycle/evidence.ts",
15
17
  "lib/agent-clients/adapters/codex-lifecycle/store.ts",
16
- "lib/agent-clients/adapters/codex-lifecycle/manifest-files.json"
18
+ "lib/agent-clients/adapters/codex-lifecycle/manifest-files.json",
19
+ "lib/sandbox/control/protocol.ts",
20
+ "lib/sandbox/control/client.ts",
21
+ "lib/sandbox/control/executor.ts",
22
+ "lib/sandbox/control/controller-registration.ts",
23
+ "lib/server/process-state.ts",
24
+ "lib/sandbox/shell.ts"
17
25
  ],
18
26
  "contractFiles": [
19
27
  ".codex/hooks.json",