@fitlab-ai/agent-infra 0.9.14 → 0.9.15

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 (250) hide show
  1. package/README.md +10 -0
  2. package/README.zh-CN.md +10 -0
  3. package/bin/internal-cli.sh +45 -0
  4. package/dist/bin/internal-cli.js +93 -30
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +165 -27
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +3 -2
  7. package/dist/lib/defaults.json +103 -103
  8. package/dist/lib/fs/durable-write.js +37 -0
  9. package/dist/lib/fs/file-lock.js +111 -0
  10. package/dist/lib/host-control/audit.js +19 -0
  11. package/dist/lib/host-control/client.js +142 -0
  12. package/dist/lib/host-control/command.js +49 -0
  13. package/dist/lib/host-control/path.js +160 -0
  14. package/dist/lib/host-control/secure-fs.js +140 -0
  15. package/dist/lib/host-control/server.js +159 -0
  16. package/dist/lib/host-control/service.js +112 -0
  17. package/dist/lib/init.js +11 -2
  18. package/dist/lib/internal/cli-route-inventory.js +3 -1
  19. package/dist/lib/internal/codex-lifecycle.js +6 -6
  20. package/dist/lib/internal/demo-promotion.js +322 -0
  21. package/dist/lib/internal/demo-transcript.js +454 -0
  22. package/dist/lib/internal/host-control.js +28 -0
  23. package/dist/lib/internal/manual-validation.js +322 -0
  24. package/dist/lib/internal/platform-pr.js +18 -4
  25. package/dist/lib/internal/release-workflow.js +63 -84
  26. package/dist/lib/internal/sandbox-control.js +149 -2
  27. package/dist/lib/internal/task-artifact.js +15 -262
  28. package/dist/lib/internal/task-create.js +6 -5
  29. package/dist/lib/internal/task-event.js +37 -37
  30. package/dist/lib/internal/task-operation-registry.js +101 -3
  31. package/dist/lib/internal/task-review.js +8 -53
  32. package/dist/lib/internal/task-validate.js +30 -10
  33. package/dist/lib/owned-path.js +33 -0
  34. package/dist/lib/platform/check-status.js +11 -0
  35. package/dist/lib/platform/comment-safety.js +4 -1
  36. package/dist/lib/platform/completion-backfill.js +1 -1
  37. package/dist/lib/platform/github-data.js +241 -0
  38. package/dist/lib/platform/github-provider.js +15 -20
  39. package/dist/lib/platform/issue-comments.js +2 -2
  40. package/dist/lib/platform/issues.js +1 -179
  41. package/dist/lib/platform/pr-checks.js +12 -65
  42. package/dist/lib/platform/pr-summary.js +55 -3
  43. package/dist/lib/platform/pull-requests.js +2 -105
  44. package/dist/lib/platform/snapshots.js +2 -0
  45. package/dist/lib/platform/verification-sync.js +127 -142
  46. package/dist/lib/release/workflow.js +1 -1
  47. package/dist/lib/sandbox/commands/create.js +7 -2
  48. package/dist/lib/sandbox/commands/rm.js +3 -1514
  49. package/dist/lib/sandbox/control/audit.js +203 -0
  50. package/dist/lib/sandbox/control/client.js +93 -54
  51. package/dist/lib/sandbox/control/executor.js +129 -65
  52. package/dist/lib/sandbox/control/finalization-response.js +27 -0
  53. package/dist/lib/sandbox/control/identity-sentinel.js +107 -0
  54. package/dist/lib/sandbox/control/lifecycle.js +36 -2
  55. package/dist/lib/sandbox/control/native-file-lock.js +6 -98
  56. package/dist/lib/sandbox/control/protocol.js +55 -4
  57. package/dist/lib/sandbox/control/server.js +491 -120
  58. package/dist/lib/sandbox/control/state.js +58 -0
  59. package/dist/lib/sandbox/control/task-workflow.js +74 -0
  60. package/dist/lib/sandbox/control/workflow-executor.js +99 -0
  61. package/dist/lib/sandbox/index.js +2 -2
  62. package/dist/lib/sandbox/recovery.js +28 -4
  63. package/dist/lib/sandbox/removal.js +1717 -0
  64. package/dist/lib/sandbox/task-cleanup.js +217 -0
  65. package/dist/lib/sandbox/workspace-view.js +13 -14
  66. package/dist/lib/task/activity-intent.js +2 -1
  67. package/dist/lib/task/activity-log.js +30 -24
  68. package/dist/lib/task/artifact-command.js +101 -0
  69. package/dist/lib/task/artifact-lifecycle.js +30 -28
  70. package/dist/lib/task/artifact-name.js +36 -0
  71. package/dist/lib/task/artifact-operations.js +23 -69
  72. package/dist/lib/task/artifact-receipts.js +3 -15
  73. package/dist/lib/task/artifact-repair-intent.js +87 -16
  74. package/dist/lib/task/artifact-schema.js +6 -14
  75. package/dist/lib/task/capabilities.js +12 -19
  76. package/dist/lib/task/codex-orchestration.js +71 -25
  77. package/dist/lib/task/commit-identity.js +28 -0
  78. package/dist/lib/task/commit-operation.js +2 -16
  79. package/dist/lib/task/control-authority.js +371 -5
  80. package/dist/lib/task/control-recovery.js +128 -0
  81. package/dist/lib/task/create.js +62 -9
  82. package/dist/lib/task/decision-details.js +2 -46
  83. package/dist/lib/task/events.js +120 -9
  84. package/dist/lib/task/finalization-state.js +9 -0
  85. package/dist/lib/task/human-override.js +3 -6
  86. package/dist/lib/task/implementation-inputs.js +3 -3
  87. package/dist/lib/task/intermediate-cleanup.js +383 -0
  88. package/dist/lib/task/invalidation.js +18 -65
  89. package/dist/lib/task/ledger-intents.js +7 -22
  90. package/dist/lib/task/ledger.js +1 -3
  91. package/dist/lib/task/lifecycle.js +19 -1
  92. package/dist/lib/task/local-artifact-finalization.js +132 -157
  93. package/dist/lib/task/manual-validation-completion.js +26 -0
  94. package/dist/lib/task/manual-validation-evidence.js +130 -0
  95. package/dist/lib/task/manual-validation-receipt.js +101 -0
  96. package/dist/lib/task/manual-validation-shared.js +40 -0
  97. package/dist/lib/task/manual-validation-transaction.js +243 -0
  98. package/dist/lib/task/markdown.js +47 -0
  99. package/dist/lib/task/orchestration.js +14 -17
  100. package/dist/lib/task/qualification-audit.js +20 -34
  101. package/dist/lib/task/review-artifacts.js +2 -18
  102. package/dist/lib/task/review-command.js +25 -0
  103. package/dist/lib/task/review-finalization.js +125 -193
  104. package/dist/lib/task/review-fingerprint.js +2 -2
  105. package/dist/lib/task/sections.js +34 -56
  106. package/dist/lib/task/verification-engine.js +116 -323
  107. package/dist/lib/task/verification.js +1 -1
  108. package/dist/lib/task/workflow-command.js +17 -0
  109. package/dist/lib/task/workflow-warning-intents.js +0 -16
  110. package/dist/lib/task/workflow-warnings.js +28 -2
  111. package/dist/lib/update.js +2 -0
  112. package/dist/lifecycle-build-manifest.json +3 -3
  113. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +223 -33
  114. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +3 -2
  115. package/lib/fs/durable-write.ts +29 -0
  116. package/lib/fs/file-lock.ts +113 -0
  117. package/lib/host-control/audit.ts +22 -0
  118. package/lib/host-control/client.ts +172 -0
  119. package/lib/host-control/command.ts +60 -0
  120. package/lib/host-control/path.ts +167 -0
  121. package/lib/host-control/secure-fs.ts +135 -0
  122. package/lib/host-control/server.ts +180 -0
  123. package/lib/host-control/service.ts +133 -0
  124. package/lib/init.ts +15 -2
  125. package/lib/internal/cli-route-inventory.ts +3 -1
  126. package/lib/internal/codex-lifecycle.ts +6 -6
  127. package/lib/internal/demo-promotion.ts +354 -0
  128. package/lib/internal/demo-transcript.ts +419 -0
  129. package/lib/internal/host-control.ts +28 -0
  130. package/lib/internal/manual-validation.ts +306 -0
  131. package/lib/internal/platform-pr.ts +15 -4
  132. package/lib/internal/release-workflow.ts +74 -82
  133. package/lib/internal/sandbox-control.ts +162 -1
  134. package/lib/internal/task-artifact.ts +13 -216
  135. package/lib/internal/task-create.ts +5 -5
  136. package/lib/internal/task-event.ts +20 -11
  137. package/lib/internal/task-operation-registry.ts +111 -3
  138. package/lib/internal/task-review.ts +6 -51
  139. package/lib/internal/task-validate.ts +26 -10
  140. package/lib/owned-path.ts +26 -0
  141. package/lib/platform/check-status.ts +9 -0
  142. package/lib/platform/comment-safety.ts +5 -0
  143. package/lib/platform/completion-backfill.ts +1 -1
  144. package/lib/platform/github-data.ts +322 -0
  145. package/lib/platform/github-provider.ts +15 -20
  146. package/lib/platform/issue-comments.ts +3 -4
  147. package/lib/platform/issues.ts +4 -245
  148. package/lib/platform/pr-checks.ts +13 -77
  149. package/lib/platform/pr-summary.ts +61 -5
  150. package/lib/platform/pull-requests.ts +3 -170
  151. package/lib/platform/snapshots.ts +53 -0
  152. package/lib/platform/verification-sync.ts +145 -155
  153. package/lib/release/workflow.ts +3 -3
  154. package/lib/sandbox/commands/create.ts +7 -6
  155. package/lib/sandbox/commands/rm.ts +3 -1909
  156. package/lib/sandbox/control/audit.ts +253 -0
  157. package/lib/sandbox/control/client.ts +109 -66
  158. package/lib/sandbox/control/executor.ts +134 -74
  159. package/lib/sandbox/control/finalization-response.ts +29 -0
  160. package/lib/sandbox/control/identity-sentinel.ts +137 -0
  161. package/lib/sandbox/control/lifecycle.ts +45 -3
  162. package/lib/sandbox/control/native-file-lock.ts +6 -107
  163. package/lib/sandbox/control/protocol.ts +75 -6
  164. package/lib/sandbox/control/server.ts +538 -123
  165. package/lib/sandbox/control/state.ts +78 -0
  166. package/lib/sandbox/control/task-workflow.ts +95 -0
  167. package/lib/sandbox/control/workflow-executor.ts +102 -0
  168. package/lib/sandbox/index.ts +2 -2
  169. package/lib/sandbox/recovery.ts +28 -5
  170. package/lib/sandbox/removal.ts +2144 -0
  171. package/lib/sandbox/task-cleanup.ts +209 -0
  172. package/lib/sandbox/workspace-view.ts +17 -15
  173. package/lib/task/activity-intent.ts +2 -2
  174. package/lib/task/activity-log.ts +27 -19
  175. package/lib/task/artifact-command.ts +104 -0
  176. package/lib/task/artifact-lifecycle.ts +31 -26
  177. package/lib/task/artifact-name.ts +55 -0
  178. package/lib/task/artifact-operations.ts +22 -68
  179. package/lib/task/artifact-receipts.ts +3 -14
  180. package/lib/task/artifact-repair-intent.ts +99 -18
  181. package/lib/task/artifact-schema.ts +5 -34
  182. package/lib/task/capabilities.ts +13 -22
  183. package/lib/task/codex-orchestration.ts +85 -24
  184. package/lib/task/commit-identity.ts +31 -0
  185. package/lib/task/commit-operation.ts +2 -16
  186. package/lib/task/control-authority.ts +502 -5
  187. package/lib/task/control-recovery.ts +174 -0
  188. package/lib/task/create.ts +63 -8
  189. package/lib/task/decision-details.ts +3 -71
  190. package/lib/task/events.ts +131 -10
  191. package/lib/task/finalization-state.ts +11 -0
  192. package/lib/task/human-override.ts +3 -8
  193. package/lib/task/implementation-inputs.ts +3 -3
  194. package/lib/task/intermediate-cleanup.ts +455 -0
  195. package/lib/task/invalidation.ts +18 -62
  196. package/lib/task/ledger-intents.ts +7 -24
  197. package/lib/task/ledger.ts +1 -3
  198. package/lib/task/lifecycle.ts +17 -1
  199. package/lib/task/local-artifact-finalization.ts +148 -182
  200. package/lib/task/manual-validation-completion.ts +40 -0
  201. package/lib/task/manual-validation-evidence.ts +209 -0
  202. package/lib/task/manual-validation-receipt.ts +152 -0
  203. package/lib/task/manual-validation-shared.ts +50 -0
  204. package/lib/task/manual-validation-transaction.ts +304 -0
  205. package/lib/task/markdown.ts +70 -0
  206. package/lib/task/orchestration.ts +19 -19
  207. package/lib/task/qualification-audit.ts +22 -33
  208. package/lib/task/review-artifacts.ts +1 -20
  209. package/lib/task/review-command.ts +23 -0
  210. package/lib/task/review-finalization.ts +131 -259
  211. package/lib/task/review-fingerprint.ts +2 -2
  212. package/lib/task/sections.ts +33 -54
  213. package/lib/task/verification-engine.ts +110 -366
  214. package/lib/task/verification-types.ts +17 -0
  215. package/lib/task/verification.ts +1 -1
  216. package/lib/task/workflow-command.ts +18 -0
  217. package/lib/task/workflow-warning-intents.ts +0 -13
  218. package/lib/task/workflow-warnings.ts +21 -2
  219. package/lib/update.ts +6 -0
  220. package/package.json +6 -6
  221. package/templates/.agents/hooks/lifecycle-delegation.js +6 -6
  222. package/templates/.agents/rules/testing-discipline.en.md +2 -0
  223. package/templates/.agents/rules/testing-discipline.zh-CN.md +2 -0
  224. package/templates/.agents/scripts/package.json +3 -0
  225. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +32 -20
  226. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +32 -20
  227. package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +1 -1
  228. package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +1 -1
  229. package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +2 -0
  230. package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +2 -0
  231. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.en.md +16 -7
  232. package/templates/.agents/skills/complete-manual-validation/reference/summary-update.zh-CN.md +16 -7
  233. package/templates/.agents/skills/complete-task/SKILL.en.md +1 -1
  234. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +1 -1
  235. package/templates/.agents/skills/release/SKILL.en.md +7 -3
  236. package/templates/.agents/skills/release/SKILL.zh-CN.md +7 -3
  237. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +20 -3
  238. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +20 -3
  239. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +4 -1
  240. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +4 -1
  241. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -0
  242. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -0
  243. package/templates/.agents/skills/run-task/SKILL.en.md +1 -1
  244. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +1 -1
  245. package/templates/.agents/skills/test/SKILL.en.md +17 -6
  246. package/templates/.agents/skills/test/SKILL.zh-CN.md +17 -6
  247. package/templates/.claude/commands/run-manual-validation.en.md +1 -1
  248. package/templates/.claude/commands/run-manual-validation.zh-CN.md +1 -1
  249. package/dist/lib/platform/adapters.js +0 -56
  250. package/lib/platform/adapters.ts +0 -197
package/README.md CHANGED
@@ -93,6 +93,16 @@ Every command above works the same way in Claude Code, Codex, Antigravity CLI, a
93
93
  - **AI-assisted updates**: template changes can be merged while preserving project-specific customization
94
94
  - **Auditable process-data archive**: preserve local tasks and GitHub evidence in append-only, verifiable snapshots
95
95
 
96
+ ### Sandbox control boundary
97
+
98
+ Task-bound sandboxes use a host-only control service for lifecycle and workflow writes. On Linux its fixed endpoint is `/run/user/<uid>/agent-infra/host-control.sock`; on macOS it is `/Users/<login>/Library/Application Support/agent-infra/run/host-control.sock`, resolved from the account identity rather than `HOME` or `TMPDIR`. The service directory is private (`0700`) and the socket is private (`0600`). If the endpoint or sandbox identity is unavailable, task-control commands fail closed.
99
+
100
+ The shell installer creates and starts the user-scoped service through systemd user units on Linux or launchd user agents on macOS. After a manual npm install, run `agent-infra-internal host-control install`, then enable the generated user service; `agent-infra-internal host-control status` reports endpoint health. No root or setuid service is required.
101
+
102
+ On macOS, the installer retries service registration during reload. If registration or startup still fails, it exits with an error and a recovery command.
103
+
104
+ The task directory exposed inside a task-bound sandbox is a writable projection. Artifact commands accept only typed workflow operations and canonical artifact basenames. The host opens candidates with `O_NOFOLLOW`, reads and hashes them through one file descriptor, checks observable metadata changes, and atomically lands validated bytes into the authoritative task directory. Changes to the projection's protected topology fail closed and never turn the projection into task state.
105
+
96
106
  ## Quick Start
97
107
 
98
108
  ### 1. Install agent-infra
package/README.zh-CN.md CHANGED
@@ -92,6 +92,16 @@ agent-infra 的目标就是把这层共享基础设施标准化。它为所有
92
92
  - **AI 辅助升级**:模板升级时可合并变更,同时尽量保留项目侧定制
93
93
  - **可审计的过程数据归档**:把本地任务与 GitHub 证据保存为只追加、可校验的快照
94
94
 
95
+ ### 沙箱控制边界
96
+
97
+ 任务沙箱的生命周期和工作流写操作由宿主专用控制服务执行。Linux 固定端点为 `/run/user/<uid>/agent-infra/host-control.sock`;macOS 固定端点为 `/Users/<login>/Library/Application Support/agent-infra/run/host-control.sock`,路径根据账户身份解析,不读取 `HOME` 或 `TMPDIR`。服务目录权限为 `0700`,socket 权限为 `0600`。端点或沙箱身份不可用时,task-control 命令会失败关闭。
98
+
99
+ Shell 安装脚本会在 Linux 通过 systemd 用户单元、在 macOS 通过 launchd 用户代理创建并启动该服务。手动 npm 安装后,可运行 `agent-infra-internal host-control install` 创建服务配置,再启用对应的用户服务;`agent-infra-internal host-control status` 可检查端点健康状态。整个服务不需要 root 或 setuid。
100
+
101
+ macOS 重载期间,安装脚本会重试服务注册;若最终仍无法注册或启动,则以错误退出并提供恢复命令。
102
+
103
+ 任务沙箱内的 task 目录是可写投影。产物命令只接受类型化 workflow 操作和规范的产物 basename。宿主使用 `O_NOFOLLOW` 打开候选文件,通过同一个文件描述符读取并计算摘要,检查可观察的元数据变化,再把已校验字节原子落入权威 task 目录。投影的受保护拓扑发生变化时会失败关闭,投影修改不会变成任务状态。
104
+
95
105
  ## 快速开始
96
106
 
97
107
  ### 1. 安装 agent-infra
@@ -0,0 +1,45 @@
1
+ #!/bin/sh
2
+ set -eu
3
+
4
+ script_path=$0
5
+ case "$script_path" in
6
+ [A-Za-z]:*)
7
+ drive=${script_path%"${script_path#?}"}
8
+ drive=$(printf '%s' "$drive" | tr '[:upper:]' '[:lower:]')
9
+ script_path="/${drive}${script_path#?:}"
10
+ script_path=$(printf '%s' "$script_path" | tr '\\' '/')
11
+ ;;
12
+ /*) ;;
13
+ *) script_path=$PWD/$script_path ;;
14
+ esac
15
+ while [ -L "$script_path" ]; do
16
+ link_target=$(readlink "$script_path")
17
+ case "$link_target" in
18
+ /*) script_path=$link_target ;;
19
+ *) script_path=$(dirname -- "$script_path")/$link_target ;;
20
+ esac
21
+ done
22
+ script_dir=$(CDPATH= cd -- "$(dirname -- "$script_path")" && pwd -P)
23
+ if [ -f "$script_dir/internal-cli.js" ]; then
24
+ internal_cli="$script_dir/internal-cli.js"
25
+ else
26
+ internal_cli="$script_dir/../dist/bin/internal-cli.js"
27
+ fi
28
+
29
+ command=${1-}
30
+ task_control_command=false
31
+ case "$command" in
32
+ task-lifecycle|task-orchestration|task-finalization) task_control_command=true ;;
33
+ esac
34
+
35
+ status_mount_present=false
36
+ if [ -d /run/agent-infra/control-status ]; then status_mount_present=true; fi
37
+ launch_config=${AGENT_INFRA_TASK_ID-}${AGENT_INFRA_CONTROL_TOKEN-}${AGENT_INFRA_CONTROL_GENERATION-}${AGENT_INFRA_CONTROL_DIR-}${AGENT_INFRA_CONTROL_STATUS_DIR-}${AGENT_INFRA_RUNTIME_DIR-}${AGENT_INFRA_EXECUTOR_MANIFEST-}
38
+ if [ "$task_control_command" = true ] && [ "$status_mount_present" = true ] && [ -z "$launch_config" ] && [ -z "${AGENT_INFRA_TEST_HOST_CONTROL_ENDPOINT-}" ]; then
39
+ printf '%s\n' '{"status":"failed","changed":false,"error":{"code":"SANDBOX_CONTROL_IDENTITY_MISSING","message":"sandbox control identity is present but its launch configuration is missing"}}'
40
+ exit 1
41
+ fi
42
+ # The outer launcher owns the Node startup boundary. A task-bound process must
43
+ # not pass preload, import, or loader injection into the control router.
44
+ unset NODE_OPTIONS NODE_PATH
45
+ exec node "$internal_cli" "$@"
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { classifySandboxControlEnvironment } from '../lib/sandbox/control/client.js';
3
- import { formatTaskViewDiagnostic, guardTaskOperation, TaskViewOperationError } from '../lib/internal/task-operation-registry.js';
2
+ import { formatTaskViewDiagnostic, guardTaskOperation, resolveSandboxControlTransport, TaskViewOperationError } from '../lib/internal/task-operation-registry.js';
4
3
  import { INTERNAL_HANDLER_ROUTE_SELECTORS } from '../lib/internal/cli-route-inventory.js';
4
+ import { hostControlRequestForCommand, requestHostControl, HostControlClientError, HOST_CONTROL_TEST_ENVIRONMENT_KEYS } from '../lib/host-control/client.js';
5
+ import { resolveHostControlEndpoint, readHostControlWorkerToken } from '../lib/host-control/path.js';
5
6
  const [major = 0, minor = 0] = process.versions.node.split('.').map((part) => parseInt(part, 10));
6
7
  if (major < 22 || (major === 22 && minor < 9)) {
7
8
  process.stderr.write(`agent-infra-internal requires Node.js >= 22.9.0 (current: ${process.version})\n`);
@@ -10,6 +11,16 @@ if (major < 22 || (major === 22 && minor < 9)) {
10
11
  const command = process.argv[2] || '';
11
12
  const internalRouteRegistered = Object.hasOwn(INTERNAL_HANDLER_ROUTE_SELECTORS, command);
12
13
  const taskControlCommand = command === 'task-lifecycle' || command === 'task-orchestration' || command === 'task-finalization';
14
+ const taskWorkflowCommand = command === 'task-artifact'
15
+ || command === 'task-review'
16
+ || command === 'task-event'
17
+ || command === 'task-ledger'
18
+ || command === 'task-invalidation'
19
+ || command === 'task-warning';
20
+ const manualValidationWorkflowCommand = command === 'manual-validation';
21
+ const localTaskControlHelp = taskControlCommand
22
+ && (process.argv[3] === '--help' || process.argv[3] === '-h')
23
+ && !process.env.AGENT_INFRA_TEST_HOST_CONTROL_ENDPOINT;
13
24
  let taskViewGuardFailed = false;
14
25
  try {
15
26
  const guard = guardTaskOperation('internal', command, process.argv.slice(3));
@@ -27,44 +38,86 @@ catch (error) {
27
38
  throw error;
28
39
  }
29
40
  }
30
- function taskControlTransportFailure(message) {
41
+ function taskControlTransportFailure(message, code = 'TASK_CONTROL_TRANSPORT_INVALID', changed = false) {
31
42
  process.stdout.write(`${JSON.stringify({
32
- status: 'failed', changed: false,
33
- error: { code: 'TASK_CONTROL_TRANSPORT_INVALID', message }
43
+ status: 'failed', changed,
44
+ error: { code, message }
34
45
  })}\n`);
35
46
  process.exit(1);
36
47
  }
37
- if (!taskViewGuardFailed && taskControlCommand) {
38
- const environment = classifySandboxControlEnvironment();
39
- if (environment.kind === 'invalid') {
40
- taskControlTransportFailure(environment.message ?? 'sandbox client control configuration is invalid');
48
+ async function runHostControlCommand(commandName, args) {
49
+ let response;
50
+ try {
51
+ const testEndpoint = process.env.AGENT_INFRA_TEST_HOST_CONTROL_ENDPOINT;
52
+ const forwardedEnvironment = testEndpoint
53
+ ? Object.fromEntries(HOST_CONTROL_TEST_ENVIRONMENT_KEYS.flatMap((key) => process.env[key] === undefined ? [] : [[key, process.env[key]]]))
54
+ : {};
55
+ response = await requestHostControl({
56
+ request: hostControlRequestForCommand(commandName, args, process.cwd(), forwardedEnvironment),
57
+ endpoint: process.env.AGENT_INFRA_TEST_HOST_CONTROL_ENDPOINT ?? resolveHostControlEndpoint()
58
+ });
41
59
  }
42
- if (environment.kind === 'controlled') {
43
- const { sandboxControl } = await import('../lib/internal/sandbox-control.js');
44
- await sandboxControl(['client', command, ...process.argv.slice(3)]);
60
+ catch (error) {
61
+ if (error instanceof HostControlClientError) {
62
+ taskControlTransportFailure(error.message, error.changed === false
63
+ ? 'SANDBOX_CONTROL_HOST_AUTHORITY_UNAVAILABLE' : error.code, error.changed);
64
+ }
65
+ throw error;
45
66
  }
46
- else {
47
- switch (command) {
48
- case 'task-orchestration': {
49
- const { taskOrchestration } = await import('../lib/internal/task-orchestration.js');
50
- await taskOrchestration(process.argv.slice(3));
51
- break;
52
- }
53
- case 'task-lifecycle': {
54
- const { taskLifecycle } = await import('../lib/internal/task-lifecycle.js');
55
- await taskLifecycle(process.argv.slice(3));
56
- break;
57
- }
58
- case 'task-finalization': {
59
- const { taskFinalization } = await import('../lib/internal/task-finalization.js');
60
- await taskFinalization(process.argv.slice(3));
61
- break;
62
- }
67
+ process.stdout.write(response.stdout);
68
+ process.stderr.write(response.stderr);
69
+ process.exitCode = response.exitCode;
70
+ }
71
+ const hostWorkerRequested = process.env.AGENT_INFRA_HOST_CONTROL_WORKER === '1';
72
+ const hostWorker = hostWorkerRequested && (() => {
73
+ try {
74
+ const endpoint = process.env.AGENT_INFRA_TEST_HOST_CONTROL_ENDPOINT ?? resolveHostControlEndpoint();
75
+ return process.env.AGENT_INFRA_HOST_CONTROL_WORKER_TOKEN === readHostControlWorkerToken(endpoint);
76
+ }
77
+ catch {
78
+ return false;
79
+ }
80
+ })();
81
+ if (hostWorkerRequested && !hostWorker && (taskControlCommand || taskWorkflowCommand || manualValidationWorkflowCommand)) {
82
+ taskControlTransportFailure('host-control worker authorization is invalid', 'SANDBOX_CONTROL_HOST_AUTHORITY_UNAVAILABLE');
83
+ }
84
+ let controlRouted = false;
85
+ if (!taskViewGuardFailed && (taskControlCommand || taskWorkflowCommand || manualValidationWorkflowCommand) && !hostWorker && !localTaskControlHelp) {
86
+ const transport = resolveSandboxControlTransport(process.env);
87
+ switch (transport.kind) {
88
+ case 'fail-closed': {
89
+ const reason = transport.reasonCode ?? 'TASK_CONTROL_TRANSPORT_INVALID';
90
+ taskControlTransportFailure(reason, reason.startsWith('SANDBOX_CONTROL_IDENTITY_') ? reason : undefined);
91
+ break;
92
+ }
93
+ case 'direct-host':
94
+ await runHostControlCommand(command, process.argv.slice(3));
95
+ break;
96
+ case 'broker-client': {
97
+ const { sandboxControl } = await import('../lib/internal/sandbox-control.js');
98
+ await sandboxControl(['client', ...(taskControlCommand ? [command] : ['task-workflow', command]), ...process.argv.slice(3)]);
99
+ break;
63
100
  }
64
101
  }
102
+ controlRouted = true;
65
103
  }
66
- else if (!taskViewGuardFailed && internalRouteRegistered)
104
+ if (!controlRouted && !taskViewGuardFailed && internalRouteRegistered)
67
105
  switch (command) {
106
+ case 'task-orchestration': {
107
+ const { taskOrchestration } = await import('../lib/internal/task-orchestration.js');
108
+ await taskOrchestration(process.argv.slice(3));
109
+ break;
110
+ }
111
+ case 'task-lifecycle': {
112
+ const { taskLifecycle } = await import('../lib/internal/task-lifecycle.js');
113
+ await taskLifecycle(process.argv.slice(3));
114
+ break;
115
+ }
116
+ case 'task-finalization': {
117
+ const { taskFinalization } = await import('../lib/internal/task-finalization.js');
118
+ await taskFinalization(process.argv.slice(3));
119
+ break;
120
+ }
68
121
  case 'task-create': {
69
122
  const { taskCreate } = await import('../lib/internal/task-create.js');
70
123
  await taskCreate(process.argv.slice(3));
@@ -75,6 +128,11 @@ else if (!taskViewGuardFailed && internalRouteRegistered)
75
128
  await sandboxControl(process.argv.slice(3));
76
129
  break;
77
130
  }
131
+ case 'host-control': {
132
+ const { hostControl } = await import('../lib/internal/host-control.js');
133
+ await hostControl(process.argv.slice(3));
134
+ break;
135
+ }
78
136
  case 'agent-client': {
79
137
  const { agentClient } = await import('../lib/internal/agent-client.js');
80
138
  agentClient(process.argv.slice(3));
@@ -240,6 +298,11 @@ else if (!taskViewGuardFailed && internalRouteRegistered)
240
298
  taskValidate(process.argv.slice(3));
241
299
  break;
242
300
  }
301
+ case 'manual-validation': {
302
+ const { manualValidation } = await import('../lib/internal/manual-validation.js');
303
+ await manualValidation(process.argv.slice(3));
304
+ break;
305
+ }
243
306
  default:
244
307
  process.stdout.write(`${JSON.stringify({
245
308
  status: 'failed',
@@ -89,6 +89,21 @@ function provenanceDetail(record, expected) {
89
89
  function exactKeys(value, keys) {
90
90
  return Object.keys(value).length === keys.length && keys.every((key) => Object.hasOwn(value, key));
91
91
  }
92
+ function isRecoveryPhase(value) {
93
+ return value === 'orchestration.prepare'
94
+ || value === 'artifact.finalize-local'
95
+ || value === 'task-event.completed';
96
+ }
97
+ function isRecoveryPhaseRecord(value) {
98
+ if (!value || typeof value !== 'object' || Array.isArray(value))
99
+ return false;
100
+ const phase = value;
101
+ return exactKeys(phase, ['phase', 'requestId', 'state'])
102
+ && isRecoveryPhase(phase.phase)
103
+ && typeof phase.requestId === 'string'
104
+ && /^[a-f0-9-]{16,64}$/u.test(phase.requestId)
105
+ && (phase.state === 'issued' || phase.state === 'consumed');
106
+ }
92
107
  function isCodexControllerBinding(value) {
93
108
  if (!value || typeof value !== 'object' || Array.isArray(value))
94
109
  return false;
@@ -176,18 +191,26 @@ function createCodexCapabilityStore(options = {}) {
176
191
  const root = options.root
177
192
  ?? resolveAgentRuntimeStoreRoot({ store: 'capabilities' });
178
193
  const now = options.now ?? Date.now;
179
- const createToken = options.token ?? (() => crypto.randomBytes(32).toString('base64url'));
194
+ const createReference = options.reference ?? (() => crypto.randomBytes(32).toString('base64url'));
180
195
  const ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
181
196
  const tombstoneMs = options.tombstoneMs ?? DEFAULT_TOMBSTONE_MS;
182
197
  const maxRecords = options.maxRecords ?? DEFAULT_MAX_RECORDS;
183
198
  fs.mkdirSync(root, { recursive: true, mode: 0o700 });
184
199
  fs.chmodSync(root, 0o700);
185
- function fileFor(token) {
186
- return path.join(root, `${digest(token)}.json`);
200
+ function fileFor(capabilityRef) {
201
+ return path.join(root, `${digest(capabilityRef)}.json`);
187
202
  }
188
203
  function read(file) {
189
204
  const value = JSON.parse(fs.readFileSync(file, 'utf8'));
190
- if (value.schemaVersion !== 1 || !Number.isSafeInteger(value.revision) || value.revision < 1) {
205
+ const keys = Object.keys(value).sort().join(',');
206
+ if (keys !== 'armedAt,attestedAt,buildIdentity,capabilityRefDigest,consumedAt,controller,expiresAt,hookDefinitionHash,recoveryOperationId,recoveryPhases,recoveryState,reservedAt,revision,schemaVersion,sessionId,status,taskId,toolUseId,turnId'
207
+ || value.schemaVersion !== 2 || !Number.isSafeInteger(value.revision) || value.revision < 1
208
+ || typeof value.capabilityRefDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(value.capabilityRefDigest)
209
+ || !['unreserved', 'reserved', 'consumed'].includes(value.recoveryState)
210
+ || (value.recoveryOperationId !== null && typeof value.recoveryOperationId !== 'string')
211
+ || (value.reservedAt !== null && !Number.isSafeInteger(value.reservedAt))
212
+ || !Array.isArray(value.recoveryPhases)
213
+ || !value.recoveryPhases.every((phase) => isRecoveryPhaseRecord(phase))) {
191
214
  throw capabilityError('CODEX_CAPABILITY_STATE_INVALID', 'capability record schema is invalid');
192
215
  }
193
216
  return value;
@@ -294,17 +317,21 @@ function createCodexCapabilityStore(options = {}) {
294
317
  }
295
318
  function arm(input) {
296
319
  sweep();
297
- const token = createToken();
298
- const file = fileFor(token);
320
+ const capabilityRef = createReference();
321
+ const file = fileFor(capabilityRef);
299
322
  if (fs.existsSync(file))
300
323
  throw capabilityError('CODEX_CAPABILITY_TOKEN_COLLISION', 'capability token already exists');
301
324
  const timestamp = now();
302
325
  const record = Object.freeze({
303
- schemaVersion: 1,
326
+ schemaVersion: 2,
304
327
  revision: 1,
305
- tokenDigest: digest(token),
328
+ capabilityRefDigest: digest(capabilityRef),
306
329
  taskId: input.taskId,
307
330
  status: 'armed',
331
+ recoveryOperationId: null,
332
+ recoveryState: 'unreserved',
333
+ reservedAt: null,
334
+ recoveryPhases: Object.freeze([]),
308
335
  buildIdentity: input.buildIdentity,
309
336
  controller: input.controller ?? null,
310
337
  sessionId: null,
@@ -317,26 +344,27 @@ function createCodexCapabilityStore(options = {}) {
317
344
  consumedAt: null
318
345
  });
319
346
  write(file, record, 0);
320
- return Object.freeze({
347
+ const result = {
321
348
  ...record,
322
- token,
349
+ capabilityRef,
323
350
  path: file,
324
- marker: `agent-infra-codex-capability:${token}`
325
- });
351
+ marker: `agent-infra-codex-capability:${capabilityRef}`
352
+ };
353
+ return Object.freeze(result);
326
354
  }
327
- function loadActive(token) {
328
- const file = fileFor(token);
355
+ function loadActive(capabilityRef, options = {}) {
356
+ const file = fileFor(capabilityRef);
329
357
  if (!fs.existsSync(file)) {
330
358
  throw capabilityError('CODEX_CAPABILITY_MISSING', 'capability token was not found');
331
359
  }
332
360
  const record = expire(read(file), file, now());
333
- if (record.status === 'expired') {
361
+ if (record.status === 'expired' && !options.allowExpiredReserved) {
334
362
  throw capabilityError('CODEX_CAPABILITY_EXPIRED', 'capability token expired');
335
363
  }
336
364
  return { file, record };
337
365
  }
338
- function attest(input) {
339
- const { file, record } = loadActive(input.token);
366
+ function attestByReference(input) {
367
+ const { file, record } = loadActive(input.capabilityRef);
340
368
  if (record.status !== 'armed') {
341
369
  throw capabilityError(record.status === 'consumed' ? 'CODEX_CAPABILITY_REPLAY' : 'CODEX_CAPABILITY_STATE_INVALID', `capability is ${record.status}, expected armed`);
342
370
  }
@@ -362,11 +390,14 @@ function createCodexCapabilityStore(options = {}) {
362
390
  write(file, next, record.revision);
363
391
  return next;
364
392
  }
365
- function validateRecord(record, expected) {
393
+ function validateRecord(record, expected, options = {}) {
366
394
  if (record.status === 'consumed') {
367
395
  throw capabilityError('CODEX_CAPABILITY_REPLAY', 'capability token was already consumed');
368
396
  }
369
- if (record.status !== 'attested') {
397
+ const expiredReserved = options.allowExpiredReserved
398
+ && record.status === 'expired'
399
+ && record.recoveryState === 'reserved';
400
+ if (record.status !== 'attested' && !expiredReserved) {
370
401
  throw capabilityError('CODEX_CAPABILITY_NOT_ATTESTED', 'capability token has no current-session hook attestation');
371
402
  }
372
403
  const identity = record.buildIdentity && typeof record.buildIdentity === 'object'
@@ -380,28 +411,135 @@ function createCodexCapabilityStore(options = {}) {
380
411
  }
381
412
  return record;
382
413
  }
383
- function validate(token, expected) {
414
+ function validateReference(capabilityRef, expected) {
384
415
  sweep();
385
- const { record } = loadActive(token);
416
+ const { record } = loadActive(capabilityRef);
386
417
  return validateRecord(record, expected);
387
418
  }
388
- function consume(token, expected) {
419
+ function consumeReference(capabilityRef, operationId, expected, options = {}) {
389
420
  sweep();
390
- const { file, record } = loadActive(token);
391
- validateRecord(record, expected);
421
+ if (!operationId || /[\r\n]/u.test(operationId)) {
422
+ throw capabilityError('CODEX_CAPABILITY_RECOVERY_INVALID', 'recovery operation id is invalid');
423
+ }
424
+ const { file, record } = loadActive(capabilityRef, options);
425
+ if (record.status === 'consumed' && record.recoveryState === 'consumed' && record.recoveryOperationId === operationId) {
426
+ return record;
427
+ }
428
+ validateRecord(record, expected, options);
429
+ if (record.recoveryState === 'consumed') {
430
+ if (record.recoveryOperationId === operationId)
431
+ return record;
432
+ throw capabilityError('CODEX_CAPABILITY_REPLAY', 'capability recovery operation was already consumed');
433
+ }
434
+ if (record.recoveryState !== 'reserved' || record.recoveryOperationId !== operationId) {
435
+ throw capabilityError('CODEX_CAPABILITY_RECOVERY_INVALID', 'capability must be reserved by this operation before consumption');
436
+ }
392
437
  const next = Object.freeze({
393
438
  ...record,
394
439
  revision: record.revision + 1,
395
440
  status: 'consumed',
441
+ recoveryState: 'consumed',
396
442
  consumedAt: now()
397
443
  });
398
444
  write(file, next, record.revision);
399
445
  return next;
400
446
  }
401
- function inspect(token) {
402
- return loadActive(token).record;
447
+ function reserveRecoveryPhase(capabilityRef, operationId, phase, requestId, expected) {
448
+ if (!operationId || /[\r\n]/u.test(operationId) || !isRecoveryPhase(phase)
449
+ || !/^[a-f0-9-]{16,64}$/u.test(requestId)) {
450
+ throw capabilityError('CODEX_CAPABILITY_RECOVERY_INVALID', 'recovery phase identity is invalid');
451
+ }
452
+ sweep();
453
+ const { file, record } = loadActive(capabilityRef);
454
+ validateRecord(record, expected);
455
+ if (record.recoveryState !== 'reserved' || record.recoveryOperationId !== operationId) {
456
+ throw capabilityError('CODEX_CAPABILITY_RECOVERY_INVALID', 'capability must be reserved by this operation before phase reservation');
457
+ }
458
+ if (record.recoveryPhases.some((entry) => entry.phase === phase)) {
459
+ throw capabilityError('CODEX_CAPABILITY_PHASE_REPLAY', 'recovery phase was already reserved');
460
+ }
461
+ const next = Object.freeze({
462
+ ...record,
463
+ revision: record.revision + 1,
464
+ recoveryPhases: Object.freeze([
465
+ ...record.recoveryPhases,
466
+ Object.freeze({ phase, requestId, state: 'issued' })
467
+ ])
468
+ });
469
+ write(file, next, record.revision);
470
+ return next;
471
+ }
472
+ function consumeRecoveryPhase(capabilityRef, operationId, phase, requestId, expected, options = {}) {
473
+ if (!operationId || /[\r\n]/u.test(operationId) || !isRecoveryPhase(phase)
474
+ || !/^[a-f0-9-]{16,64}$/u.test(requestId)) {
475
+ throw capabilityError('CODEX_CAPABILITY_RECOVERY_INVALID', 'recovery phase identity is invalid');
476
+ }
477
+ sweep();
478
+ const { file, record } = loadActive(capabilityRef, options);
479
+ validateRecord(record, expected, options);
480
+ if (record.recoveryState !== 'reserved' && record.recoveryState !== 'consumed') {
481
+ throw capabilityError('CODEX_CAPABILITY_RECOVERY_INVALID', 'capability has no reserved recovery operation');
482
+ }
483
+ if (record.recoveryOperationId !== operationId) {
484
+ throw capabilityError('CODEX_CAPABILITY_REPLAY', 'capability recovery operation does not match');
485
+ }
486
+ const current = record.recoveryPhases.find((entry) => entry.phase === phase);
487
+ if (!current || current.requestId !== requestId) {
488
+ throw capabilityError('CODEX_CAPABILITY_PHASE_REPLAY', 'recovery phase identity does not match');
489
+ }
490
+ if (current.state === 'consumed')
491
+ return record;
492
+ const next = Object.freeze({
493
+ ...record,
494
+ revision: record.revision + 1,
495
+ recoveryPhases: Object.freeze(record.recoveryPhases.map((entry) => entry.phase === phase ? Object.freeze({ ...entry, state: 'consumed' }) : entry))
496
+ });
497
+ write(file, next, record.revision);
498
+ return next;
499
+ }
500
+ function reserveReference(capabilityRef, operationId, expected) {
501
+ if (!operationId || /[\r\n]/u.test(operationId)) {
502
+ throw capabilityError('CODEX_CAPABILITY_RECOVERY_INVALID', 'recovery operation id is invalid');
503
+ }
504
+ sweep();
505
+ const { file, record } = loadActive(capabilityRef);
506
+ validateRecord(record, expected);
507
+ if (record.recoveryState === 'reserved') {
508
+ if (record.recoveryOperationId === operationId)
509
+ return record;
510
+ throw capabilityError('CODEX_CAPABILITY_BUSY', 'capability is reserved by another recovery operation');
511
+ }
512
+ if (record.recoveryState === 'consumed') {
513
+ if (record.recoveryOperationId === operationId)
514
+ return record;
515
+ throw capabilityError('CODEX_CAPABILITY_REPLAY', 'capability recovery operation was already consumed');
516
+ }
517
+ const next = Object.freeze({
518
+ ...record,
519
+ revision: record.revision + 1,
520
+ recoveryOperationId: operationId,
521
+ recoveryState: 'reserved',
522
+ reservedAt: now()
523
+ });
524
+ write(file, next, record.revision);
525
+ return next;
526
+ }
527
+ function inspectReference(capabilityRef) {
528
+ return loadActive(capabilityRef).record;
529
+ }
530
+ function findByRecoveryOperation(operationId) {
531
+ if (!operationId || /[\r\n]/u.test(operationId)) {
532
+ throw capabilityError('CODEX_CAPABILITY_RECOVERY_INVALID', 'recovery operation id is invalid');
533
+ }
534
+ return Object.freeze(files()
535
+ .map((file) => read(file))
536
+ .filter((record) => record.recoveryOperationId === operationId));
403
537
  }
404
- return Object.freeze({ arm, attest, consume, inspect, validate, sweep });
538
+ return Object.freeze({
539
+ arm, attestByReference, reserveReference, reserveRecoveryPhase, consumeRecoveryPhase,
540
+ consumeReference, inspectReference,
541
+ findByRecoveryOperation, validateReference, sweep
542
+ });
405
543
  }
406
544
  export { createCodexCapabilityStore };
407
545
  export { isCodexCapabilityProvenanceDetail };
@@ -274,9 +274,10 @@ function prepareCodexSandboxController(input, options = {}) {
274
274
  const shimDir = path.join(home, 'bin');
275
275
  fs.mkdirSync(shimDir, { mode: 0o700 });
276
276
  const internalCli = path.resolve(process.argv[1] ?? path.join(repoRoot, 'bin', 'internal-cli.ts'));
277
+ const launcherPrefix = ['#!/bin/sh', 'set -eu', 'unset NODE_OPTIONS NODE_PATH'].join('\n') + '\n';
277
278
  const source = internalCli.endsWith('.ts')
278
- ? `#!/bin/sh\nexec ${shellQuote(process.execPath)} --experimental-strip-types ${shellQuote(internalCli)} "$@"\n`
279
- : `#!/bin/sh\nexec ${shellQuote(process.execPath)} ${shellQuote(internalCli)} "$@"\n`;
279
+ ? `${launcherPrefix}exec ${shellQuote(process.execPath)} --experimental-strip-types ${shellQuote(internalCli)} "$@"\n`
280
+ : `${launcherPrefix}exec ${shellQuote(process.execPath)} ${shellQuote(internalCli)} "$@"\n`;
280
281
  fs.writeFileSync(path.join(shimDir, 'agent-infra-internal'), source, { mode: 0o700 });
281
282
  const policy = [
282
283
  input.executorModel ? `--executor-model ${input.executorModel}` : '',