@iislee/opencodex 2.31.0 → 2.35.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 (261) hide show
  1. package/README.md +3 -3
  2. package/bin/ocx.mjs +99 -70
  3. package/gui/dist/assets/index-Be1sDpWw.css +1 -0
  4. package/gui/dist/assets/index-D4OKgUV4.js +114 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +3 -1
  7. package/src/adapters/anthropic.ts +104 -15
  8. package/src/adapters/base.ts +29 -1
  9. package/src/adapters/command-code.ts +42 -6
  10. package/src/adapters/cursor/call-id.ts +44 -0
  11. package/src/adapters/cursor/catalog.ts +541 -0
  12. package/src/adapters/cursor/checkpoint-store.ts +15 -10
  13. package/src/adapters/cursor/cursor-errors.ts +15 -0
  14. package/src/adapters/cursor/discovery.ts +69 -18
  15. package/src/adapters/cursor/effort-map.ts +79 -1
  16. package/src/adapters/cursor/envelope-echo.ts +290 -0
  17. package/src/adapters/cursor/live-models.ts +7 -2
  18. package/src/adapters/cursor/live-transport.ts +14 -0
  19. package/src/adapters/cursor/message-mapper.ts +4 -1
  20. package/src/adapters/cursor/native-exec-desktop.ts +23 -0
  21. package/src/adapters/cursor/native-exec-fs.ts +2 -2
  22. package/src/adapters/cursor/native-exec-network.ts +1 -1
  23. package/src/adapters/cursor/native-exec-shell.ts +5 -2
  24. package/src/adapters/cursor/native-exec.ts +20 -0
  25. package/src/adapters/cursor/protobuf-request.ts +68 -14
  26. package/src/adapters/cursor/request-builder.ts +40 -10
  27. package/src/adapters/cursor/tool-definitions.ts +14 -4
  28. package/src/adapters/cursor/tool-result-normalize.ts +28 -5
  29. package/src/adapters/cursor/types.ts +20 -0
  30. package/src/adapters/cursor.ts +209 -38
  31. package/src/adapters/exec-tool-result-normalize.ts +99 -0
  32. package/src/adapters/google-antigravity-replay.ts +173 -25
  33. package/src/adapters/google-antigravity-wire.ts +5 -0
  34. package/src/adapters/google-errors.ts +44 -12
  35. package/src/adapters/google.ts +136 -24
  36. package/src/adapters/image.ts +1 -1
  37. package/src/adapters/kiro-constants.ts +27 -0
  38. package/src/adapters/kiro-tools.ts +55 -16
  39. package/src/adapters/kiro.ts +182 -23
  40. package/src/adapters/openai-chat.ts +300 -242
  41. package/src/adapters/openai-responses.ts +353 -27
  42. package/src/adapters/run-turn-queue.ts +36 -1
  43. package/src/adapters/tool-catalog-nudge.ts +4 -3
  44. package/src/adapters/xai-tool-schema.ts +436 -0
  45. package/src/adapters/xai-web-search.ts +17 -16
  46. package/src/bridge.ts +90 -37
  47. package/src/chat/inbound.ts +14 -1
  48. package/src/chat/outbound.ts +15 -7
  49. package/src/claude/agents-inject.ts +8 -1
  50. package/src/claude/context-windows.ts +16 -9
  51. package/src/claude/outbound.ts +24 -11
  52. package/src/cli/access.ts +46 -3
  53. package/src/cli/account-api.ts +116 -19
  54. package/src/cli/account-extended.ts +262 -29
  55. package/src/cli/account-main.ts +12 -12
  56. package/src/cli/account.ts +69 -15
  57. package/src/cli/agent.ts +8 -1
  58. package/src/cli/alias.ts +66 -0
  59. package/src/cli/capabilities-command.ts +94 -0
  60. package/src/cli/capabilities.ts +496 -0
  61. package/src/cli/claude-desktop.ts +31 -11
  62. package/src/cli/claude.ts +26 -1
  63. package/src/cli/dispatch.ts +256 -28
  64. package/src/cli/doctor.ts +126 -14
  65. package/src/cli/help.ts +17 -6
  66. package/src/cli/index.ts +39 -9
  67. package/src/cli/init.ts +1 -0
  68. package/src/cli/inspect.ts +230 -0
  69. package/src/cli/models-runtime.ts +95 -0
  70. package/src/cli/models.ts +26 -10
  71. package/src/cli/observe.ts +31 -8
  72. package/src/cli/provider-runtime.ts +16 -2
  73. package/src/cli/provider.ts +2 -1
  74. package/src/cli/registry.ts +49 -10
  75. package/src/cli/runtime-api.ts +51 -7
  76. package/src/cli/status.ts +41 -1
  77. package/src/cli/storage.ts +234 -0
  78. package/src/cli/system-command.ts +16 -0
  79. package/src/cli/system-restart-client.ts +1 -1
  80. package/src/cli/usage-report.ts +184 -0
  81. package/src/cli/v2.ts +34 -10
  82. package/src/cli/version-skew.ts +46 -0
  83. package/src/clients/config-export.ts +85 -3
  84. package/src/clients/effective-status.ts +30 -2
  85. package/src/clients/probes/cline.ts +81 -0
  86. package/src/codex/account-label.ts +21 -0
  87. package/src/codex/account-pause.ts +2 -1
  88. package/src/codex/account-priority.ts +3 -2
  89. package/src/codex/app-server-processes.ts +83 -7
  90. package/src/codex/auth-api.ts +50 -10
  91. package/src/codex/auth-context.ts +21 -18
  92. package/src/codex/autostart-health.ts +16 -0
  93. package/src/codex/catalog/aggregation.ts +18 -0
  94. package/src/codex/catalog/effort.ts +18 -3
  95. package/src/codex/catalog/metadata.ts +27 -1
  96. package/src/codex/catalog/native-models.ts +5 -2
  97. package/src/codex/catalog/parsing.ts +72 -27
  98. package/src/codex/catalog/provider-fetch.ts +187 -31
  99. package/src/codex/catalog/sync.ts +131 -5
  100. package/src/codex/catalog.ts +1 -1
  101. package/src/codex/codex-write-lock.ts +3 -1
  102. package/src/codex/convergence-types.ts +1 -1
  103. package/src/codex/convergence.ts +27 -2
  104. package/src/codex/desired-state.ts +2 -2
  105. package/src/codex/desktop-app-restart.ts +355 -0
  106. package/src/codex/history-job.ts +32 -3
  107. package/src/codex/history-manifest.ts +112 -0
  108. package/src/codex/history-migration-guardian.ts +5 -5
  109. package/src/codex/history-provider.ts +825 -247
  110. package/src/codex/history-worker.ts +8 -5
  111. package/src/codex/inject-coordination.ts +83 -0
  112. package/src/codex/inject.ts +63 -22
  113. package/src/codex/injected-marker.ts +1 -1
  114. package/src/codex/internal/history-writer.ts +4 -3
  115. package/src/codex/log-guard/inspect.ts +22 -4
  116. package/src/codex/model-entitlements.ts +9 -2
  117. package/src/codex/native-profile-startup.ts +157 -27
  118. package/src/codex/native-residue.ts +26 -33
  119. package/src/codex/prompt-layers.ts +371 -25
  120. package/src/codex/prompt-text-probe.ts +238 -0
  121. package/src/codex/quota.ts +91 -10
  122. package/src/codex/routing.ts +9 -0
  123. package/src/codex/shim.ts +56 -3
  124. package/src/codex/subagent-model-fallback.ts +104 -25
  125. package/src/codex/transition-state.ts +116 -8
  126. package/src/combos/failover.ts +27 -0
  127. package/src/combos/types.ts +10 -0
  128. package/src/compatibility/index.ts +26 -0
  129. package/src/compatibility/manifest.ts +253 -0
  130. package/src/compatibility/openai-responses.ts +121 -0
  131. package/src/config/atomic-write.ts +219 -0
  132. package/src/config/paths.ts +40 -0
  133. package/src/config/process-state.ts +309 -0
  134. package/src/config/provider-validation.ts +177 -0
  135. package/src/config/rebase-provenance.ts +68 -0
  136. package/src/config.ts +206 -824
  137. package/src/generated/compatibility-version.json +373 -213
  138. package/src/images/loop.ts +5 -4
  139. package/src/images/plan.ts +5 -4
  140. package/src/integrations/ownership-policy.ts +160 -0
  141. package/src/integrations/ownership.ts +46 -2
  142. package/src/integrations/registry.ts +7 -0
  143. package/src/integrations/state.ts +79 -7
  144. package/src/integrations/writer.ts +26 -2
  145. package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
  146. package/src/lab/fabric/producer-child.ts +1 -1
  147. package/src/lib/admin-secrets.ts +24 -0
  148. package/src/lib/bounded-body.ts +14 -2
  149. package/src/lib/config-ownership.ts +20 -0
  150. package/src/lib/errors.ts +36 -3
  151. package/src/lib/package-tree-integrity.ts +101 -0
  152. package/src/lib/process-control.ts +2 -1
  153. package/src/lib/service-secrets.ts +15 -0
  154. package/src/lib/tool-argument-integers.ts +56 -5
  155. package/src/oauth/callback-server.ts +22 -2
  156. package/src/oauth/cline.ts +203 -0
  157. package/src/oauth/generic-account-failover.ts +231 -0
  158. package/src/oauth/health.ts +1 -1
  159. package/src/oauth/index.ts +65 -6
  160. package/src/oauth/key-providers.ts +10 -1
  161. package/src/oauth/kimi.ts +9 -1
  162. package/src/oauth/kiro.ts +45 -0
  163. package/src/oauth/local-token-detect.ts +2 -0
  164. package/src/oauth/open-browser-choice.ts +26 -0
  165. package/src/oauth/store.ts +14 -5
  166. package/src/oauth/types.ts +15 -0
  167. package/src/providers/auto-compact-budget.ts +65 -0
  168. package/src/providers/command-code-efforts.ts +38 -6
  169. package/src/providers/context-cap.ts +4 -3
  170. package/src/providers/default-aliases.ts +65 -0
  171. package/src/providers/derive.ts +28 -0
  172. package/src/providers/fastwire.ts +7 -1
  173. package/src/providers/label.ts +34 -1
  174. package/src/providers/model-presets.ts +119 -0
  175. package/src/providers/new-model-policy.ts +146 -0
  176. package/src/providers/provider-id-rewrite.ts +2 -1
  177. package/src/providers/quota.ts +65 -7
  178. package/src/providers/registry.ts +184 -69
  179. package/src/providers/slug-codec.ts +52 -0
  180. package/src/providers/xai-transport.ts +21 -0
  181. package/src/reasoning-effort.ts +19 -2
  182. package/src/responses/apply-patch-envelope.ts +63 -0
  183. package/src/responses/code-mode-helper-compat.ts +50 -0
  184. package/src/responses/custom-tool-compat.ts +157 -39
  185. package/src/responses/namespace-tool-compat.ts +84 -4
  186. package/src/responses/parser.ts +7 -2
  187. package/src/responses/reasoning-replay-cache.ts +81 -3
  188. package/src/responses/schema.ts +5 -1
  189. package/src/responses/state.ts +94 -4
  190. package/src/responses/thought-signature-replay.ts +17 -0
  191. package/src/responses/turn-termination.ts +107 -0
  192. package/src/router.ts +40 -2
  193. package/src/routing/analytics.ts +1 -0
  194. package/src/routing/quota.ts +10 -0
  195. package/src/server/auth-cors.ts +27 -7
  196. package/src/server/chat-completions.ts +24 -16
  197. package/src/server/chat-native-sse.ts +3 -3
  198. package/src/server/chat-native.ts +30 -11
  199. package/src/server/claude-messages.ts +1 -1
  200. package/src/server/index.ts +179 -32
  201. package/src/server/lifecycle.ts +52 -1
  202. package/src/server/local-management-read-client.ts +1 -1
  203. package/src/server/local-provider-reload-client.ts +1 -1
  204. package/src/server/management/agent-settings-routes.ts +45 -23
  205. package/src/server/management/codex-prompt-routes.ts +570 -0
  206. package/src/server/management/combo-routes.ts +2 -1
  207. package/src/server/management/config-routes.ts +59 -12
  208. package/src/server/management/context.ts +10 -1
  209. package/src/server/management/logs-usage-routes.ts +30 -24
  210. package/src/server/management/model-routes.ts +274 -4
  211. package/src/server/management/model-rows.ts +4 -0
  212. package/src/server/management/native-integration-routes.ts +2 -1
  213. package/src/server/management/oauth-account-routes.ts +10 -4
  214. package/src/server/management/provider-capability-config.ts +1 -1
  215. package/src/server/management/provider-routes.ts +81 -11
  216. package/src/server/management/route-registry.ts +332 -0
  217. package/src/server/management/routing-profile-routes.ts +2 -2
  218. package/src/server/management/system-restart.ts +1 -1
  219. package/src/server/management-api.ts +18 -16
  220. package/src/server/port-reclaim.ts +1 -1
  221. package/src/server/proxy-liveness.ts +29 -5
  222. package/src/server/relay-eager.ts +147 -20
  223. package/src/server/relay.ts +251 -19
  224. package/src/server/request-log-conversation.ts +51 -0
  225. package/src/server/request-log.ts +70 -16
  226. package/src/server/responses/codex-auth-error.ts +55 -0
  227. package/src/server/responses/collaboration.ts +37 -3
  228. package/src/server/responses/combo-stream-preflight.ts +178 -0
  229. package/src/server/responses/compact.ts +6 -21
  230. package/src/server/responses/core.ts +810 -208
  231. package/src/server/responses/empty-completion-guard.ts +63 -6
  232. package/src/server/responses/fetch-helpers.ts +17 -102
  233. package/src/server/responses/input-admission.ts +3 -1
  234. package/src/server/responses/passthrough-error.ts +33 -9
  235. package/src/server/responses/responses-field-backfill.ts +105 -13
  236. package/src/server/responses/ws-upstream.ts +75 -2
  237. package/src/server/responses-custom-tool-repair.ts +87 -6
  238. package/src/server/responses-terminal-repair.ts +25 -4
  239. package/src/server/responses-undeclared-tool-guard.ts +241 -18
  240. package/src/server/sse-frame-buffer.ts +31 -4
  241. package/src/server/ws-bridge.ts +14 -2
  242. package/src/service.ts +42 -4
  243. package/src/storage/policy-job.ts +14 -4
  244. package/src/storage/policy.ts +88 -23
  245. package/src/types/config.ts +66 -0
  246. package/src/types/provider.ts +69 -0
  247. package/src/types/request.ts +17 -4
  248. package/src/types/tools.ts +116 -11
  249. package/src/types.ts +3 -1
  250. package/src/update/index.ts +5 -4
  251. package/src/update/job.ts +3 -1
  252. package/src/update/transactional-install.mjs +8 -1
  253. package/src/usage/expected-prices.ts +43 -24
  254. package/src/usage/log.ts +63 -12
  255. package/src/usage/summary.ts +211 -8
  256. package/src/vision/describe.ts +18 -13
  257. package/src/web-search/exa-executor.ts +40 -9
  258. package/src/web-search/executor.ts +10 -3
  259. package/src/web-search/loop.ts +5 -4
  260. package/gui/dist/assets/index-BF38heuV.js +0 -104
  261. package/gui/dist/assets/index-DMiI18Kv.css +0 -1
@@ -0,0 +1,496 @@
1
+ /**
2
+ * What `ocx` can do, as data an agent can read without parsing help text.
3
+ *
4
+ * This is the machine-readable index behind `ocx capabilities`. It relates each CLI
5
+ * capability to the management route(s) it drives, which nothing in this repository did
6
+ * before: help lived in twenty per-module `USAGE` constants and a hand-written banner
7
+ * that a test explicitly licensed to drift from the command registry.
8
+ *
9
+ * LEAF MODULE. It imports nothing from `src/cli/`, and nothing here may import a command
10
+ * module. That is not tidiness. Each command module declares its usage text as a
11
+ * top-level `const USAGE`, evaluated at import time, so a cycle back into this table
12
+ * would resolve to `undefined` under ESM rather than throwing -- silently emptying the
13
+ * usage text that `rejectArgs` hands to `CliUsageError`, in the exact error-reporting
14
+ * surface the CLI-operability issues are about. `tests/cli-capabilities.test.ts` asserts
15
+ * the absence of those imports and that every rendered usage string is non-empty, so the
16
+ * failure mode is loud instead of degraded.
17
+ *
18
+ * Head-handled surfaces (`--version`, `help`) are declared separately in
19
+ * `HEAD_CAPABILITIES`. They exit in the CLI head (`root.ts`) before dispatch and have no
20
+ * runner key, so listing them as ordinary capabilities would break the registry parity
21
+ * assertion that every canonical entry is a direct runner. `help` is excluded from
22
+ * `CLI_COMMANDS` deliberately -- `tests/cli-registry.test.ts` documents it as a
23
+ * head-handled pseudo-case -- and that decision is preserved here rather than reversed.
24
+ */
25
+
26
+ /** A management route a capability drives. Path text only; never a handler reference. */
27
+ export interface CapabilityRoute {
28
+ readonly method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
29
+ readonly path: string;
30
+ }
31
+
32
+ export interface CapabilityFlag {
33
+ readonly name: string;
34
+ readonly value?: "string" | "number" | "boolean";
35
+ readonly required?: boolean;
36
+ readonly summary: string;
37
+ }
38
+
39
+ /**
40
+ * How a capability emits JSON.
41
+ *
42
+ * - `payload`: the API payload, largely unwrapped.
43
+ * - `envelope`: a CLI-shaped object with its own schema.
44
+ * - `none`: no `--json` mode.
45
+ */
46
+ export type CapabilityJsonMode = "payload" | "envelope" | "none";
47
+
48
+ export interface Capability {
49
+ /** Command path, e.g. `["account", "pause"]`. */
50
+ readonly command: readonly string[];
51
+ readonly summary: string;
52
+ readonly routes: readonly CapabilityRoute[];
53
+ readonly flags: readonly CapabilityFlag[];
54
+ readonly mutates: boolean;
55
+ readonly json: CapabilityJsonMode;
56
+ readonly details?: readonly string[];
57
+ /**
58
+ * Extra banner rows this capability owns, for surfaces the banner shows separately
59
+ * from the bare command (`ocx restore back`, `ocx doctor --reclaim-response-temps`).
60
+ * Without this the banner cannot equal the capability set: it legitimately carries more
61
+ * rows than there are commands.
62
+ */
63
+ readonly bannerLines?: readonly string[];
64
+ }
65
+
66
+ /**
67
+ * Surfaces resolved in the CLI head, before dispatch.
68
+ *
69
+ * They belong in `ocx capabilities` output and in the banner, but not in `CLI_COMMANDS`:
70
+ * `--version`, `-v`, and `version` are answered at `root.ts` and exit, so none of them is
71
+ * a runner key to parity-check against.
72
+ */
73
+ export interface HeadCapability {
74
+ readonly invocations: readonly string[];
75
+ readonly summary: string;
76
+ readonly bannerLine: string;
77
+ }
78
+
79
+ export const HEAD_CAPABILITIES: readonly HeadCapability[] = [
80
+ {
81
+ invocations: ["--version", "-v", "version"],
82
+ summary: "Print the CLI version and exit.",
83
+ bannerLine: "ocx --version | -v Print version",
84
+ },
85
+ {
86
+ invocations: ["help", "--help", "-h"],
87
+ summary: "Print the command list, or one command's usage with `ocx help <command>`.",
88
+ bannerLine: "ocx help [command] Show help for a command",
89
+ },
90
+ ];
91
+
92
+ /**
93
+ * Capabilities declared so far. Incomplete by design: later phases add verbs.
94
+ * `ocx capabilities` is the index of what is listed here, not of every CLI command.
95
+ * A capability must not name a route the command does not actually fetch.
96
+ */
97
+ export const CAPABILITIES: readonly Capability[] = [
98
+ {
99
+ command: ["status"],
100
+ summary: "Proxy status, injection state, and version skew between this CLI and the running proxy.",
101
+ // No management route: `collectStatus` identity-probes `/healthz` through
102
+ // `findLiveProxy` and reads local config. Declaring `GET /api/status` here was wrong
103
+ // -- that route does not exist, and the registry cross-check caught it.
104
+ routes: [],
105
+ flags: [{ name: "--json", value: "boolean", summary: "Emit the status envelope as JSON." }],
106
+ mutates: false,
107
+ json: "envelope",
108
+ details: ["Reads /healthz plus local config; drives no management API route."],
109
+ },
110
+ {
111
+ command: ["capabilities"],
112
+ summary: "List the declared CLI capabilities and the management routes they drive.",
113
+ routes: [],
114
+ flags: [
115
+ { name: "--json", value: "boolean", summary: "Emit the full capability table as JSON." },
116
+ { name: "--mutating-only", value: "boolean", summary: "Restrict output to capabilities that mutate state." },
117
+ { name: "--route", value: "string", summary: "Show which capabilities drive a management route." },
118
+ ],
119
+ mutates: false,
120
+ json: "envelope",
121
+ details: ["Start here when driving ocx programmatically: it is the declared surface index, not a complete verb list."],
122
+ },
123
+ {
124
+ command: ["provider", "list"],
125
+ summary: "Configured providers with connectivity and selected models.",
126
+ // Local config + PROVIDER_REGISTRY. Does not call GET /api/providers.
127
+ routes: [],
128
+ flags: [{ name: "--json", value: "boolean", summary: "Emit the provider list as JSON." }],
129
+ mutates: false,
130
+ json: "envelope",
131
+ details: ["Reads local config; drives no management API route."],
132
+ },
133
+ {
134
+ command: ["account", "list"],
135
+ summary: "Codex OAuth accounts with pool priority and pause state.",
136
+ routes: [{ method: "GET", path: "/api/codex-auth/accounts" }],
137
+ flags: [{ name: "--json", value: "boolean", summary: "Emit the account list as JSON." }],
138
+ mutates: false,
139
+ json: "payload",
140
+ details: [
141
+ "STATUS names `paused` alongside `selected`: a paused-but-selected account still receives requests.",
142
+ "`--quota` shows cached Codex windows (including 5h); `--refresh` bypasses the server TTL.",
143
+ ],
144
+ },
145
+ {
146
+ command: ["usage"],
147
+ summary: "Token and estimated-cost report over a time range.",
148
+ routes: [{ method: "GET", path: "/api/usage" }],
149
+ flags: [
150
+ { name: "--range", value: "string", summary: "today | 1d | 7d | 30d | all" },
151
+ { name: "--provider", value: "string", summary: "Restrict to one provider." },
152
+ { name: "--model", value: "string", summary: "Restrict to one model id." },
153
+ { name: "--json", value: "boolean", summary: "Emit the usage report as JSON." },
154
+ ],
155
+ mutates: false,
156
+ json: "payload",
157
+ details: [
158
+ "Per-account totals are withheld under `--provider` or `--model`: account rows cannot be honestly re-partitioned by provider, so the report says so rather than printing an empty table.",
159
+ "An `(ambiguous)` account row aggregates several accounts; do not read it as one identity.",
160
+ ],
161
+ },
162
+ {
163
+ command: ["account", "pause"],
164
+ summary: "Stop routing new requests to one account in the Codex pool.",
165
+ // One route, both directions: `resume` is the same PUT with `paused: false`.
166
+ routes: [{ method: "PUT", path: "/api/codex-auth/accounts/pause" }],
167
+ flags: [{ name: "--json", value: "boolean", summary: "Emit the pause result as JSON." }],
168
+ mutates: true,
169
+ json: "envelope",
170
+ details: [
171
+ "Pausing also unbinds threads pinned to the account and selects a fallback if it was active -- side effects of the route, not of the word `pause`.",
172
+ "The issue that requested this reported the route as POST; it is PUT.",
173
+ ],
174
+ },
175
+ {
176
+ command: ["account", "resume"],
177
+ summary: "Return a paused account to the Codex pool.",
178
+ routes: [{ method: "PUT", path: "/api/codex-auth/accounts/pause" }],
179
+ flags: [{ name: "--json", value: "boolean", summary: "Emit the resume result as JSON." }],
180
+ mutates: true,
181
+ json: "envelope",
182
+ },
183
+ {
184
+ command: ["account", "pause-exhausted"],
185
+ summary: "Pause every Codex account whose quota is spent.",
186
+ routes: [{ method: "PUT", path: "/api/codex-auth/accounts/pause-exhausted" }],
187
+ flags: [{ name: "--json", value: "boolean", summary: "Emit paused ids and the checked/failed counts as JSON." }],
188
+ mutates: true,
189
+ json: "envelope",
190
+ details: [
191
+ "The route refreshes quota per account and can partially fail; a non-zero failed count exits 1 and sets ok:false, because silence would read as `none were exhausted`.",
192
+ ],
193
+ },
194
+ {
195
+ command: ["account", "strategy"],
196
+ summary: "Show or set how an account pool picks the next account.",
197
+ // Both pools, because both have the setting. The Codex pool reads its applied values
198
+ // from the active payload; the Anthropic pool has its own GET.
199
+ routes: [
200
+ { method: "GET", path: "/api/codex-auth/active" },
201
+ { method: "PUT", path: "/api/codex-auth/pool-strategy" },
202
+ { method: "GET", path: "/api/oauth/accounts/pool" },
203
+ { method: "PUT", path: "/api/oauth/accounts/pool" },
204
+ ],
205
+ flags: [{ name: "--json", value: "boolean", summary: "Emit the applied strategy and sticky limit as JSON." }],
206
+ mutates: true,
207
+ json: "envelope",
208
+ details: [
209
+ "A bare invocation reads and never writes.",
210
+ "The APPLIED value is echoed, not the requested one, so a server-side normalization stays visible.",
211
+ "Values are not re-validated in the CLI: the server owns the strategy names and the 1-100 sticky bound.",
212
+ "`anthropic` is the only OAuth pool with this setting; other OAuth providers are refused without a round-trip.",
213
+ ],
214
+ },
215
+ {
216
+ command: ["account", "sticky"],
217
+ summary: "Show or set how many consecutive requests stay on one account.",
218
+ routes: [
219
+ { method: "GET", path: "/api/codex-auth/active" },
220
+ { method: "PUT", path: "/api/codex-auth/pool-strategy" },
221
+ { method: "GET", path: "/api/oauth/accounts/pool" },
222
+ { method: "PUT", path: "/api/oauth/accounts/pool" },
223
+ ],
224
+ flags: [{ name: "--json", value: "boolean", summary: "Emit the applied strategy and sticky limit as JSON." }],
225
+ mutates: true,
226
+ json: "envelope",
227
+ details: ["Only meaningful under the sticky-capable strategies; the pool strategy is the other half of this setting."],
228
+ },
229
+ {
230
+ command: ["logs"],
231
+ summary: "Recent request log rows, filterable by provider, model, conversation, and status.",
232
+ routes: [{ method: "GET", path: "/api/logs" }],
233
+ flags: [
234
+ { name: "--provider", value: "string", summary: "Restrict to one provider, matching failover attempts too." },
235
+ { name: "--model", value: "string", summary: "Restrict to one model id, matching failover attempts too." },
236
+ { name: "--conversation", value: "string", summary: "Restrict to one conversation id (`--conversationId` is accepted too)." },
237
+ { name: "--status", value: "string", summary: "An exact code (429) or a class (5xx)." },
238
+ { name: "--limit", value: "number", summary: "Row cap; defaults to 200." },
239
+ { name: "--follow", value: "boolean", summary: "Stream new rows as JSONL; implies --jsonl." },
240
+ { name: "--json", value: "boolean", summary: "Emit the server payload as JSON." },
241
+ { name: "--jsonl", value: "boolean", summary: "Emit one row per line." },
242
+ ],
243
+ mutates: false,
244
+ json: "payload",
245
+ details: [
246
+ "`--provider` and `--model` both match a failover attempt, so a request is findable by what actually served it, not only by what was asked for.",
247
+ "Rows print `conv=<id>` when the entry carries one, so a conversation filter can be told apart from an empty result.",
248
+ "`--follow` deduplicates by row id and cannot be combined with `--json`.",
249
+ ],
250
+ },
251
+ {
252
+ command: ["storage", "report"],
253
+ summary: "Disk usage under CODEX_HOME, with the log-guard protection report.",
254
+ routes: [{ method: "GET", path: "/api/storage" }],
255
+ flags: [{ name: "--json", value: "boolean", summary: "Emit the storage report as JSON." }],
256
+ mutates: false,
257
+ json: "payload",
258
+ bannerLines: ["ocx storage Storage report (default subcommand)"],
259
+ },
260
+ {
261
+ command: ["storage", "cleanup"],
262
+ summary: "Preview or delete the oldest archived sessions by percentage.",
263
+ // Both routes, because the verb always previews: the mutating route requires the digest the
264
+ // preview returns and rejects a stale one, so the two are one operation.
265
+ routes: [
266
+ { method: "POST", path: "/api/storage/cleanup/preview" },
267
+ { method: "POST", path: "/api/storage/cleanup" },
268
+ ],
269
+ flags: [
270
+ { name: "--percent", value: "number", summary: "Portion of the oldest archived sessions to target (0-100)." },
271
+ { name: "--mode", value: "string", summary: "quarantine (recoverable from trash) or permanent." },
272
+ { name: "--yes", value: "boolean", summary: "Required to actually delete; without it this is a preview." },
273
+ { name: "--json", value: "boolean", summary: "Emit the preview or result as JSON." },
274
+ ],
275
+ mutates: true,
276
+ json: "payload",
277
+ details: [
278
+ "Without `--yes` it prints what WOULD be freed and exits 0 having changed nothing.",
279
+ "There is no interactive confirmation: a prompt an agent can answer is not a safety boundary.",
280
+ "`--mode quarantine` moves files to trash, so `storage trash restore` can undo it; `permanent` cannot be undone.",
281
+ ],
282
+ },
283
+ {
284
+ command: ["storage", "trash"],
285
+ summary: "List quarantined cleanup batches, or restore one.",
286
+ routes: [
287
+ { method: "GET", path: "/api/storage/trash" },
288
+ { method: "POST", path: "/api/storage/trash/restore" },
289
+ ],
290
+ flags: [
291
+ { name: "--yes", value: "boolean", summary: "Required for restore, which moves files and reconciles database rows." },
292
+ { name: "--json", value: "boolean", summary: "Emit the trash list or restore result as JSON." },
293
+ ],
294
+ mutates: true,
295
+ json: "payload",
296
+ details: ["Restore fails with a named 409 when the destination already exists, rather than overwriting it."],
297
+ },
298
+ {
299
+ command: ["storage", "policy"],
300
+ summary: "Show, change, or run the automatic archived-session cleanup policy.",
301
+ routes: [
302
+ { method: "GET", path: "/api/storage/cleanup-policy" },
303
+ { method: "PUT", path: "/api/storage/cleanup-policy" },
304
+ { method: "POST", path: "/api/storage/cleanup-policy/run" },
305
+ ],
306
+ flags: [
307
+ { name: "--enabled", value: "string", summary: "true or false." },
308
+ { name: "--percent", value: "number", summary: "Portion of oldest archived sessions each run targets." },
309
+ { name: "--mode", value: "string", summary: "quarantine or permanent." },
310
+ { name: "--schedule", value: "string", summary: "startup, daily, weekly, or manual." },
311
+ { name: "--yes", value: "boolean", summary: "Required for `policy run`, which deletes immediately." },
312
+ { name: "--json", value: "boolean", summary: "Emit the policy or run state as JSON." },
313
+ ],
314
+ mutates: true,
315
+ json: "payload",
316
+ details: [
317
+ "`policy set` never enables implicitly: omitting `--enabled` keeps the stored value.",
318
+ "`policy run` forces a run regardless of schedule, so it needs `--yes`.",
319
+ ],
320
+ },
321
+ {
322
+ command: ["inspect", "config"],
323
+ summary: "The effective merged configuration the proxy is running.",
324
+ routes: [{ method: "GET", path: "/api/config" }],
325
+ flags: [{ name: "--json", value: "boolean", summary: "Emit the config as JSON." }],
326
+ mutates: false,
327
+ json: "payload",
328
+ },
329
+ {
330
+ command: ["inspect", "catalog"],
331
+ summary: "The generated model catalog served to clients.",
332
+ routes: [{ method: "GET", path: "/api/catalog" }],
333
+ flags: [{ name: "--json", value: "boolean", summary: "Emit the catalog as JSON." }],
334
+ mutates: false,
335
+ json: "payload",
336
+ },
337
+ {
338
+ command: ["inspect", "routing-analytics"],
339
+ summary: "Aggregate routing outcomes per provider and model.",
340
+ routes: [{ method: "GET", path: "/api/routing-analytics" }],
341
+ flags: [{ name: "--json", value: "boolean", summary: "Emit the analytics payload as JSON." }],
342
+ mutates: false,
343
+ json: "payload",
344
+ },
345
+ {
346
+ command: ["inspect", "pacing"],
347
+ summary: "Request-pacing state for one provider or all of them.",
348
+ routes: [{ method: "GET", path: "/api/provider-request-pacing" }],
349
+ flags: [
350
+ { name: "--name", value: "string", summary: "Restrict to one provider; omitted means every provider." },
351
+ { name: "--json", value: "boolean", summary: "Emit the pacing state as JSON." },
352
+ ],
353
+ mutates: false,
354
+ json: "payload",
355
+ details: ["An unknown provider name is a 404 rather than an empty result."],
356
+ },
357
+ {
358
+ command: ["inspect", "key-providers"],
359
+ summary: "Providers that authenticate with an API key rather than OAuth.",
360
+ routes: [{ method: "GET", path: "/api/key-providers" }],
361
+ flags: [{ name: "--json", value: "boolean", summary: "Emit the provider list as JSON." }],
362
+ mutates: false,
363
+ json: "payload",
364
+ },
365
+ {
366
+ command: ["inspect", "codex-prompt"],
367
+ summary: "The Codex system prompt state, or the prompt text itself.",
368
+ routes: [
369
+ { method: "GET", path: "/api/codex-prompt" },
370
+ { method: "GET", path: "/api/codex-prompt/text" },
371
+ ],
372
+ flags: [
373
+ { name: "--text", value: "boolean", summary: "Print the prompt body verbatim instead of its metadata." },
374
+ { name: "--json", value: "boolean", summary: "Emit the prompt metadata as JSON." },
375
+ ],
376
+ mutates: false,
377
+ json: "payload",
378
+ details: ["Read-only by design: the six mutating prompt routes require a dashboard session."],
379
+ },
380
+ {
381
+ command: ["inspect", "client-config"],
382
+ summary: "The generated configuration snippet for a supported client.",
383
+ routes: [{ method: "GET", path: "/api/client-config" }],
384
+ flags: [
385
+ { name: "--client", value: "string", summary: "Required client id; the route names every accepted value on error." },
386
+ { name: "--json", value: "boolean", summary: "Emit the snippet payload as JSON." },
387
+ ],
388
+ mutates: false,
389
+ json: "payload",
390
+ },
391
+ {
392
+ command: ["inspect", "star"],
393
+ summary: "Whether this repository is starred by the signed-in GitHub account.",
394
+ // GET only, permanently. The POST spends the operator identity and requires a dashboard
395
+ // session precisely so an agent cannot answer that question for them.
396
+ routes: [{ method: "GET", path: "/api/github/star" }],
397
+ flags: [{ name: "--json", value: "boolean", summary: "Emit the star status as JSON." }],
398
+ mutates: false,
399
+ json: "payload",
400
+ details: ["Starring is never available from the CLI; the verb says so rather than offering a flag that cannot work."],
401
+ },
402
+ {
403
+ command: ["inspect", "windows-tray"],
404
+ summary: "Windows tray helper state.",
405
+ routes: [{ method: "GET", path: "/api/windows-tray" }],
406
+ flags: [{ name: "--json", value: "boolean", summary: "Emit the tray state as JSON." }],
407
+ mutates: false,
408
+ json: "payload",
409
+ },
410
+ {
411
+ command: ["system", "codex-app-server"],
412
+ summary: "Codex app-server reachability and process state, as the dashboard sees it.",
413
+ routes: [{ method: "GET", path: "/api/system/codex-app-server" }],
414
+ flags: [{ name: "--json", value: "boolean", summary: "Emit the app-server state as JSON." }],
415
+ mutates: false,
416
+ json: "payload",
417
+ details: [
418
+ "The GUI reads this state directly; without a verb an agent could not tell whether the Codex app-server was reachable at all.",
419
+ ],
420
+ },
421
+ {
422
+ command: ["system", "codex-restart"],
423
+ summary: "Restart the Codex app-server.",
424
+ routes: [{ method: "POST", path: "/api/system/codex-restart" }],
425
+ flags: [
426
+ { name: "--yes", value: "boolean", summary: "Required: restarts the operator's running Codex app-server." },
427
+ { name: "--json", value: "boolean", summary: "Emit the restart result as JSON." },
428
+ ],
429
+ mutates: true,
430
+ json: "payload",
431
+ details: [
432
+ "`sync --restart-codex` is not a substitute: it restarts only as a side effect after a catalog or cache write, so it cannot restart a healthy install on request.",
433
+ "--yes is mandatory because this interrupts a running editor session, which must never happen because an agent guessed a subcommand.",
434
+ ],
435
+ },
436
+ {
437
+ command: ["claude", "desktop", "status"],
438
+ summary: "Applied-vs-desired Claude Desktop state, including staleness, drift, and health.",
439
+ routes: [{ method: "GET", path: "/api/claude-desktop/status" }],
440
+ flags: [{ name: "--json", value: "boolean", summary: "Emit the live status as JSON." }],
441
+ mutates: false,
442
+ json: "payload",
443
+ details: [
444
+ "Distinct from `claude desktop show`, which reports what this machine WOULD write; this reports what is actually in effect, which only the running proxy knows.",
445
+ ],
446
+ },
447
+ {
448
+ command: ["integration", "native"],
449
+ summary: "Show or toggle the native Claude, Claude Desktop, Codex, and Grok integrations.",
450
+ routes: [
451
+ { method: "GET", path: "/api/native-integrations" },
452
+ { method: "PUT", path: "/api/native-integrations/claude" },
453
+ { method: "PUT", path: "/api/native-integrations/claude-desktop" },
454
+ { method: "PUT", path: "/api/native-integrations/codex" },
455
+ { method: "PUT", path: "/api/native-integrations/grok" },
456
+ ],
457
+ flags: [{ name: "--json", value: "boolean", summary: "Emit the client rows or toggle result as JSON." }],
458
+ mutates: true,
459
+ json: "payload",
460
+ details: [
461
+ "The list renders per-client state, installed, and desired columns; a blocked disable is named rather than left silent.",
462
+ "Each client has its own route because a toggle rewrites that client's own config file.",
463
+ ],
464
+ },
465
+ {
466
+ command: ["agent", "request-user-input"],
467
+ summary: "Show or set whether default mode may ask the operator a question mid-task.",
468
+ routes: [
469
+ { method: "GET", path: "/api/codex-auth/features/default-mode-request-user-input" },
470
+ { method: "PUT", path: "/api/codex-auth/features/default-mode-request-user-input" },
471
+ ],
472
+ flags: [{ name: "--json", value: "boolean", summary: "Emit the feature state as JSON." }],
473
+ mutates: true,
474
+ json: "payload",
475
+ details: ["A bare invocation reads and never writes."],
476
+ },
477
+ ];
478
+
479
+ /** Capabilities that drive `route`, for `ocx capabilities --route`. */
480
+ export function capabilitiesForRoute(path: string): Capability[] {
481
+ return CAPABILITIES.filter(cap => cap.routes.some(r => r.path === path));
482
+ }
483
+
484
+ /** Every `(method, path)` pair any capability drives. */
485
+ export function capabilityRouteKeys(): Set<string> {
486
+ const keys = new Set<string>();
487
+ for (const cap of CAPABILITIES) {
488
+ for (const route of cap.routes) keys.add(`${route.method} ${route.path}`);
489
+ }
490
+ return keys;
491
+ }
492
+
493
+ /** Rendered command path, e.g. `ocx account pause`. */
494
+ export function capabilityInvocation(cap: Capability): string {
495
+ return `ocx ${cap.command.join(" ")}`;
496
+ }
@@ -14,7 +14,7 @@ import { writeDesktop3pConfig, type Desktop3pConfigMode, parseDesktop3pModeArgs
14
14
  import { filterCatalogVisibleModels, desktopVisibleNativeSlugs, nativeContextLimits } from "../codex/catalog";
15
15
  import { buildClaudeDesktopState, fetchAllModels } from "../server/management-api";
16
16
  import { findLiveProxy } from "../server/proxy-liveness";
17
- import { runtimeRequest } from "./runtime-api";
17
+ import { CliUsageError, runtimeRequest, takeJsonFlag } from "./runtime-api";
18
18
  import { OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers";
19
19
 
20
20
  function isFamily(value: string | undefined): value is DesktopFamily {
@@ -25,6 +25,7 @@ function printDesktopHelp(): void {
25
25
  console.log(`Usage:
26
26
  ocx claude desktop [apply] [--static|--hybrid|--discovery-only]
27
27
  ocx claude desktop show [--json]
28
+ ocx claude desktop status [--json]
28
29
  ocx claude desktop move <provider/model> <opus|fable|sonnet|haiku> [--default]
29
30
  ocx claude desktop default <family> <provider/model|none>
30
31
  ocx claude desktop export <path|->
@@ -118,10 +119,10 @@ export async function handleClaudeDesktopCommand(argv: string[], deps: ApplyProf
118
119
  const nonMode = argv.filter(arg => !["apply", "--static", "--hybrid", "--discovery-only"].includes(arg));
119
120
  if (nonMode.length > 0) {
120
121
  console.error(`알 수 없는 인자: ${nonMode.join(" ")}`);
121
- return 1;
122
+ return 2;
122
123
  }
123
124
  const parsedMode = parseDesktop3pModeArgs(legacyFlags);
124
- if ("error" in parsedMode) { console.error(parsedMode.error); return 1; }
125
+ if ("error" in parsedMode) { console.error(parsedMode.error); return 2; }
125
126
  try {
126
127
  const config = loadConfig();
127
128
  const state = await buildClaudeDesktopState(config);
@@ -145,10 +146,29 @@ export async function handleClaudeDesktopCommand(argv: string[], deps: ApplyProf
145
146
 
146
147
  try {
147
148
  const config = loadConfig();
149
+ // `status` is API-backed and must NOT build local state first: the whole point of the
150
+ // route the GUI polls (/api/claude-desktop/status) is the applied-vs-desired comparison,
151
+ // including staleness, drift and health, which only the running proxy knows. `show`
152
+ // reports what this machine would write; `status` reports what is actually in effect.
153
+ if (command === "status") {
154
+ const rest = argv.slice(1);
155
+ const wantsJson = takeJsonFlag(rest);
156
+ if (rest.length > 0) throw new CliUsageError("Usage: ocx claude desktop status [--json]");
157
+ const live = await runtimeRequest<Record<string, unknown>>("/api/claude-desktop/status", {});
158
+ if (wantsJson) console.log(JSON.stringify(live, null, 2));
159
+ else {
160
+ for (const [key, value] of Object.entries(live)) {
161
+ console.log(`${key}: ${typeof value === "object" ? JSON.stringify(value) : String(value)}`);
162
+ }
163
+ }
164
+ return 0;
165
+ }
148
166
  const state = await buildClaudeDesktopState(config);
149
167
  if (command === "show") {
150
- if (argv.length > 2 || (argv[1] && argv[1] !== "--json")) throw new Error("Usage: ocx claude desktop show [--json]");
151
- if (argv[1] === "--json") console.log(JSON.stringify(state));
168
+ const rest = argv.slice(1);
169
+ const wantsJson = takeJsonFlag(rest);
170
+ if (rest.length > 0) throw new CliUsageError("Usage: ocx claude desktop show [--json]");
171
+ if (wantsJson) console.log(JSON.stringify(state));
152
172
  else {
153
173
  for (const family of DESKTOP_FAMILIES) {
154
174
  console.log(`${family.toUpperCase()}${state.profile.defaults[family] ? ` (default: ${state.profile.defaults[family]})` : ""}`);
@@ -161,7 +181,7 @@ export async function handleClaudeDesktopCommand(argv: string[], deps: ApplyProf
161
181
  }
162
182
  if (command === "move") {
163
183
  const [, route, familyRaw, ...flags] = argv;
164
- if (!route || !isFamily(familyRaw) || flags.some(flag => flag !== "--default")) throw new Error("Usage: ocx claude desktop move <route> <family> [--default]");
184
+ if (!route || !isFamily(familyRaw) || flags.some(flag => flag !== "--default")) throw new CliUsageError("Usage: ocx claude desktop move <route> <family> [--default]");
165
185
  if (!state.models.some(model => model.route === route && model.available)) throw new Error(`현재 사용할 수 없는 모델입니다: ${route}`);
166
186
  const profile = moveDesktopRoute(state.profile, route, familyRaw, flags.includes("--default"));
167
187
  config.claudeCode = { ...(config.claudeCode ?? {}), desktopProfile: profile };
@@ -171,7 +191,7 @@ export async function handleClaudeDesktopCommand(argv: string[], deps: ApplyProf
171
191
  }
172
192
  if (command === "default") {
173
193
  const [, familyRaw, routeRaw] = argv;
174
- if (!isFamily(familyRaw) || !routeRaw || argv.length !== 3) throw new Error("Usage: ocx claude desktop default <family> <route|none>");
194
+ if (!isFamily(familyRaw) || !routeRaw || argv.length !== 3) throw new CliUsageError("Usage: ocx claude desktop default <family> <route|none>");
175
195
  const route = routeRaw === "none" ? null : routeRaw;
176
196
  if (route && !state.models.some(model => model.route === route && model.available)) throw new Error(`현재 사용할 수 없는 모델입니다: ${route}`);
177
197
  const profile = setDesktopFamilyDefault(state.profile, familyRaw, route);
@@ -182,7 +202,7 @@ export async function handleClaudeDesktopCommand(argv: string[], deps: ApplyProf
182
202
  }
183
203
  if (command === "export") {
184
204
  const target = argv[1];
185
- if (!target || argv.length !== 2) throw new Error("Usage: ocx claude desktop export <path|->");
205
+ if (!target || argv.length !== 2) throw new CliUsageError("Usage: ocx claude desktop export <path|->");
186
206
  const json = JSON.stringify(state.profile, null, 2) + "\n";
187
207
  if (target === "-") process.stdout.write(json);
188
208
  else writeFileSync(resolve(target), json, { encoding: "utf8", mode: 0o600 });
@@ -191,7 +211,7 @@ export async function handleClaudeDesktopCommand(argv: string[], deps: ApplyProf
191
211
  if (command === "import") {
192
212
  const source = argv[1];
193
213
  const flags = argv.slice(2);
194
- if (!source || flags.some(flag => flag !== "--apply")) throw new Error("Usage: ocx claude desktop import <path> [--apply]");
214
+ if (!source || flags.some(flag => flag !== "--apply")) throw new CliUsageError("Usage: ocx claude desktop import <path> [--apply]");
195
215
  const profile = parseDesktopProfile(JSON.parse(readFileSync(resolve(source), "utf8")));
196
216
  const reconciled = (await buildClaudeDesktopState(config, profile)).profile;
197
217
  config.claudeCode = { ...(config.claudeCode ?? {}), desktopProfile: reconciled };
@@ -205,9 +225,9 @@ export async function handleClaudeDesktopCommand(argv: string[], deps: ApplyProf
205
225
  return 0;
206
226
  }
207
227
  printDesktopHelp();
208
- return 1;
228
+ return 2;
209
229
  } catch (error) {
210
230
  console.error(error instanceof Error ? error.message : String(error));
211
- return 1;
231
+ return error instanceof CliUsageError ? 2 : 1;
212
232
  }
213
233
  }
package/src/cli/claude.ts CHANGED
@@ -34,6 +34,8 @@ export type ClaudeEnvDeps = {
34
34
  authDetect?: Omit<Partial<AuthDetectDeps>, "env" | "ownTokens">;
35
35
  /** Test seam; production uses the authenticated Node-launcher context. */
36
36
  preBunAnthropicSlots?: readonly AnthropicParentEnvSlot[] | null;
37
+ /** Explicit unsafe opt-in from a root `--dangerously-skip-permissions` launch. */
38
+ allowRootSkipPermissions?: boolean;
37
39
  };
38
40
 
39
41
  function isClaudeLoopbackHostname(hostname: string): boolean {
@@ -115,6 +117,9 @@ export function buildClaudeEnv(
115
117
  if (env[name] !== undefined && env[name] !== "") return; // user wins
116
118
  env[name] = value;
117
119
  };
120
+ if (deps.allowRootSkipPermissions === true) {
121
+ setDefault("IS_SANDBOX", "1");
122
+ }
118
123
  setDefault("ANTHROPIC_BASE_URL", `http://127.0.0.1:${port}`);
119
124
  const existingBaseUrl = env.ANTHROPIC_BASE_URL;
120
125
  if (existingBaseUrl) {
@@ -301,6 +306,22 @@ export function claudeNotFoundHint(
301
306
  return platform === "win32" && code === 9009 && !signal ? CLAUDE_INSTALL_HINT : null;
302
307
  }
303
308
 
309
+ export function shouldAllowRootSkipPermissions(
310
+ args: readonly string[],
311
+ getuid: (() => number) | null | undefined = process.getuid,
312
+ ): boolean {
313
+ return args.includes("--dangerously-skip-permissions")
314
+ && typeof getuid === "function"
315
+ && getuid() === 0;
316
+ }
317
+
318
+ export function rootSkipPermissionsNotice(env: ClaudeLaunchEnv): string {
319
+ if (env.IS_SANDBOX === "1") {
320
+ return "⚠ Root --dangerously-skip-permissions requested: OpenCodex set IS_SANDBOX=1 to bypass Claude Code's root guard. OpenCodex did not create an OS sandbox; prefer running as a non-root user.";
321
+ }
322
+ return `⚠ Root --dangerously-skip-permissions requested: preserving user IS_SANDBOX=${env.IS_SANDBOX}; Claude Code's root guard remains in control.`;
323
+ }
324
+
304
325
  export async function cmdClaude(args: string[]): Promise<number> {
305
326
  const config = loadConfig();
306
327
  if (config.claudeCode?.enabled === false) {
@@ -313,7 +334,11 @@ export async function cmdClaude(args: string[]): Promise<number> {
313
334
  return 1;
314
335
  }
315
336
  const contextWindows = await fetchClaudeContextWindows(config, port);
316
- const env = buildClaudeEnv(config, port, process.env, contextWindows);
337
+ const allowRootSkipPermissions = shouldAllowRootSkipPermissions(args);
338
+ const env = buildClaudeEnv(config, port, process.env, contextWindows, { allowRootSkipPermissions });
339
+ if (allowRootSkipPermissions) {
340
+ console.error(rootSkipPermissionsNotice(env));
341
+ }
317
342
  // Pre-write the CLI's gateway-model cache (devlog 030): without a token the CLI
318
343
  // never refreshes it, so the picker would keep showing yesterday's aliases.
319
344
  try {