@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
@@ -6,11 +6,22 @@ import path from 'node:path';
6
6
  import semver from 'semver';
7
7
  import * as toml from 'smol-toml';
8
8
 
9
- import { requestSandboxControl } from '../../../sandbox/control/client.ts';
9
+ import {
10
+ requestCodexControllerClose,
11
+ requestCodexControllerOpen,
12
+ requestCodexControllerVerify,
13
+ requestSandboxControl
14
+ } from '../../../sandbox/control/client.ts';
10
15
  import { readSandboxControlStatus } from '../../../sandbox/control/state.ts';
11
- import { getProcessStartTime, isProcessAlive } from '../../../server/process-state.ts';
12
- import { computeLifecycleBuildIdentity } from './build-identity.ts';
13
- import type { LifecycleBuildIdentity } from './build-identity.ts';
16
+ import { getProcessStartTime, type ProcessIdentity } from '../../../server/process-state.ts';
17
+ import { LIFECYCLE_PROTOCOL_VERSION, type LifecycleIdentityWarning } from './build-identity.ts';
18
+ import {
19
+ contextFromControllerLease,
20
+ controllerProofFromContext,
21
+ verifyCodexSandboxControllerContextWithWarnings as verifyContextFileWithWarnings,
22
+ writeCodexSandboxControllerContext,
23
+ type CodexSandboxControllerContextV2
24
+ } from './controller-context.ts';
14
25
 
15
26
  type ControllerControl = Readonly<{
16
27
  token: string;
@@ -20,23 +31,7 @@ type ControllerControl = Readonly<{
20
31
  runtimeDir: string;
21
32
  }>;
22
33
 
23
- type CodexSandboxControllerContext = Readonly<{
24
- version: 1;
25
- taskId: string;
26
- controlGeneration: string;
27
- controllerInstanceDigest: string;
28
- parentPid: number;
29
- parentStartTime: number;
30
- issuedAt: number;
31
- expiresAt: number;
32
- buildIdentity: LifecycleBuildIdentity;
33
- hookDefinitionHash: string;
34
- lifecycleProfilesHash: string;
35
- }>;
36
-
37
34
  type ControllerInput = Readonly<{
38
- taskId: string;
39
- taskRef: string;
40
35
  executorModel?: string;
41
36
  executorReasoningEffort?: string;
42
37
  reviewerModel?: string;
@@ -51,6 +46,8 @@ type ControllerOptions = Readonly<{
51
46
  now?: () => number;
52
47
  codexVersion?: () => string;
53
48
  verifyTaskBinding?: (taskId: string, control: ControllerControl) => void;
49
+ openController?: typeof requestCodexControllerOpen;
50
+ closeController?: typeof requestCodexControllerClose;
54
51
  environment?: NodeJS.ProcessEnv;
55
52
  }>;
56
53
 
@@ -60,26 +57,18 @@ type PreparedCodexSandboxController = Readonly<{
60
57
  env: NodeJS.ProcessEnv;
61
58
  home: string;
62
59
  contextPath: string;
63
- context: CodexSandboxControllerContext;
60
+ context: CodexSandboxControllerContextV2;
64
61
  cleanup: () => void;
65
62
  }>;
66
63
 
67
- const CONTEXT_TTL_MS = 4 * 60 * 60 * 1_000;
68
-
69
- function digestFiles(files: readonly string[]): string {
70
- const hash = crypto.createHash('sha256');
71
- for (const file of [...files].sort()) {
72
- const stat = fs.lstatSync(file);
73
- if (!stat.isFile() || stat.isSymbolicLink()) {
74
- throw new Error('CODEX_SANDBOX_CONTROLLER_BUNDLE_MISMATCH');
75
- }
76
- hash.update(path.basename(file));
77
- hash.update('\0');
78
- hash.update(fs.readFileSync(file));
79
- hash.update('\0');
80
- }
81
- return hash.digest('hex');
82
- }
64
+ type VerifyContextOptions = Readonly<{
65
+ repoRoot?: string;
66
+ now?: number;
67
+ generation?: string;
68
+ probeProcess?: (identity: ProcessIdentity) => 'alive' | 'dead' | 'unknown';
69
+ control?: ControllerControl;
70
+ requestControllerVerify?: typeof requestCodexControllerVerify;
71
+ }>;
83
72
 
84
73
  function copyRegular(source: string, destination: string, mode: number): void {
85
74
  const stat = fs.lstatSync(source);
@@ -162,6 +151,34 @@ function verifyControl(taskId: string, control: ControllerControl): void {
162
151
  if (payload.taskId !== taskId) throw new Error('CODEX_SANDBOX_CONTROLLER_TASK_BINDING_INVALID');
163
152
  }
164
153
 
154
+ function verifyCodexSandboxControllerContextWithWarnings(
155
+ contextPath: string,
156
+ options: VerifyContextOptions = {}
157
+ ): Readonly<{ context: CodexSandboxControllerContextV2; warnings: readonly LifecycleIdentityWarning[] }> {
158
+ const control = options.control ?? controlFromEnvironment();
159
+ const fileVerification = verifyContextFileWithWarnings(contextPath, {
160
+ repoRoot: options.repoRoot,
161
+ now: options.now,
162
+ generation: control.generation,
163
+ probeProcess: options.probeProcess
164
+ });
165
+ const context = fileVerification.context;
166
+ const verified = (options.requestControllerVerify ?? requestCodexControllerVerify)({
167
+ controllerProof: controllerProofFromContext(context),
168
+ ...control,
169
+ timeoutMs: 30_000
170
+ });
171
+ if (verified.binding.taskId !== context.taskId
172
+ || verified.binding.controlGeneration !== context.controlGeneration
173
+ || verified.binding.controllerInstanceDigest !== context.controllerInstanceDigest) {
174
+ throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
175
+ }
176
+ return Object.freeze({
177
+ context,
178
+ warnings: fileVerification.warnings
179
+ });
180
+ }
181
+
165
182
  function detectedCodexVersion(): string {
166
183
  const result = spawnSync('codex', ['--version'], { encoding: 'utf8' });
167
184
  const version = /codex-cli\s+(\d+\.\d+\.\d+)/u.exec(result.stdout ?? '')?.[1];
@@ -256,7 +273,6 @@ function prepareCodexSandboxController(
256
273
  const repoRoot = path.resolve(options.repoRoot ?? process.cwd());
257
274
  const sourceHome = path.resolve(options.codexHome ?? process.env.CODEX_HOME ?? path.join(os.homedir(), '.codex'));
258
275
  const control = options.control ?? controlFromEnvironment();
259
- (options.verifyTaskBinding ?? verifyControl)(input.taskId, control);
260
276
  const version = (options.codexVersion ?? detectedCodexVersion)();
261
277
  if (!semver.gte(version, '0.147.0')) throw new Error('CODEX_SANDBOX_CONTROLLER_CODEX_UNSUPPORTED');
262
278
 
@@ -265,46 +281,33 @@ function prepareCodexSandboxController(
265
281
  fs.mkdirSync(runtimeRoot, { recursive: true, mode: 0o700 });
266
282
  fs.chmodSync(runtimeRoot, 0o700);
267
283
  const key = crypto.createHash('sha256')
268
- .update(`${input.taskId}\0${control.generation}`)
284
+ .update(`${process.pid}\0${control.generation}`)
269
285
  .digest('hex');
270
- const leasePath = path.join(runtimeRoot, `${key}.lease.json`);
271
- if (fs.existsSync(leasePath)) {
272
- const lease = JSON.parse(fs.readFileSync(leasePath, 'utf8')) as { pid?: number; startTime?: number };
273
- if (Number.isSafeInteger(lease.pid) && typeof lease.startTime === 'number'
274
- && getProcessStartTime(lease.pid!) === lease.startTime) {
275
- throw new Error('CODEX_SANDBOX_CONTROLLER_BUSY');
276
- }
277
- fs.unlinkSync(leasePath);
278
- }
279
-
280
286
  const parentStartTime = getProcessStartTime(process.pid);
281
287
  if (!parentStartTime) throw new Error('CODEX_SANDBOX_CONTROLLER_PROCESS_IDENTITY_INVALID');
282
- const instanceDigest = crypto.randomBytes(32).toString('hex');
283
- fs.writeFileSync(leasePath, `${JSON.stringify({
284
- version: 1,
285
- pid: process.pid,
286
- startTime: parentStartTime,
287
- taskId: input.taskId,
288
- generation: control.generation,
289
- instanceDigest
290
- })}\n`, { mode: 0o600, flag: 'wx' });
291
-
292
288
  const home = fs.mkdtempSync(path.join(runtimeRoot, `${key}-`));
293
289
  fs.chmodSync(home, 0o700);
294
290
  let cleaned = false;
291
+ let context: CodexSandboxControllerContextV2 | null = null;
295
292
  const cleanup = () => {
296
293
  if (cleaned) return;
297
294
  cleaned = true;
295
+ if (context) {
296
+ try {
297
+ (options.closeController ?? requestCodexControllerClose)({
298
+ controllerProcess: context.controllerProcess,
299
+ controllerProof: controllerProofFromContext(context),
300
+ ...control,
301
+ timeoutMs: 30_000
302
+ });
303
+ } catch {
304
+ // The wrapper is exiting; dead-process recovery safely handles an unknown close result.
305
+ }
306
+ }
298
307
  const relative = path.relative(runtimeRoot, home);
299
308
  if (!relative.startsWith('..') && !path.isAbsolute(relative)) {
300
309
  fs.rmSync(home, { recursive: true, force: true });
301
310
  }
302
- try {
303
- const lease = JSON.parse(fs.readFileSync(leasePath, 'utf8')) as { instanceDigest?: string };
304
- if (lease.instanceDigest === instanceDigest) fs.unlinkSync(leasePath);
305
- } catch {
306
- // A missing or replaced lease is owned by another cleanup path.
307
- }
308
311
  };
309
312
 
310
313
  try {
@@ -323,32 +326,27 @@ function prepareCodexSandboxController(
323
326
  copyRegular(hooks, path.join(home, 'hooks.json'), 0o600);
324
327
  copyRegular(executor, path.join(home, 'agents', path.basename(executor)), 0o600);
325
328
  copyRegular(reviewer, path.join(home, 'agents', path.basename(reviewer)), 0o600);
326
- if (crypto.createHash('sha256').update(fs.readFileSync(path.join(home, 'hooks.json'))).digest('hex')
327
- !== crypto.createHash('sha256').update(fs.readFileSync(hooks)).digest('hex')
328
- || digestFiles([
329
- path.join(home, 'agents', path.basename(executor)),
330
- path.join(home, 'agents', path.basename(reviewer))
331
- ]) !== digestFiles([executor, reviewer])) {
332
- throw new Error('CODEX_SANDBOX_CONTROLLER_BUNDLE_MISMATCH');
333
- }
334
329
 
335
- const buildIdentity = computeLifecycleBuildIdentity(repoRoot);
336
- const now = (options.now ?? Date.now)();
337
- const context: CodexSandboxControllerContext = Object.freeze({
338
- version: 1,
339
- taskId: input.taskId,
340
- controlGeneration: control.generation,
341
- controllerInstanceDigest: instanceDigest,
342
- parentPid: process.pid,
343
- parentStartTime,
344
- issuedAt: now,
345
- expiresAt: now + CONTEXT_TTL_MS,
346
- buildIdentity,
347
- hookDefinitionHash: crypto.createHash('sha256').update(fs.readFileSync(hooks)).digest('hex'),
348
- lifecycleProfilesHash: digestFiles([executor, reviewer])
330
+ const opened = (options.openController ?? requestCodexControllerOpen)({
331
+ controllerProcess: { pid: process.pid, startTime: parentStartTime },
332
+ ...control,
333
+ timeoutMs: 30_000
334
+ });
335
+ const taskId = opened.lease.taskId;
336
+ if (opened.lease.buildIdentity.protocolVersion !== LIFECYCLE_PROTOCOL_VERSION) {
337
+ throw new Error('CODEX_LIFECYCLE_PROTOCOL_MISMATCH: Codex lifecycle protocol version does not match');
338
+ }
339
+ context = contextFromControllerLease(opened.lease, {
340
+ hookDefinitionHash: crypto.createHash('sha256').update(fs.readFileSync(hooks)).digest('hex')
349
341
  });
342
+ if (opened.lease.controlGeneration !== control.generation
343
+ || opened.lease.controllerProcess.pid !== process.pid
344
+ || opened.lease.controllerProcess.startTime !== parentStartTime) {
345
+ throw new Error('SANDBOX_CONTROL_RESULT_INVALID');
346
+ }
347
+ (options.verifyTaskBinding ?? verifyControl)(taskId, control);
350
348
  const contextPath = path.join(home, 'controller-context.json');
351
- fs.writeFileSync(contextPath, `${JSON.stringify(context, null, 2)}\n`, { mode: 0o600 });
349
+ writeCodexSandboxControllerContext(contextPath, context);
352
350
 
353
351
  const shimDir = path.join(home, 'bin');
354
352
  fs.mkdirSync(shimDir, { mode: 0o700 });
@@ -364,7 +362,7 @@ function prepareCodexSandboxController(
364
362
  input.reviewerModel ? `--reviewer-model ${input.reviewerModel}` : '',
365
363
  input.reviewerReasoningEffort ? `--reviewer-reasoning-effort ${input.reviewerReasoningEffort}` : ''
366
364
  ].filter(Boolean).join(' ');
367
- const prompt = `$run-task ${input.taskRef}${policy ? ` ${policy}` : ''}`;
365
+ const prompt = `$run-task ${taskId}${policy ? ` ${policy}` : ''}`;
368
366
  const args = Object.freeze([
369
367
  'exec',
370
368
  '--enable', 'hooks',
@@ -379,7 +377,7 @@ function prepareCodexSandboxController(
379
377
  home,
380
378
  shimDir,
381
379
  contextPath,
382
- input.taskId,
380
+ taskId,
383
381
  control,
384
382
  providerEnvironment,
385
383
  options.environment ?? process.env
@@ -399,32 +397,6 @@ function prepareCodexSandboxController(
399
397
  }
400
398
  }
401
399
 
402
- function verifyCodexSandboxControllerContext(
403
- contextPath: string,
404
- taskId: string,
405
- options: Readonly<{ repoRoot?: string; now?: number; generation?: string }> = {}
406
- ): CodexSandboxControllerContext {
407
- const stat = fs.lstatSync(contextPath);
408
- if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 0o077) !== 0) {
409
- throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
410
- }
411
- const context = JSON.parse(fs.readFileSync(contextPath, 'utf8')) as CodexSandboxControllerContext;
412
- const generation = options.generation ?? process.env.AGENT_INFRA_CONTROL_GENERATION;
413
- if (context.version !== 1
414
- || context.taskId !== taskId
415
- || context.controlGeneration !== generation
416
- || context.expiresAt < (options.now ?? Date.now())
417
- || getProcessStartTime(context.parentPid) !== context.parentStartTime
418
- || !isProcessAlive(context.parentPid)) {
419
- throw new Error('CODEX_SANDBOX_CONTROLLER_CONTEXT_INVALID');
420
- }
421
- const current = computeLifecycleBuildIdentity(options.repoRoot ?? process.cwd());
422
- if (JSON.stringify(current) !== JSON.stringify(context.buildIdentity)) {
423
- throw new Error('CODEX_SANDBOX_CONTROLLER_BUNDLE_MISMATCH');
424
- }
425
- return Object.freeze(context);
426
- }
427
-
428
400
  async function runCodexSandboxController(
429
401
  input: ControllerInput,
430
402
  options: ControllerOptions = {}
@@ -469,10 +441,11 @@ async function runCodexSandboxController(
469
441
  export {
470
442
  prepareCodexSandboxController,
471
443
  runCodexSandboxController,
472
- verifyCodexSandboxControllerContext
444
+ verifyCodexSandboxControllerContextWithWarnings,
445
+ controllerProofFromContext
473
446
  };
474
447
  export type {
475
- CodexSandboxControllerContext,
448
+ CodexSandboxControllerContextV2 as CodexSandboxControllerContext,
476
449
  ControllerControl,
477
450
  ControllerInput,
478
451
  ControllerOptions,
@@ -37,6 +37,8 @@ function resolveHostCatalogPath(value: unknown, hostHomeDir: string): string | n
37
37
  }
38
38
 
39
39
  const CODEX_DISABLED_FEATURE_FLAGS = ['apps', 'enable_mcp_apps'] as const;
40
+ const SANDBOX_CODEX_HOOKS_PATH = '/workspace/.codex/hooks.json';
41
+ const CODEX_HOOK_TRUST_HASH = /^sha256:[a-f0-9]{64}$/;
40
42
 
41
43
  function removeCodexMcpServers(sandboxParsed: JsonObject): boolean {
42
44
  if (!Object.hasOwn(sandboxParsed, 'mcp_servers')) {
@@ -75,27 +77,75 @@ function inheritDisabledCodexFeatureFlags(
75
77
  return changed;
76
78
  }
77
79
 
80
+ function inheritCodexHookTrustState(
81
+ sandboxParsed: JsonObject,
82
+ hostParsed: JsonObject,
83
+ hostProjectDir?: string
84
+ ): boolean {
85
+ if (!hostProjectDir) {
86
+ return false;
87
+ }
88
+
89
+ if (Object.hasOwn(sandboxParsed, 'hooks') && !isJsonObjectRecord(sandboxParsed.hooks)) {
90
+ return false;
91
+ }
92
+ const sandboxHooks = (sandboxParsed.hooks as JsonObject | undefined) ?? {};
93
+ if (Object.hasOwn(sandboxHooks, 'state') && !isJsonObjectRecord(sandboxHooks.state)) {
94
+ return false;
95
+ }
96
+
97
+ const sandboxState = (sandboxHooks.state as JsonObject | undefined) ?? {};
98
+ const sandboxPrefix = `${SANDBOX_CODEX_HOOKS_PATH}:`;
99
+ const nextState = Object.fromEntries(
100
+ Object.entries(sandboxState).filter(([key]) => !key.startsWith(sandboxPrefix))
101
+ );
102
+
103
+ const hostState = isJsonObjectRecord(hostParsed.hooks)
104
+ && isJsonObjectRecord(hostParsed.hooks.state)
105
+ ? hostParsed.hooks.state
106
+ : {};
107
+ const hostPrefix = `${path.join(hostProjectDir, '.codex', 'hooks.json')}:`;
108
+ for (const [key, value] of Object.entries(hostState)) {
109
+ if (!key.startsWith(hostPrefix) || !isJsonObjectRecord(value)) {
110
+ continue;
111
+ }
112
+ const trustedHash = value.trusted_hash;
113
+ if (typeof trustedHash !== 'string' || !CODEX_HOOK_TRUST_HASH.test(trustedHash)) {
114
+ continue;
115
+ }
116
+ nextState[`${sandboxPrefix}${key.slice(hostPrefix.length)}`] = {
117
+ trusted_hash: trustedHash,
118
+ ...(typeof value.enabled === 'boolean' ? { enabled: value.enabled } : {})
119
+ };
120
+ }
121
+
122
+ if (JSON.stringify(sandboxState) === JSON.stringify(nextState)) {
123
+ return false;
124
+ }
125
+ if (Object.keys(nextState).length > 0) {
126
+ sandboxHooks.state = nextState;
127
+ sandboxParsed.hooks = sandboxHooks;
128
+ } else {
129
+ delete sandboxHooks.state;
130
+ if (Object.keys(sandboxHooks).length > 0) {
131
+ sandboxParsed.hooks = sandboxHooks;
132
+ } else {
133
+ delete sandboxParsed.hooks;
134
+ }
135
+ }
136
+ return true;
137
+ }
138
+
78
139
  function ensureCodexModelInheritance(
79
140
  toolDir: string,
80
141
  hostHomeDir?: string,
81
- containerCodexDir: string = '/home/devuser/.codex'
142
+ containerCodexDir: string = '/home/devuser/.codex',
143
+ hostProjectDir?: string
82
144
  ): void {
83
145
  if (!hostHomeDir) {
84
146
  return;
85
147
  }
86
148
 
87
- const hostConfigPath = path.join(hostHomeDir, '.codex', 'config.toml');
88
- if (!fs.existsSync(hostConfigPath)) {
89
- return;
90
- }
91
-
92
- let hostParsed: JsonObject;
93
- try {
94
- hostParsed = toml.parse(fs.readFileSync(hostConfigPath, 'utf8')) as JsonObject;
95
- } catch {
96
- return;
97
- }
98
-
99
149
  const sandboxConfigPath = path.join(toolDir, 'config.toml');
100
150
  // This rewrites sandbox-side TOML and drops comments; the host config stays untouched.
101
151
  let sandboxParsed: JsonObject = {};
@@ -107,8 +157,25 @@ function ensureCodexModelInheritance(
107
157
  }
108
158
  }
109
159
 
160
+ const hostConfigPath = path.join(hostHomeDir, '.codex', 'config.toml');
161
+ let hostParsed: JsonObject | null = null;
162
+ if (fs.existsSync(hostConfigPath)) {
163
+ try {
164
+ hostParsed = toml.parse(fs.readFileSync(hostConfigPath, 'utf8')) as JsonObject;
165
+ } catch {
166
+ // An unavailable host trust source must not leave previously projected trust active.
167
+ }
168
+ }
169
+ if (!hostParsed) {
170
+ if (inheritCodexHookTrustState(sandboxParsed, {}, hostProjectDir)) {
171
+ fs.writeFileSync(sandboxConfigPath, `${toml.stringify(sandboxParsed)}\n`, 'utf8');
172
+ }
173
+ return;
174
+ }
175
+
110
176
  let changed = removeCodexMcpServers(sandboxParsed);
111
177
  changed = inheritDisabledCodexFeatureFlags(sandboxParsed, hostParsed) || changed;
178
+ changed = inheritCodexHookTrustState(sandboxParsed, hostParsed, hostProjectDir) || changed;
112
179
 
113
180
  const inheritSpecs: Array<readonly [string, 'string' | 'number']> = [
114
181
  ['model', 'string'],
@@ -180,7 +247,15 @@ const codexBeforeContainerCreateHook: AgentClientSandboxHook = {
180
247
  message: 'Codex sandbox hook requires its resolved tool state.'
181
248
  };
182
249
  }
183
- ensureCodexModelInheritance(entry.dir, create.hostHome, entry.tool.containerMount);
250
+ const hostProjectDir = typeof context.config?.repoRoot === 'string'
251
+ ? context.config.repoRoot
252
+ : undefined;
253
+ ensureCodexModelInheritance(
254
+ entry.dir,
255
+ create.hostHome,
256
+ entry.tool.containerMount,
257
+ hostProjectDir
258
+ );
184
259
  ensureCodexWorkspaceTrust(entry.dir);
185
260
  return { status: 'ready' };
186
261
  }
@@ -4,13 +4,15 @@ import { claudeCodeAdapter } from './claude-code.ts';
4
4
  import { codexAdapter } from './codex.ts';
5
5
  import { antigravityCliAdapter } from './antigravity-cli.ts';
6
6
  import { opencodeAdapter } from './opencode.ts';
7
+ import { traecliAdapter } from './traecli.ts';
7
8
 
8
9
  const adaptersById = new Map(
9
10
  [
10
11
  claudeCodeAdapter,
11
12
  codexAdapter,
12
13
  antigravityCliAdapter,
13
- opencodeAdapter
14
+ opencodeAdapter,
15
+ traecliAdapter
14
16
  ].map((adapter) => [adapter.id, adapter])
15
17
  );
16
18
 
@@ -0,0 +1,67 @@
1
+ import { defineAgentClientAdapter } from '../adapter.ts';
2
+ import { hostJoin } from '../../sandbox/engines/wsl2-paths.ts';
3
+
4
+ const traecliAdapter = defineAgentClientAdapter({
5
+ id: 'traecli',
6
+ displayName: 'TraeCode CLI',
7
+ invocation: '/${skillName}',
8
+ capabilities: {
9
+ instructions: { level: 'compatible' },
10
+ skills: { level: 'compatible' },
11
+ commands: { level: 'integrated' },
12
+ hooks: { level: 'compatible' },
13
+ subagents: { level: 'experimental' },
14
+ orchestration: { level: 'experimental' },
15
+ sandbox: { level: 'integrated' },
16
+ verification: { level: 'compatible' }
17
+ },
18
+ modelSelection: {
19
+ kind: 'interactive-only',
20
+ command: '/model',
21
+ guidance: 'Use the host model picker for the complete model and reasoning-effort catalog.'
22
+ },
23
+ delegationEvidence: {
24
+ actualModel: 'unavailable',
25
+ actualReasoningEffort: 'unavailable'
26
+ },
27
+ project: {
28
+ ownedPathPrefixes: ['.trae/'],
29
+ managed: ['.trae/skills/'],
30
+ merged: [],
31
+ ejected: [],
32
+ seedCommands: [{
33
+ templates: {
34
+ en: '.trae/skills/update-agent-infra.en.md',
35
+ 'zh-CN': '.trae/skills/update-agent-infra.zh-CN.md'
36
+ },
37
+ target: '.trae/skills/update-agent-infra.md'
38
+ }],
39
+ customCommand: {
40
+ target: '.trae/skills/${skillName}.md',
41
+ frontmatter: {},
42
+ includeUsage: true,
43
+ inheritDisableModelInvocation: true
44
+ }
45
+ },
46
+ sandbox: {
47
+ createTool: ({ home }) => ({
48
+ id: 'traecli',
49
+ name: 'TraeCode CLI',
50
+ install: {
51
+ type: 'shell',
52
+ cmd: 'curl -fsSL https://trae.cn/trae-cli/install_v2.sh | bash'
53
+ },
54
+ sandboxBase: hostJoin(home, '.agent-infra', 'sandboxes', 'traecli'),
55
+ containerMount: '/home/devuser/.trae',
56
+ versionCmd: 'traecli --version',
57
+ setupHint: 'Run traecli login inside the container to authenticate.'
58
+ }),
59
+ aliases: [
60
+ { name: 'traecli-yolo', command: 'traecli --dangerously-bypass-approvals-and-sandbox; tput ed' },
61
+ { name: 'ty', command: 'traecli --dangerously-bypass-approvals-and-sandbox; tput ed' }
62
+ ],
63
+ hooks: []
64
+ }
65
+ });
66
+
67
+ export { traecliAdapter };
@@ -3,7 +3,7 @@
3
3
  // internal commands and the rendering side share one definition. Historical
4
4
  // Gemini tokens remain read-only and are handled by classifyAgent below.
5
5
  export const KNOWN_AI_AGENTS: ReadonlySet<string> =
6
- new Set(['claude', 'codex', 'antigravity', 'opencode', 'cursor']);
6
+ new Set(['claude', 'codex', 'antigravity', 'opencode', 'cursor', 'traecli']);
7
7
 
8
8
  // Long name -> short name mapping (HD-4, decision A): `claude-code` reuses the
9
9
  // existing orchestration precedent, and `antigravity-cli` is the active
@@ -14,7 +14,7 @@ export const AGENT_LONG_NAMES: Readonly<Record<string, string>> = {
14
14
  };
15
15
 
16
16
  export const AGENT_USAGE_HINT =
17
- "agent must be a short token (claude/codex/antigravity/opencode/cursor), " +
17
+ "agent must be a short token (claude/codex/antigravity/opencode/cursor/traecli), " +
18
18
  "a long name (claude-code -> claude, antigravity-cli -> antigravity), or 'human'";
19
19
 
20
20
  // Read-side historical compatibility for activity-log rendering: `gemini` and
@@ -2,7 +2,8 @@ const AGENT_CLIENT_IDS = [
2
2
  'claude-code',
3
3
  'codex',
4
4
  'antigravity-cli',
5
- 'opencode'
5
+ 'opencode',
6
+ 'traecli'
6
7
  ] as const;
7
8
 
8
9
  type AgentClientId = (typeof AGENT_CLIENT_IDS)[number];
package/lib/defaults.json CHANGED
@@ -22,6 +22,11 @@
22
22
  "id": "opencode",
23
23
  "enabled": true,
24
24
  "installInSandbox": true
25
+ },
26
+ {
27
+ "id": "traecli",
28
+ "enabled": true,
29
+ "installInSandbox": true
25
30
  }
26
31
  ],
27
32
  "sandbox": {
@@ -105,7 +105,7 @@ function commitExplicitPaths(input: { cwd: string; paths: readonly string[]; mes
105
105
  return { status: 'applied' as const, changed: true, snapshot: after.snapshot, operations: [{ name: 'stage', status: 'applied' as const }, { name: 'commit', status: 'applied' as const }], error: null };
106
106
  }
107
107
 
108
- function pushGitRefs(input: { cwd: string; remote: string; refs: readonly string[] }, runner: GitRunner = defaultRunner) {
108
+ function pushGitRefs(input: { cwd: string; remote: string; refs: readonly string[]; expectedSha?: string }, runner: GitRunner = defaultRunner) {
109
109
  if (!input.remote || input.refs.length === 0) return { status: 'failed' as const, changed: false, snapshot: inspectGitWorkflow(input.cwd, runner).snapshot, operations: [], error: { code: 'GIT_PUSH_INPUT_INVALID', message: 'Remote and refs are required' } };
110
110
  const operations: GitOperation[] = [];
111
111
  for (const ref of input.refs) {
@@ -120,7 +120,16 @@ function pushGitRefs(input: { cwd: string; remote: string; refs: readonly string
120
120
  }
121
121
  const canonical = ref.startsWith('refs/') ? ref : `refs/heads/${ref}`;
122
122
  const remoteFact = run(runner, input.cwd, ['ls-remote', '--exit-code', input.remote, canonical]);
123
- operations.push({ name: 'push', ref, status: remoteFact.status === 0 ? 'applied' : 'failed', message: remoteFact.status === 0 ? undefined : 'Remote ref verification failed' });
123
+ const remoteSha = remoteFact.status === 0 ? remoteFact.stdout.trim().split(/\s+/)[0] ?? null : null;
124
+ const verified = remoteFact.status === 0 && (!input.expectedSha || remoteSha === input.expectedSha);
125
+ operations.push({
126
+ name: 'push',
127
+ ref,
128
+ status: verified ? 'applied' : 'failed',
129
+ message: verified ? undefined : input.expectedSha
130
+ ? `Remote ref verification failed: expected ${input.expectedSha}, received ${remoteSha ?? 'unavailable'}`
131
+ : 'Remote ref verification failed'
132
+ });
124
133
  }
125
134
  const failures = operations.filter((item) => item.status === 'failed').length;
126
135
  return { status: failures === 0 ? 'applied' as const : failures === operations.length ? 'failed' as const : 'degraded' as const, changed: failures < operations.length, snapshot: inspectGitWorkflow(input.cwd, runner).snapshot, operations, error: failures ? { code: 'GIT_PUSH_PARTIAL', message: `${failures} ref push operation(s) failed` } : null };
@@ -11,9 +11,10 @@ import {
11
11
  import { createCodexLifecycleStore } from '../agent-clients/adapters/codex-lifecycle/store.ts';
12
12
  import { createCodexCapabilityStore } from '../agent-clients/adapters/codex-lifecycle/capability-store.ts';
13
13
  import { computeLifecycleBuildIdentity } from '../agent-clients/adapters/codex-lifecycle/build-identity.ts';
14
- import { verifyCodexSandboxControllerContext } from '../agent-clients/adapters/codex-lifecycle/sandbox-controller.ts';
14
+ import { verifyCodexSandboxControllerContextWithWarnings } from '../agent-clients/adapters/codex-lifecycle/sandbox-controller.ts';
15
15
  import type { CodexLifecycleEvent } from '../agent-clients/adapters/codex-lifecycle/evidence.ts';
16
16
  import { resolveTaskRef } from '../task/resolve-ref.ts';
17
+ import { hasSealableOrchestrationDelegation } from '../task/orchestration.ts';
17
18
  import {
18
19
  activateCodexOrchestrationDelegation,
19
20
  activateCodexSpawnDelegation,
@@ -100,7 +101,7 @@ function hookDefinitionHash(): string {
100
101
  function controllerBinding(taskId: string) {
101
102
  const contextPath = process.env.AGENT_INFRA_CODEX_CONTROLLER_CONTEXT;
102
103
  if (!contextPath) return undefined;
103
- const context = verifyCodexSandboxControllerContext(contextPath, taskId, { repoRoot: process.cwd() });
104
+ const context = verifyCodexSandboxControllerContextWithWarnings(contextPath, { repoRoot: process.cwd() }).context;
104
105
  return {
105
106
  instanceDigest: context.controllerInstanceDigest,
106
107
  controlGeneration: context.controlGeneration
@@ -139,7 +140,8 @@ function recordFromPayload(phase: string, payload: unknown): CodexLifecycleEvent
139
140
  sessionId: text('sessionId'),
140
141
  turnId: text('turnId'),
141
142
  childThreadId: text('childThreadId'),
142
- nativeAgent: text('nativeAgent')
143
+ nativeAgent: text('nativeAgent'),
144
+ source: 'hook'
143
145
  };
144
146
  if (phase === 'subagent-stop') return {
145
147
  type: 'hook-stop',
@@ -326,8 +328,17 @@ async function codexLifecycle(args: string[] = []): Promise<void> {
326
328
  }
327
329
  const result = store.apply(event);
328
330
  if (parsed.values['--bridge'] === 'true' && event.type === 'hook-stop') {
329
- const bridged = await sealCodexOrchestrationDelegation(event.childThreadId, { store });
330
- outputBridgeResult(bridged);
331
+ if (!hasSealableOrchestrationDelegation('codex', event.childThreadId, { repoRoot: process.cwd() })) {
332
+ output({ status: 'ignored', changed: false, evidence: null, diagnostics: [], error: null });
333
+ return;
334
+ }
335
+ output({
336
+ status: result.state.status,
337
+ changed: true,
338
+ evidence: result.state,
339
+ diagnostics: [],
340
+ error: result.state.error
341
+ });
331
342
  return;
332
343
  }
333
344
  output({ status: result.state.status, changed: true, evidence: result.state, diagnostics: [], error: result.state.error });
@@ -350,7 +361,9 @@ async function codexLifecycle(args: string[] = []): Promise<void> {
350
361
  return;
351
362
  }
352
363
  if (parsed.operation === 'resolve-stop') {
353
- const terminal = await resolveCodexTerminal(childThreadId);
364
+ const stopTurnId = store.read(childThreadId).state.stop?.turnId;
365
+ if (!stopTurnId) throw new Error('Codex lifecycle stop hook is not available');
366
+ const terminal = await resolveCodexTerminal(childThreadId, stopTurnId);
354
367
  const latest = store.apply(terminal);
355
368
  if (latest.state.status !== 'stop-ready') {
356
369
  throw new Error(`Codex lifecycle stop evidence is not ready (status=${latest.state.status})`);