@bitkyc08/opencodex 2.86.0 → 2.87.0

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 (378) hide show
  1. package/gui/dist/assets/App-ZfYbLHkB.js +58 -0
  2. package/gui/dist/assets/App-wT3mXqFz.css +1 -0
  3. package/gui/dist/assets/{Tray-DyR9yBee.js → Tray-DCd-A1az.js} +1 -1
  4. package/gui/dist/assets/index-CfYDMj34.js +94 -0
  5. package/gui/dist/assets/index-DPeB9HpV.css +1 -0
  6. package/gui/dist/assets/quota-summary-ChvmKGOk.js +1 -0
  7. package/gui/dist/index.html +2 -2
  8. package/package.json +1 -1
  9. package/src/adapters/anthropic/beta-allowlist.ts +9 -8
  10. package/src/adapters/anthropic/beta-body-pairs.ts +93 -0
  11. package/src/adapters/anthropic/passthrough.ts +7 -23
  12. package/src/adapters/anthropic-progress.ts +84 -0
  13. package/src/adapters/anthropic.ts +108 -21
  14. package/src/adapters/base.ts +2 -0
  15. package/src/adapters/claude-cli/adapter.ts +111 -23
  16. package/src/adapters/claude-cli/profiles.ts +2 -1
  17. package/src/adapters/claude-cli/stable-replay.ts +108 -0
  18. package/src/adapters/claude-cli/usage-admission.ts +230 -0
  19. package/src/adapters/coding-agent/protocol.ts +1 -1
  20. package/src/adapters/coding-agent/turn.ts +18 -2
  21. package/src/adapters/dsh-account.ts +97 -0
  22. package/src/adapters/google.ts +35 -8
  23. package/src/adapters/openai-chat/messages.ts +4 -3
  24. package/src/adapters/openai-chat/passthrough.ts +8 -2
  25. package/src/adapters/openai-chat/reasoning-replay.ts +52 -0
  26. package/src/adapters/openai-responses/compaction-search-history.ts +14 -7
  27. package/src/adapters/openai-responses/passthrough.ts +57 -125
  28. package/src/adapters/openai-responses/request-strips.ts +25 -3
  29. package/src/adapters/openai-responses/transport.ts +81 -0
  30. package/src/adapters/qoder/adapter.ts +4 -2
  31. package/src/adapters/registry.ts +5 -0
  32. package/src/adapters/zed.ts +26 -1
  33. package/src/advisor/activity.ts +55 -0
  34. package/src/advisor/budget.ts +62 -0
  35. package/src/advisor/consult.ts +4 -1
  36. package/src/advisor/context.ts +16 -4
  37. package/src/advisor/evidence-tool.ts +21 -0
  38. package/src/advisor/evidence.ts +117 -0
  39. package/src/advisor/runtime.ts +79 -31
  40. package/src/advisor/settings.ts +11 -2
  41. package/src/bridge/errors.ts +19 -1
  42. package/src/bridge/internal.ts +3 -1
  43. package/src/bridge/response-json.ts +18 -3
  44. package/src/bridge/sse.ts +58 -8
  45. package/src/bridge/terminal-refusal.ts +59 -0
  46. package/src/claude/desktop-3p-library.ts +32 -7
  47. package/src/claude/desktop-3p.ts +55 -13
  48. package/src/claude/desktop-first-party.ts +1 -1
  49. package/src/claude/desktop-picker-platform.ts +4 -0
  50. package/src/claude/desktop-picker.ts +15 -8
  51. package/src/claude/desktop-system-proxy.ts +50 -4
  52. package/src/claude/inbound-content-options.ts +11 -0
  53. package/src/claude/inbound.ts +33 -0
  54. package/src/claude/intercept/picker-ca-startup.ts +2 -1
  55. package/src/claude/intercept/picker-runtime.ts +6 -5
  56. package/src/claude/intercept/picker-trust-windows.ts +338 -0
  57. package/src/claude/intercept/picker-trust.ts +19 -0
  58. package/src/claude/intercept/port-policy.ts +30 -0
  59. package/src/claude/intercept/runtime.ts +97 -94
  60. package/src/claude/outbound.ts +38 -13
  61. package/src/cli/account-api.ts +1 -0
  62. package/src/cli/account-policy-dto.ts +6 -0
  63. package/src/cli/account-policy.ts +34 -5
  64. package/src/cli/account-preservation.ts +44 -0
  65. package/src/cli/account.ts +8 -2
  66. package/src/cli/advisor.ts +17 -3
  67. package/src/cli/capabilities-accounts.ts +20 -1
  68. package/src/cli/capabilities-agents-routing.ts +5 -2
  69. package/src/cli/capabilities-base.ts +38 -10
  70. package/src/cli/capabilities-observe-system.ts +63 -3
  71. package/src/cli/capabilities-provider-models.ts +11 -0
  72. package/src/cli/codex-cli-update.ts +151 -41
  73. package/src/cli/codex-provider.ts +44 -0
  74. package/src/cli/combo-input.ts +10 -1
  75. package/src/cli/combo-result.ts +1 -1
  76. package/src/cli/combo.ts +2 -1
  77. package/src/cli/cross-home-owner.ts +17 -14
  78. package/src/cli/dispatch.ts +25 -4
  79. package/src/cli/doctor.ts +6 -1
  80. package/src/cli/ensure-desktop-cold-start.ts +60 -7
  81. package/src/cli/help.ts +3 -2
  82. package/src/cli/index.ts +1 -1
  83. package/src/cli/integration-plan-dto.ts +1 -0
  84. package/src/cli/log-history.ts +172 -0
  85. package/src/cli/models-runtime-subcommands.ts +1 -0
  86. package/src/cli/models-runtime.ts +72 -8
  87. package/src/cli/observe-stream.ts +2 -0
  88. package/src/cli/observe.ts +16 -4
  89. package/src/cli/provider.ts +27 -5
  90. package/src/cli/registry.ts +28 -8
  91. package/src/cli/runtime-api.ts +5 -3
  92. package/src/cli/system-command.ts +40 -8
  93. package/src/cli/uninstall-client-state.ts +32 -0
  94. package/src/cli/usage-compare.ts +309 -0
  95. package/src/cli/usage-model-search.ts +4 -4
  96. package/src/cli/usage-report.ts +2 -2
  97. package/src/cli/v2.ts +1 -1
  98. package/src/client/machine-listener.ts +1 -3
  99. package/src/client/runtime.ts +1 -0
  100. package/src/clients/client-path.ts +51 -0
  101. package/src/clients/config-export/contracts.ts +2 -1
  102. package/src/clients/config-export.ts +18 -44
  103. package/src/clients/gentle-shell-path.ts +87 -0
  104. package/src/codex/account-auto-switch.ts +2 -0
  105. package/src/codex/account-identity.ts +29 -0
  106. package/src/codex/account-label.ts +1 -1
  107. package/src/codex/account-lifecycle.ts +21 -5
  108. package/src/codex/account-preservation.ts +28 -0
  109. package/src/codex/account-use-remaining.ts +23 -0
  110. package/src/codex/app-server-processes.ts +97 -7
  111. package/src/codex/app-server-restart-service.ts +55 -4
  112. package/src/codex/auth-api/account-list.ts +4 -0
  113. package/src/codex/auth-api/account-pause-group.ts +6 -50
  114. package/src/codex/auth-api/account-preservation.ts +57 -0
  115. package/src/codex/auth-api/routes.ts +8 -0
  116. package/src/codex/auth-api/use-remaining-route.ts +36 -0
  117. package/src/codex/auth-api.ts +0 -1
  118. package/src/codex/auth-context.ts +2 -2
  119. package/src/codex/catalog/auto-review.ts +20 -3
  120. package/src/codex/catalog/build-entries.ts +9 -1
  121. package/src/codex/catalog/combo-member.ts +14 -5
  122. package/src/codex/catalog/configured-tiers.ts +1 -1
  123. package/src/codex/catalog/gather-capture.ts +2 -0
  124. package/src/codex/catalog/metadata.ts +11 -9
  125. package/src/codex/catalog/model-hints.ts +21 -16
  126. package/src/codex/catalog/model-visibility.ts +3 -1
  127. package/src/codex/catalog/parsing.ts +30 -86
  128. package/src/codex/catalog/provider-models.ts +36 -34
  129. package/src/codex/catalog/routed-gather.ts +12 -13
  130. package/src/codex/cli-install-provenance.ts +105 -17
  131. package/src/codex/cli-update-plan.ts +545 -0
  132. package/src/codex/config-write-lock.ts +23 -2
  133. package/src/codex/desktop-app/picker-freshness.ts +96 -0
  134. package/src/codex/desktop-app-restart.ts +21 -0
  135. package/src/codex/desktop-compatibility/installed-build.ts +24 -6
  136. package/src/codex/desktop-compatibility/routing-preflight.ts +1 -1
  137. package/src/codex/desktop-compatibility/runtime.ts +12 -3
  138. package/src/codex/desktop-compatibility/windows-key-protection.ts +115 -9
  139. package/src/codex/features.ts +85 -45
  140. package/src/codex/inject/history-failure.ts +27 -0
  141. package/src/codex/inject/restore.ts +24 -2
  142. package/src/codex/inject/routing-classify.ts +4 -3
  143. package/src/codex/inject.ts +18 -27
  144. package/src/codex/main-account-hard-lock.ts +30 -1
  145. package/src/codex/native-profile-startup.ts +1 -1
  146. package/src/codex/ocx-compaction-history.ts +12 -10
  147. package/src/codex/paths.ts +86 -1
  148. package/src/codex/provider-routing.ts +174 -0
  149. package/src/codex/refresh.ts +2 -2
  150. package/src/codex/routing/health-store.ts +1 -1
  151. package/src/codex/routing/selection.ts +4 -3
  152. package/src/codex/routing/thread-affinity.ts +1 -1
  153. package/src/codex/signed-in-identity.ts +43 -0
  154. package/src/codex/subagent-model-fallback.ts +1 -1
  155. package/src/codex/sync.ts +6 -1
  156. package/src/codex/user-identity.ts +16 -5
  157. package/src/codex/windows-local-appdata.ts +84 -0
  158. package/src/combos/failover.ts +6 -1
  159. package/src/combos/index.ts +1 -0
  160. package/src/combos/resolve.ts +46 -10
  161. package/src/combos/types.ts +15 -0
  162. package/src/config/diagnostics.ts +33 -6
  163. package/src/config/live-reconcile.ts +38 -2
  164. package/src/config/load-degrade.ts +17 -0
  165. package/src/config/opencode-wire-upgrade.ts +54 -0
  166. package/src/config/opencode-zen-upgrade.ts +52 -0
  167. package/src/config/owner-registry.ts +22 -17
  168. package/src/config/provider-validation.ts +45 -2
  169. package/src/config/schema/config-schema.ts +22 -0
  170. package/src/config/schema/leaf-validators.ts +38 -9
  171. package/src/config/schema/main-account-hard-lock-fallback.ts +26 -0
  172. package/src/config/schema/native-reasoning-retention.ts +26 -0
  173. package/src/config.ts +2 -0
  174. package/src/generated/compatibility-version.json +608 -284
  175. package/src/grok/sync.ts +1 -1
  176. package/src/images/loop.ts +46 -8
  177. package/src/integrations/catalog-refresh.ts +1 -1
  178. package/src/integrations/mutation-plan.ts +1 -0
  179. package/src/integrations/registry.ts +8 -0
  180. package/src/lib/codex-restart-contract.ts +15 -0
  181. package/src/lib/desktop-upstream-tunnel.ts +18 -53
  182. package/src/lib/empty-tool-catalog.ts +62 -0
  183. package/src/lib/errors.ts +19 -3
  184. package/src/lib/gcp-adc.ts +96 -9
  185. package/src/lib/gcp-credential-input.ts +12 -0
  186. package/src/lib/lab-live-route-production.ts +5 -0
  187. package/src/lib/opaque-proxy-tunnel.ts +217 -0
  188. package/src/lib/redact.ts +2 -2
  189. package/src/oauth/account-quota-rank.ts +69 -2
  190. package/src/oauth/anthropic-account-refusal.ts +20 -2
  191. package/src/oauth/anthropic-oauth-definitions.ts +3 -1
  192. package/src/oauth/anthropic-routing.ts +1 -21
  193. package/src/oauth/anthropic.ts +38 -7
  194. package/src/oauth/callback-server.ts +27 -1
  195. package/src/oauth/chatgpt-device.ts +1 -1
  196. package/src/oauth/chatgpt-shared.ts +83 -0
  197. package/src/oauth/chatgpt.ts +8 -72
  198. package/src/oauth/dsh.ts +203 -0
  199. package/src/oauth/generic-account-failover.ts +33 -7
  200. package/src/oauth/generic-session-affinity.ts +125 -0
  201. package/src/oauth/index.ts +184 -109
  202. package/src/oauth/local-token-detect.ts +21 -1
  203. package/src/oauth/login-cli-errors.ts +44 -0
  204. package/src/oauth/login-cli.ts +69 -7
  205. package/src/oauth/login-flow-state.ts +371 -23
  206. package/src/oauth/meta-muse-errors.ts +41 -0
  207. package/src/oauth/meta-muse-keychain.ts +52 -0
  208. package/src/oauth/meta-muse.ts +51 -84
  209. package/src/oauth/model-discovery-auth.ts +3 -0
  210. package/src/oauth/pool-settings-capability.ts +4 -2
  211. package/src/oauth/refresh-flight-retirement.ts +48 -0
  212. package/src/oauth/session-affinity-key.ts +23 -0
  213. package/src/oauth/store-anthropic-instance.ts +9 -2
  214. package/src/oauth/store.ts +12 -5
  215. package/src/protocols/encoders/adapter-events.ts +5 -4
  216. package/src/protocols/encoders/messages.ts +9 -6
  217. package/src/protocols/provider-summary.ts +2 -1
  218. package/src/providers/api-key-cooldowns.ts +49 -0
  219. package/src/providers/api-key-resolve.ts +11 -6
  220. package/src/providers/api-key-response-ownership.ts +57 -0
  221. package/src/providers/api-key-selection.ts +31 -6
  222. package/src/providers/api-keys.ts +214 -3
  223. package/src/providers/claude-cli-usage.ts +96 -0
  224. package/src/providers/dsh-account-destination.ts +31 -0
  225. package/src/providers/dsh-account-models.ts +41 -0
  226. package/src/providers/gcp-credential-cleanup.ts +59 -0
  227. package/src/providers/key-failover.ts +54 -50
  228. package/src/providers/key-store.ts +7 -2
  229. package/src/providers/model-rename-fields.ts +3 -0
  230. package/src/providers/model-rename-migration.ts +2 -2
  231. package/src/providers/openai-tiers-destination.ts +11 -2
  232. package/src/providers/openai-tiers.ts +5 -12
  233. package/src/providers/opencode-go-transport.ts +3 -1
  234. package/src/providers/quota/account-cache.ts +6 -5
  235. package/src/providers/quota/account-row.ts +34 -0
  236. package/src/providers/quota/antigravity.ts +28 -7
  237. package/src/providers/quota/dsh-account.ts +70 -0
  238. package/src/providers/quota/vendor-probes-oauth.ts +1 -93
  239. package/src/providers/quota-types.ts +2 -0
  240. package/src/providers/quota.ts +14 -20
  241. package/src/providers/registry/entries-core.ts +37 -8
  242. package/src/providers/registry/types.ts +1 -1
  243. package/src/providers/request-pacing.ts +77 -19
  244. package/src/providers/routed-config.ts +339 -0
  245. package/src/providers/service-tier.ts +2 -1
  246. package/src/quota/window-mapping.ts +5 -2
  247. package/src/responses/anthropic-progress.ts +34 -0
  248. package/src/responses/assistant-replay.ts +35 -0
  249. package/src/responses/compaction-history-framing.ts +65 -0
  250. package/src/responses/compaction.ts +6 -1
  251. package/src/responses/incomplete-reason.ts +31 -0
  252. package/src/responses/parser.ts +50 -5
  253. package/src/responses/reasoning-envelope.ts +4 -0
  254. package/src/responses/reasoning-replay-cache.ts +24 -2
  255. package/src/responses/reasoning-retention.ts +173 -0
  256. package/src/responses/retained-compaction.ts +46 -0
  257. package/src/responses/schema.ts +23 -0
  258. package/src/router.ts +8 -321
  259. package/src/routing/health.ts +37 -22
  260. package/src/routing/history/indexer.ts +6 -5
  261. package/src/routing/history/ingest.ts +6 -2
  262. package/src/routing/profile.ts +1 -1
  263. package/src/server/admission-model-scope.ts +17 -0
  264. package/src/server/auth-cors.ts +11 -0
  265. package/src/server/chat-completions.ts +7 -1
  266. package/src/server/chat-native-reasoning.ts +123 -0
  267. package/src/server/chat-native-sse.ts +110 -7
  268. package/src/server/chat-native.ts +76 -17
  269. package/src/server/claude-messages.ts +55 -414
  270. package/src/server/effort-policy.ts +1 -1
  271. package/src/server/images.ts +15 -2
  272. package/src/server/index/model-alias-id.ts +36 -0
  273. package/src/server/index/optional-listeners.ts +2 -0
  274. package/src/server/index/serve-options.ts +9 -1
  275. package/src/server/index/startup-warnings.ts +2 -0
  276. package/src/server/index.ts +1 -1
  277. package/src/server/inference/advisor-diagnostics.ts +31 -0
  278. package/src/server/management/advisor-routes.ts +15 -4
  279. package/src/server/management/claude-desktop-picker-routes.ts +1 -1
  280. package/src/server/management/combo-routes.ts +176 -50
  281. package/src/server/management/companion-routes.ts +1 -1
  282. package/src/server/management/config-routes.ts +75 -4
  283. package/src/server/management/cursor-integration-routes.ts +1 -1
  284. package/src/server/management/model-routes.ts +11 -6
  285. package/src/server/management/model-rows.ts +14 -0
  286. package/src/server/management/native-integration-routes.ts +2 -7
  287. package/src/server/management/native-reasoning-retention-routes.ts +41 -0
  288. package/src/server/management/oauth-account-routes.ts +87 -14
  289. package/src/server/management/provider-gcp-credential-routes.ts +29 -0
  290. package/src/server/management/provider-key-pacing-routes.ts +31 -0
  291. package/src/server/management/provider-overwrite-carry.ts +7 -0
  292. package/src/server/management/provider-routes.ts +25 -30
  293. package/src/server/management/route-registry.ts +10 -1
  294. package/src/server/management-api.ts +3 -1
  295. package/src/server/messages-native-tool-guard.ts +185 -0
  296. package/src/server/messages-native.ts +101 -21
  297. package/src/server/messages-shared.ts +431 -0
  298. package/src/server/relay.ts +1 -1
  299. package/src/server/request-log-terminal-status.ts +1 -1
  300. package/src/server/request-log.ts +6 -1
  301. package/src/server/responses/account-change-state.ts +8 -2
  302. package/src/server/responses/adapter-continuation.ts +13 -1
  303. package/src/server/responses/adapter-delivery.ts +19 -6
  304. package/src/server/responses/adapter-dispatch.ts +19 -2
  305. package/src/server/responses/advisor-retention.ts +117 -0
  306. package/src/server/responses/advisor-slot.ts +73 -41
  307. package/src/server/responses/codex-ws-wire.ts +15 -8
  308. package/src/server/responses/compact.ts +303 -125
  309. package/src/server/responses/compaction-recovery.ts +18 -10
  310. package/src/server/responses/compaction-retention.ts +61 -0
  311. package/src/server/responses/context-overflow.ts +11 -6
  312. package/src/server/responses/core-auth.ts +6 -0
  313. package/src/server/responses/core-codex-account.ts +11 -1
  314. package/src/server/responses/core-combo-failure.ts +2 -0
  315. package/src/server/responses/core-combo.ts +120 -36
  316. package/src/server/responses/core-errors.ts +3 -3
  317. package/src/server/responses/core-lifetime.ts +5 -2
  318. package/src/server/responses/core-opaque-recovery.ts +35 -0
  319. package/src/server/responses/core-options.ts +5 -0
  320. package/src/server/responses/core-pipeline.ts +141 -0
  321. package/src/server/responses/core-replay.ts +18 -2
  322. package/src/server/responses/core.ts +2 -115
  323. package/src/server/responses/fetch-helpers.ts +73 -19
  324. package/src/server/responses/main-hard-lock-routing.ts +109 -0
  325. package/src/server/responses/native-response-control.ts +4 -2
  326. package/src/server/responses/passthrough-delivery.ts +3 -1
  327. package/src/server/responses/passthrough-dispatch.ts +15 -13
  328. package/src/server/responses/request-prepare.ts +76 -12
  329. package/src/server/responses/request-sidecar-auth.ts +2 -2
  330. package/src/server/responses/request-transport.ts +104 -27
  331. package/src/server/responses/response-effects.ts +1 -0
  332. package/src/server/responses/run-turn-execution.ts +96 -30
  333. package/src/server/responses/sidecar-execution.ts +20 -2
  334. package/src/server/responses/terminal-guard.ts +2 -16
  335. package/src/service/auth-preflight.ts +71 -0
  336. package/src/service/cli.ts +4 -2
  337. package/src/service/definition.ts +97 -0
  338. package/src/service/guards.ts +1 -61
  339. package/src/service/health-sentinel-linux.ts +112 -0
  340. package/src/service/health-sentinel-main.ts +76 -57
  341. package/src/service/health.ts +5 -89
  342. package/src/service/launchd.ts +27 -7
  343. package/src/service/logging.ts +72 -9
  344. package/src/service/repair.ts +2 -1
  345. package/src/service/state.ts +7 -8
  346. package/src/service/systemd.ts +1 -1
  347. package/src/service/windows-guarded-manager.ts +2 -1
  348. package/src/service/windows-ops.ts +1 -1
  349. package/src/service/windows-scheduler.ts +2 -7
  350. package/src/service/windows-taskxml.ts +2 -2
  351. package/src/service/windows-wscript.ts +14 -0
  352. package/src/service.ts +4 -2
  353. package/src/sidecar/auth.ts +2 -0
  354. package/src/types/config.ts +64 -3
  355. package/src/types/opencode-go.ts +61 -0
  356. package/src/types/opencode-zen.ts +29 -0
  357. package/src/types/provider.ts +32 -6
  358. package/src/types/request.ts +23 -2
  359. package/src/types/wire.ts +29 -37
  360. package/src/types.ts +1 -0
  361. package/src/update/npm-invocation.mjs +12 -2
  362. package/src/usage/cost.ts +48 -12
  363. package/src/usage/expected-prices.ts +3 -0
  364. package/src/usage/ledger-retention.ts +2 -1
  365. package/src/usage/ledger-scanner.ts +6 -5
  366. package/src/usage/log.ts +17 -8
  367. package/src/usage/summary.ts +22 -9
  368. package/src/usage/user-cost-overlays.ts +14 -19
  369. package/src/usage/user-cost-validation.ts +38 -0
  370. package/src/vision/eligibility.ts +13 -0
  371. package/src/web-search/loop.ts +55 -5
  372. package/src/web-search/progress-stream.ts +4 -1
  373. package/src/web-search/run-turn-loop.ts +9 -1
  374. package/gui/dist/assets/App-BjlttVLy.js +0 -58
  375. package/gui/dist/assets/App-CSJvmjNs.css +0 -1
  376. package/gui/dist/assets/index-BB6uK6tU.css +0 -1
  377. package/gui/dist/assets/index-BMssP4k3.js +0 -94
  378. package/gui/dist/assets/quota-summary-D8M1bc_R.js +0 -1
@@ -21,7 +21,7 @@ import {
21
21
  clampImageMaxRounds,
22
22
  } from "../../images";
23
23
  import type { ProviderAdapter } from "../../adapters/base";
24
- import type { OcxParsedRequest } from "../../types";
24
+ import type { OcxParsedRequest, OcxUsage } from "../../types";
25
25
  import { rotateProviderTransportOn429, rateLimitRetryPolicyFor } from "../../providers/key-failover";
26
26
  import {
27
27
  isGenericOAuthFailoverEnabled,
@@ -50,6 +50,7 @@ import { providerFetch } from "./fetch-helpers";
50
50
  import type { AttemptRecoveryKind } from "../../usage/log";
51
51
  import { recordAdapterReasoning, recordAdapterTier } from "../request-log";
52
52
  import { normalizeLogConversationId } from "../request-log-conversation";
53
+ import { attemptAccountChanged } from "../request-log-account-rotation";
53
54
  import { rememberResponseState } from "../../responses/state";
54
55
  import { trackStreamLifetime } from "../lifecycle";
55
56
  import { isAntigravityRawValidationRefusal } from "./antigravity-validation-refusal";
@@ -65,6 +66,7 @@ export async function executeResponsesSidecars(
65
66
  | "inboundWire"
66
67
  | "selectedForwardHeaders"
67
68
  | "translatorBudget"
69
+ | "clientToolCatalogRestriction"
68
70
  | "toolBridgeMaps"
69
71
  | "rememberKiroDeliveredFinalAnswer"
70
72
  | "responseStateOptions"
@@ -417,7 +419,8 @@ export async function executeResponsesSidecars(
417
419
  hop.permit?.release();
418
420
  return null;
419
421
  }
420
- recoveryKind = originalResponse?.status === 401 ? "oauth-401" : "anthropic-oauth-429";
422
+ recoveryKind = originalResponse?.status === 401 ? "oauth-401"
423
+ : originalResponse?.status === 403 ? "oauth-account-403" : "anthropic-oauth-429";
421
424
  sidecarBudget.ownCredentialHop(hop.permit);
422
425
  } else {
423
426
  // No key pool, no generic OAuth roster, no Anthropic pool could produce a replacement
@@ -451,6 +454,15 @@ export async function executeResponsesSidecars(
451
454
  });
452
455
  return { adapter: rotatedAdapter, recoveryKind };
453
456
  };
457
+ // A loop's hidden rounds before a rotation were spent by the account it left. Bill them to that
458
+ // account's attempt while it is still open; the next send seals it. Declined when that send
459
+ // keeps the account (no new attempt), so the loop's final total still carries them.
460
+ const billRotatedUsage = (usage: OcxUsage): boolean => {
461
+ const attempt = logCtx.activeAttempt;
462
+ if (!attempt || !attemptAccountChanged(attempt.accountLogLabel, logCtx.accountLogLabel, attempt.provider, logCtx.provider)) return false;
463
+ transportState.bindKeyUsageFromBridge(usage);
464
+ return true;
465
+ };
454
466
  if ((imgPlan || vidPlan) && wsPlan) {
455
467
  // Web search takes priority when both are active — the media bridge cannot run
456
468
  // alongside runWithWebSearch. Surface a runtime signal so the user knows their
@@ -508,6 +520,7 @@ export async function executeResponsesSidecars(
508
520
  );
509
521
  const imgResponse = await runWithImageBridge({
510
522
  parsed, adapter: transportState.adapter,
523
+ clientToolCatalogRestriction: route.provider.authMode === "forward" ? undefined : requestState.clientToolCatalogRestriction,
511
524
  incomingMeta: { headers: requestState.selectedForwardHeaders, providerName: route.providerName, abortSignal: options.abortSignal, translatorBudget, ...(options.comboInitialSend ? { providerFetch: imageProviderFetch } : {}), ...sidecarBudget.incomingMeta },
512
525
  onProducerStart: sidecarBudget.takeProducerOwnership,
513
526
  onProducerEnd: sidecarBudget.release,
@@ -549,10 +562,12 @@ export async function executeResponsesSidecars(
549
562
  transportState.bindKeyUsageFromBridge(usage);
550
563
  },
551
564
  on429: rotateSidecarProviderOn429,
565
+ onRotatedUsage: billRotatedUsage,
552
566
  retryOn429Policy: route.providerName === "kiro" && isGenericOAuthFailoverEnabled(config, "kiro")
553
567
  ? null : rateLimitRetryPolicyFor(route.provider),
554
568
  onFirstOutput: noteSidecarOutput,
555
569
  ...(options.forceEmptyResponseId ? { forceEmptyResponseId: true } : {}),
570
+ terminalRefusalAsFailed: inboundWire !== "chat" && inboundWire !== "anthropic",
556
571
  onCompletedResponse: (response, providerState) => {
557
572
  const served = transportState.replayOAuthCredentialSnapshot;
558
573
  if (route.providerName === "kiro" && response.status === "completed" && served
@@ -603,6 +618,7 @@ export async function executeResponsesSidecars(
603
618
  })(input, init)) as typeof globalThis.fetch;
604
619
  const loopResponse = await runWithWebSearch({
605
620
  parsed, adapter: transportState.adapter,
621
+ clientToolCatalogRestriction: route.provider.authMode === "forward" ? undefined : requestState.clientToolCatalogRestriction,
606
622
  onIterationEnd: sidecarBudget.releaseUnsentHop,
607
623
  fetchForRequest: (request, iterParsed, adapter) => providerFetch(route.provider, options.codexWsRuntimeIdentity, {
608
624
  dispatchOverride: oauthDispatch(request, iterParsed),
@@ -633,6 +649,7 @@ export async function executeResponsesSidecars(
633
649
  settings: wsPlan.settings,
634
650
  maxSearches: wsPlan.maxSearches,
635
651
  forceEmptyResponseId: true,
652
+ terminalRefusalAsFailed: inboundWire !== "chat" && inboundWire !== "anthropic",
636
653
  abortSignal: options.abortSignal,
637
654
  onFirstOutput: noteSidecarOutput,
638
655
  onRequestBuilt: request => {
@@ -651,6 +668,7 @@ export async function executeResponsesSidecars(
651
668
  stallTimeoutSec: wsPlan.stallTimeoutSec,
652
669
  streamRoutedModelOutput: wsPlan.streamRoutedModelOutput,
653
670
  on429: rotateSidecarProviderOn429,
671
+ onRotatedUsage: billRotatedUsage,
654
672
  ...(!options.comboInitialSend && route.providerName === "google-antigravity" ? { onIterationEnd: () => {
655
673
  // The next Antigravity adapter dispatch claims this permit. If request construction,
656
674
  // admission or cancellation stopped it first, return the unused send allowance.
@@ -1,6 +1,6 @@
1
+ import { assistantReplayContent } from "../../responses/assistant-replay";
1
2
  import type {
2
3
  AdapterEvent,
3
- OcxAssistantContentPart,
4
4
  OcxAssistantMessage,
5
5
  OcxParsedRequest,
6
6
  OcxUsage,
@@ -121,21 +121,7 @@ export function analyzeTerminalTurn(parsed: OcxParsedRequest, events: readonly A
121
121
  }
122
122
 
123
123
  function assistantMessageFromEvents(events: readonly AdapterEvent[], timestamp: number): OcxAssistantMessage | undefined {
124
- let text = "";
125
- let thinking = "";
126
- let signature: string | undefined;
127
- const redacted: string[] = [];
128
- for (const event of events) {
129
- if (event.type === "text_delta") text += event.text;
130
- else if (event.type === "thinking_delta") thinking += event.thinking;
131
- else if (event.type === "thinking_signature") signature = event.signature;
132
- else if (event.type === "redacted_thinking") redacted.push(event.data);
133
- }
134
- const content: OcxAssistantContentPart[] = [];
135
- if (thinking || signature || redacted.length > 0) {
136
- content.push({ type: "thinking", thinking, ...(signature ? { signature } : {}), ...(redacted.length > 0 ? { redacted } : {}) });
137
- }
138
- if (text) content.push({ type: "text", text });
124
+ const content = assistantReplayContent(events);
139
125
  if (content.length === 0) return undefined;
140
126
  return { role: "assistant", content, timestamp };
141
127
  }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * The install/repair preflight over the data-plane credential, plus the retry command it
3
+ * names. Split out of `guards.ts` so the guard module stops importing `diagnostics`.
4
+ *
5
+ * Direction of the remaining edge is deliberate: this module asks `guards` for
6
+ * `assertNotAdminToken` and `diagnostics` for the install verdict, and neither imports it back.
7
+ */
8
+ import { loadConfig } from "../config";
9
+ import { isLoopbackHostname } from "../codex/loopback-target";
10
+ import { readServiceApiTokenState, serviceApiTokenFilePath } from "../lib/service-secrets";
11
+ import { diagnoseService } from "./diagnostics";
12
+ import type { ServiceDiagnostic } from "./diagnostics";
13
+ import { assertNotAdminToken } from "./guards";
14
+
15
+ /**
16
+ * The `ocx` command a user should rerun for the service state they actually have.
17
+ *
18
+ * `installed` alone is not enough: `repairService()` refuses a Task-Scheduler-plus-WinSW
19
+ * conflict outright, so recommending repair there names a command guaranteed to fail.
20
+ * Install IS the valid conflict recovery, because `installWindows` removes the native
21
+ * backend first. Exported so the guard tests the real selector rather than a copy of it.
22
+ */
23
+ export function serviceRetryCommand(
24
+ diag: Pick<ServiceDiagnostic, "installed" | "conflict"> = diagnoseService(),
25
+ ): string {
26
+ return diag.installed && !diag.conflict ? "ocx service repair" : "ocx service install";
27
+ }
28
+
29
+ /**
30
+ * Preflight for `service install` / `service repair` on the data-plane credential.
31
+ *
32
+ * It used to DEMAND `OPENCODEX_API_AUTH_TOKEN` for a non-loopback hostname, and it threw
33
+ * even when `~/.opencodex/service-api-token` already held a perfectly good token. That is
34
+ * the defect behind the incident this unit exists to close (#4236): an operator exported the
35
+ * ADMIN token as OPENCODEX_API_AUTH_TOKEN because `install` asked for a token, the hub then
36
+ * crash-looped on `assertNotAdminToken`, and `service repair` asked for the same env var
37
+ * again — so the only remembered way to make the command proceed was the thing that broke it.
38
+ *
39
+ * Nobody should have to export a token by hand to run a hub. {@link writeServiceApiTokenFile}
40
+ * provisions one, so the only conditions left that install cannot fix are an admin-token
41
+ * collision in the environment and a token file that exists but cannot be used.
42
+ */
43
+ export function assertServiceAuthEnvironment(): void {
44
+ const config = loadConfig();
45
+ // Both collision checks come BEFORE the loopback short-circuit, because the launch wrapper
46
+ // exports the token file unconditionally (`buildServiceShellCommand` cats it whenever it
47
+ // exists, whatever the hostname): a management token in either source fences the whole
48
+ // management plane closed at boot, even on a loopback install that needs no admission
49
+ // secret. Returning early is what let that broken state through.
50
+ const present = process.env.OPENCODEX_API_AUTH_TOKEN?.trim();
51
+ if (present) assertNotAdminToken(present);
52
+ const state = readServiceApiTokenState();
53
+ // An existing FILE holding the admin token is the incident shape itself, and the first round
54
+ // only checked the env var — so install/repair reused it and the hub crash-looped at boot.
55
+ // On a machine connected to a hub this same file holds that hub's issued client key, which
56
+ // is never a management token, so the check is a no-op there.
57
+ if (state.kind === "present") assertNotAdminToken(state.token, process.env, "file");
58
+ if (isLoopbackHostname(config.hostname)) return;
59
+ if (present) return;
60
+ // Absent is fine — install/repair generates one below. `unsafe` is not: the writer refuses
61
+ // to replace a path it cannot vouch for, so say so here, where the operator can still act,
62
+ // instead of failing mid-install. Reached from `service repair` as well as `install`, so
63
+ // name a command that can actually succeed (see serviceRetryCommand).
64
+ if (state.kind !== "unsafe") return;
65
+ const diag = diagnoseService();
66
+ throw new Error(
67
+ `The data-plane token file cannot be used (${state.reason}): ${serviceApiTokenFilePath()}. `
68
+ + `Move it aside, then rerun \`${serviceRetryCommand(diag)}\`; the service provisions a `
69
+ + "fresh owner-only token and needs nothing from the environment.",
70
+ );
71
+ }
@@ -11,8 +11,10 @@ import { statusWinswRaw, type WinswStatus } from "../lib/winsw";
11
11
  import { withWindowsServiceMutationLock } from "../lib/windows-service-mutation-lock";
12
12
  import { maybeShowStarPrompt } from "../cli/star-prompt";
13
13
  import { serviceStatusReport } from "./diagnostics";
14
- import { assertServiceEnvironmentMatchesInstall, assertServiceAuthEnvironment } from "./guards";
15
- import { resolveServiceListenPort, reportServiceServing } from "./health";
14
+ import { assertServiceAuthEnvironment } from "./auth-preflight";
15
+ import { assertServiceEnvironmentMatchesInstall } from "./guards";
16
+ import { resolveServiceListenPort } from "./definition";
17
+ import { reportServiceServing } from "./health";
16
18
  import { platformOps, proxyStillLiveAfterStop, stopTrackedProxyForServiceCommand, installServiceSafely, installFreshWindowsSchedulerSafely, removeServiceInstallState, isServiceInstalled } from "./orchestration";
17
19
  import { repairService } from "./repair";
18
20
  import type { ServiceRepairVerb } from "./repair";
@@ -0,0 +1,97 @@
1
+ /**
2
+ * The shape of a service definition: the shell command both Unix backends bake into
3
+ * their unit/plist, the port they pin into it, and the proxy environment it carries.
4
+ *
5
+ * Leaf module. It reads config and the token-file path, and asks nothing of the
6
+ * backends that consume it — importing it from `launchd.ts`/`systemd.ts` cannot
7
+ * route back through them (see `installedServiceListenPort` in `health.ts`, which
8
+ * intentionally stays behind because it asks every backend).
9
+ */
10
+ import { loadConfig } from "../config";
11
+ import { serviceApiTokenFilePath } from "../lib/service-secrets";
12
+ import { PROXY_ENV_KEYS } from "../lib/proxy-env";
13
+
14
+ export function shellQuote(value: string): string {
15
+ return `'${value.replace(/'/g, "'\\''")}'`;
16
+ }
17
+
18
+ /**
19
+ * Listen port baked into service wrappers / WinSW XML.
20
+ * Priority: explicit override → OCX_BAKE_PORT (update restart) → config.port → 10100.
21
+ * `config.port === 0` means ephemeral for interactive start; services need a stable pin,
22
+ * so treat 0 / invalid like unset (default 10100) instead of baking `--port 0`.
23
+ */
24
+ export function resolveServiceListenPort(override?: number): number {
25
+ if (typeof override === "number" && Number.isFinite(override) && override > 0 && override <= 65535) {
26
+ return Math.trunc(override);
27
+ }
28
+ const baked = process.env.OCX_BAKE_PORT?.trim();
29
+ if (baked && /^\d+$/.test(baked)) {
30
+ const n = Number(baked);
31
+ if (n > 0 && n <= 65535) return n;
32
+ }
33
+ const configured = loadConfig().port;
34
+ if (typeof configured === "number" && configured > 0 && configured <= 65535) return configured;
35
+ return 10100;
36
+ }
37
+
38
+ export function buildServiceShellCommand(bun: string, cli: string | null, port = resolveServiceListenPort()): string {
39
+ const tokenFile = serviceApiTokenFilePath();
40
+ const args = cli ? `${shellQuote(cli)} start` : "start";
41
+ return `if [ -f ${shellQuote(tokenFile)} ]; then OPENCODEX_API_AUTH_TOKEN="$(cat ${shellQuote(tokenFile)})"; export OPENCODEX_API_AUTH_TOKEN; fi; exec ${shellQuote(bun)} ${args} --port ${port}`;
42
+ }
43
+
44
+ /**
45
+ * The same command shape, launched through a stable `ocx` executable instead of an
46
+ * explicit Bun + CLI pair. The token-file preamble is identical and deliberately shared
47
+ * in form: the service still reads the token from disk at start and never carries it in
48
+ * the unit.
49
+ */
50
+ export function buildServiceLauncherShellCommand(launcher: string, port = resolveServiceListenPort()): string {
51
+ const tokenFile = serviceApiTokenFilePath();
52
+ return `if [ -f ${shellQuote(tokenFile)} ]; then OPENCODEX_API_AUTH_TOKEN="$(cat ${shellQuote(tokenFile)})"; export OPENCODEX_API_AUTH_TOKEN; fi; exec ${shellQuote(launcher)} start --port ${port}`;
53
+ }
54
+
55
+ /**
56
+ * Shared tail parser for the baked `--port <n>`.
57
+ *
58
+ * Terminators cover all three artifact shapes: whitespace (batch wrapper, systemd
59
+ * unit), `"` (systemd's quoted ExecStart), `<` (WinSW's `</arguments>`), and `&` (an
60
+ * XML-escaped quote). Matched LAST because every artifact carries the Bun and CLI
61
+ * paths ahead of the argument, and a path containing the literal must not shadow it.
62
+ */
63
+ export function parseBakedListenPort(read: () => string): number | null {
64
+ try {
65
+ const last = [...read().matchAll(/start --port (\d{1,5})(?:\s|"|&|<|$)/gm)].at(-1);
66
+ if (!last) return null;
67
+ const n = Number(last[1]);
68
+ return n > 0 && n <= 65535 ? n : null;
69
+ } catch {
70
+ return null;
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Outbound proxy settings the installing shell had, resolved for baking into a service
76
+ * definition.
77
+ *
78
+ * A service manager does not inherit the environment of the shell that installed it, and
79
+ * `ExecStart=/bin/sh -lc` is dash on Ubuntu/WSL — login dash reads `.profile`, not
80
+ * `.bashrc`, which is where proxy exports usually live. So a user who needs a proxy to
81
+ * reach the upstream got a service that dialed direct: the socket was reset, the retry
82
+ * budget drained, and the request surfaced as `502 Provider unreachable` (#2107). The
83
+ * same install driven through `ocx codex-shim` worked, because that path spawns with
84
+ * `{ ...process.env }`.
85
+ *
86
+ * Lower-case variants are honored because curl-style tooling sets them and the runtime's
87
+ * own `applyProxyEnv` already treats both cases as equivalent. Only the canonical
88
+ * upper-case name is baked, so a definition never carries two spellings of one setting.
89
+ */
90
+ export function resolvedProxyEnv(env: NodeJS.ProcessEnv = process.env): { name: string; value: string }[] {
91
+ const resolved: { name: string; value: string }[] = [];
92
+ for (const key of PROXY_ENV_KEYS) {
93
+ const value = env[key]?.trim() || env[key.toLowerCase()]?.trim();
94
+ if (value) resolved.push({ name: key, value });
95
+ }
96
+ return resolved;
97
+ }
@@ -2,14 +2,12 @@ import { execSync } from "node:child_process";
2
2
  import { chmodSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
3
3
  import { getConfigDir, loadConfig } from "../config";
4
4
  import { withConfigMutationLockSync } from "../config/mutation-lock";
5
- import { hardenReusedServiceApiToken, readServiceApiTokenState, serviceApiTokenFilePath } from "../lib/service-secrets";
5
+ import { hardenReusedServiceApiToken, serviceApiTokenFilePath } from "../lib/service-secrets";
6
6
  import { tokenCollidesWithAdmin } from "../lib/admin-secrets";
7
7
  import { randomBytes } from "node:crypto";
8
8
  import { hardenSecretDir, hardenSecretPath } from "../lib/windows-secret-acl";
9
9
  import { recordOwnedConfigPath } from "../lib/config-ownership";
10
10
  import { isTestHomeGuardArmed } from "../lib/test-home-guard";
11
- import { diagnoseService } from "./diagnostics";
12
- import type { ServiceDiagnostic } from "./diagnostics";
13
11
  import { currentCodexHome, currentOpenCodexHome, resolveServiceState, serviceCodexHomeMatchesInstall, servicePathMatchesInstall } from "./state";
14
12
  import { resolveCodexSqliteHome } from "../codex/paths";
15
13
  import type { CodexHomeDeps } from "../codex/home";
@@ -76,20 +74,6 @@ export function assertServiceEnvironmentMatchesInstall(deps: CodexHomeDeps = {})
76
74
  }
77
75
  }
78
76
 
79
- /**
80
- * The `ocx` command a user should rerun for the service state they actually have.
81
- *
82
- * `installed` alone is not enough: `repairService()` refuses a Task-Scheduler-plus-WinSW
83
- * conflict outright, so recommending repair there names a command guaranteed to fail.
84
- * Install IS the valid conflict recovery, because `installWindows` removes the native
85
- * backend first. Exported so the guard tests the real selector rather than a copy of it.
86
- */
87
- export function serviceRetryCommand(
88
- diag: Pick<ServiceDiagnostic, "installed" | "conflict"> = diagnoseService(),
89
- ): string {
90
- return diag.installed && !diag.conflict ? "ocx service repair" : "ocx service install";
91
- }
92
-
93
77
  /**
94
78
  * Refuse a management (admin) token as the data-plane secret.
95
79
  *
@@ -139,50 +123,6 @@ export function assertNotAdminToken(
139
123
  );
140
124
  }
141
125
 
142
- /**
143
- * Preflight for `service install` / `service repair` on the data-plane credential.
144
- *
145
- * It used to DEMAND `OPENCODEX_API_AUTH_TOKEN` for a non-loopback hostname, and it threw
146
- * even when `~/.opencodex/service-api-token` already held a perfectly good token. That is
147
- * the defect behind the incident this unit exists to close (#4236): an operator exported the
148
- * ADMIN token as OPENCODEX_API_AUTH_TOKEN because `install` asked for a token, the hub then
149
- * crash-looped on `assertNotAdminToken`, and `service repair` asked for the same env var
150
- * again — so the only remembered way to make the command proceed was the thing that broke it.
151
- *
152
- * Nobody should have to export a token by hand to run a hub. {@link writeServiceApiTokenFile}
153
- * provisions one, so the only conditions left that install cannot fix are an admin-token
154
- * collision in the environment and a token file that exists but cannot be used.
155
- */
156
- export function assertServiceAuthEnvironment(): void {
157
- const config = loadConfig();
158
- // Both collision checks come BEFORE the loopback short-circuit, because the launch wrapper
159
- // exports the token file unconditionally (`buildServiceShellCommand` cats it whenever it
160
- // exists, whatever the hostname): a management token in either source fences the whole
161
- // management plane closed at boot, even on a loopback install that needs no admission
162
- // secret. Returning early is what let that broken state through.
163
- const present = process.env.OPENCODEX_API_AUTH_TOKEN?.trim();
164
- if (present) assertNotAdminToken(present);
165
- const state = readServiceApiTokenState();
166
- // An existing FILE holding the admin token is the incident shape itself, and the first round
167
- // only checked the env var — so install/repair reused it and the hub crash-looped at boot.
168
- // On a machine connected to a hub this same file holds that hub's issued client key, which
169
- // is never a management token, so the check is a no-op there.
170
- if (state.kind === "present") assertNotAdminToken(state.token, process.env, "file");
171
- if (isLoopbackHostname(config.hostname)) return;
172
- if (present) return;
173
- // Absent is fine — install/repair generates one below. `unsafe` is not: the writer refuses
174
- // to replace a path it cannot vouch for, so say so here, where the operator can still act,
175
- // instead of failing mid-install. Reached from `service repair` as well as `install`, so
176
- // name a command that can actually succeed (see serviceRetryCommand).
177
- if (state.kind !== "unsafe") return;
178
- const diag = diagnoseService();
179
- throw new Error(
180
- `The data-plane token file cannot be used (${state.reason}): ${serviceApiTokenFilePath()}. `
181
- + `Move it aside, then rerun \`${serviceRetryCommand(diag)}\`; the service provisions a `
182
- + "fresh owner-only token and needs nothing from the environment.",
183
- );
184
- }
185
-
186
126
  /** How the data-plane token the service will export was obtained. */
187
127
  export type ServiceApiTokenOrigin = "env" | "file" | "generated";
188
128
 
@@ -0,0 +1,112 @@
1
+ /** Linux pidfd ownership for the opt-in sentinel. Loaded only on Linux. */
2
+ import { dlopen, FFIType, read } from "bun:ffi";
3
+ import type { TerminateResult } from "./health-sentinel-main";
4
+
5
+ const ESRCH = 3;
6
+ const ENOSYS = 38;
7
+ // Syscalls from 424 onward share one number on every Linux architecture. Binding is limited to
8
+ // x64/arm64 because the FFI signature below assumes their LP64 integer-register calling convention.
9
+ const SYS_PIDFD_OPEN = 434;
10
+ const SYS_PIDFD_SEND_SIGNAL = 424;
11
+
12
+ type CallResult = { value: number; errno: number };
13
+ export type PidfdSyscalls = {
14
+ open(pid: number): CallResult;
15
+ signal(fd: number, signal: number): CallResult;
16
+ close(fd: number): void;
17
+ dispose(): void;
18
+ };
19
+ export type ParentPidfd = { terminate(): TerminateResult; close(): void };
20
+ export type ParentPidfdResult =
21
+ | { kind: "bound"; handle: ParentPidfd }
22
+ | { kind: "unsupported"; reason: string }
23
+ | { kind: "refused"; reason: string };
24
+
25
+ function nativeSyscalls(): PidfdSyscalls {
26
+ const symbols = {
27
+ // syscall is variadic, but these syscalls use integer/pointer arguments only. All supplied
28
+ // slots are machine-word integers (including zero for siginfo and flags), on these LP64 ABIs.
29
+ syscall: { args: [FFIType.i64, FFIType.i64, FFIType.i64, FFIType.i64, FFIType.i64], returns: FFIType.i64 },
30
+ __errno_location: { args: [], returns: FFIType.ptr },
31
+ close: { args: [FFIType.i32], returns: FFIType.i32 },
32
+ } as const;
33
+ const load = (path: string) => dlopen(path, symbols);
34
+ let libc: ReturnType<typeof load>;
35
+ try {
36
+ libc = load("libc.so.6");
37
+ } catch {
38
+ // musl exports libc symbols through its loader; it has no glibc soname or pidfd wrappers.
39
+ libc = load(`/lib/ld-musl-${process.arch === "x64" ? "x86_64" : "aarch64"}.so.1`);
40
+ }
41
+ const call = (nr: number, a: number, b: number, c = 0, d = 0): CallResult => {
42
+ const value = Number(libc.symbols.syscall(nr, a, b, c, d));
43
+ return { value, errno: value === -1 ? read.i32(libc.symbols.__errno_location()!) : 0 };
44
+ };
45
+ return {
46
+ open: pid => call(SYS_PIDFD_OPEN, pid, 0),
47
+ signal: (fd, signal) => call(SYS_PIDFD_SEND_SIGNAL, fd, signal),
48
+ // Never retry close on Linux: even an EINTR has released the descriptor.
49
+ close: fd => { libc.symbols.close(fd); },
50
+ dispose: () => libc.close(),
51
+ };
52
+ }
53
+
54
+ /** Acquire before arming; reparenting during acquisition invalidates even a successfully opened fd. */
55
+ export function openParentPidfd(
56
+ pid: number,
57
+ isParent: () => boolean,
58
+ options: { arch?: string; load?: () => PidfdSyscalls } = {},
59
+ ): ParentPidfdResult {
60
+ if (!isParent()) return { kind: "refused", reason: "the sentinel is not the watched process's child" };
61
+ const arch = options.arch ?? process.arch;
62
+ if (arch !== "x64" && arch !== "arm64") return { kind: "unsupported", reason: `pidfd syscall ABI is unsupported on ${arch}` };
63
+ let api: PidfdSyscalls;
64
+ try { api = (options.load ?? nativeSyscalls)(); }
65
+ catch { return { kind: "refused", reason: "Linux pidfd libc binding is unavailable" }; }
66
+ let fd: number | null = null;
67
+ let closed = false;
68
+ let signalled = false;
69
+ const close = () => {
70
+ if (closed) return;
71
+ closed = true;
72
+ try { if (fd !== null) api.close(fd); } finally { api.dispose(); }
73
+ };
74
+ const refuse = (reason: string): ParentPidfdResult => { close(); return { kind: "refused", reason }; };
75
+ try {
76
+ const opened = api.open(pid);
77
+ if (opened.value < 0) {
78
+ close();
79
+ return opened.errno === ENOSYS
80
+ ? { kind: "unsupported", reason: "pidfd_open is unsupported by this kernel" }
81
+ : { kind: "refused", reason: `pidfd_open failed (errno ${opened.errno})` };
82
+ }
83
+ fd = opened.value;
84
+ if (!isParent()) return refuse("the watched parent exited during pidfd acquisition");
85
+ // Check both syscall support and permission before arming, without sending a signal.
86
+ const probe = api.signal(fd, 0);
87
+ if (probe.value < 0) {
88
+ close();
89
+ return probe.errno === ENOSYS
90
+ ? { kind: "unsupported", reason: "pidfd_send_signal is unsupported by this kernel" }
91
+ : { kind: "refused", reason: `pidfd signal check failed (errno ${probe.errno})` };
92
+ }
93
+ if (!isParent()) return refuse("the watched parent exited during pidfd verification");
94
+ return {
95
+ kind: "bound",
96
+ handle: {
97
+ close,
98
+ terminate: () => {
99
+ if (closed || signalled) return "not_target";
100
+ // A throwing binding must settle the recorded intent as "failed", like process.kill does.
101
+ let sent: CallResult;
102
+ try { sent = api.signal(fd!, 9); } catch { return "failed"; }
103
+ if (sent.value < 0) return sent.errno === ESRCH ? "not_target" : "failed";
104
+ signalled = true;
105
+ return "terminated";
106
+ },
107
+ },
108
+ };
109
+ } catch {
110
+ return refuse("Linux pidfd binding failed");
111
+ }
112
+ }