@prohost/cli 0.2.2 → 0.2.3

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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,121 @@ Versions follow [semver](https://semver.org/). Publishing is automated: merging
4
4
  a version bump to `main` triggers `.github/workflows/npm-publish-cli.yml`, which
5
5
  builds via `prepack`, runs the suite, publishes, and tags `cli-v<version>`.
6
6
 
7
+ ## 0.2.3
8
+
9
+ Three things, all of them about a run knowing what is actually true about
10
+ itself: a prompt bullet that taught agents to doubt tools they had, a way to
11
+ give an agent a tool on purpose, and closing the gaps 0.2.2 shipped with an
12
+ honest note attached.
13
+
14
+ - **A ToolSearch miss no longer reads as proof.** 0.2.1 told the agent to
15
+ "search ToolSearch for a capability before telling anyone you lack it", which
16
+ is right about deferred web browsing and badly wrong as a general rule. In
17
+ production a paired brain searched ToolSearch for `bash`, found nothing —
18
+ ToolSearch indexes only the *deferred* tools, and Bash is a direct one — and
19
+ spent an entire resumed session reporting it had no shell, while
20
+ `--dangerously-skip-permissions` had been handing it the full toolset the
21
+ whole time. The prompt now states that the directly-listed tools are the
22
+ authoritative set, that a ToolSearch miss is evidence of nothing, and that
23
+ "looking" means running the command rather than reading one's own tool list:
24
+ a claim of no shell, no file access or no checkout has to come after an
25
+ attempt that failed and can be quoted. The original bullet stays — deferred
26
+ browsing is real, and dropping it would just swap one wrong inference for the
27
+ other.
28
+ - **`~/.prohost/extra-mcp.json` — opt-in extra MCP servers.** A run gets the
29
+ ProhostAI server and nothing else, and that isolation is a security boundary:
30
+ a run executes with permission prompts off on prompt text written by whoever
31
+ mentioned the agent. This adds a way to widen it deliberately without weakening
32
+ the default. Write the file yourself — the CLI never creates it — holding the
33
+ same `{"mcpServers": {…}}` object Claude Code's `--mcp-config` takes; it works
34
+ on both brains. The name `prohost` is reserved and an entry using it is
35
+ rejected out loud rather than silently honoured; a malformed file is a warning
36
+ and a run without extras rather than a run that doesn't happen; and which
37
+ servers loaded is printed when the grant set changes so `agent.log` can answer "did my server
38
+ load". Every server in that file is exercisable by anyone who can trigger the
39
+ agent, which the README, the file's own documentation and `--help` all say in
40
+ those words.
41
+ - **Codex gets the isolation Claude Code has.** 0.2.2 shipped with "there is no
42
+ Codex equivalent of `--strict-mcp-config`", which turned out to be true of the
43
+ two mechanisms that look like one and false overall. `-c mcp_servers={…}` does
44
+ not replace the table — Codex deep-merges overrides — and `--ignore-user-config`
45
+ works but discards the operator's model, profile and sandbox settings along
46
+ with their servers. The one that works is per-server: the CLI now asks the
47
+ binary what it has (`codex mcp list --json`) and passes a single
48
+ `-c mcp_servers={"<name>"={enabled=false}, …}`, verified end to end against
49
+ `codex-cli 0.144.1` — the tools are genuinely gone from the model's list
50
+ mid-run, each server's own definition survives, and servers not named are
51
+ untouched. Asking the binary rather than parsing `~/.codex/config.toml`
52
+ matters twice over: on a normal install `codex mcp list` reports servers that
53
+ appear nowhere in that file, and the name has to go inside the value because
54
+ a server called `fly.io` written as `-c mcp_servers.fly.io.enabled=false`
55
+ addresses a three-segment path and makes Codex reject the entire config. The
56
+ override is also **verified against the binary before it is used**, which one
57
+ case genuinely needs: `{enabled=false}` merges onto a server's existing
58
+ definition, and a plugin-contributed server has none to merge onto, so the
59
+ override leaves a table with no transport and Codex refuses to start —
60
+ shipped unverified that would have broken every run for anyone with a Codex
61
+ plugin installed. The CLI falls back to checking each server on its own and
62
+ fails the run before Codex starts if any cannot be disabled. If the
63
+ enumeration fails outright the run also fails closed instead of proceeding
64
+ under `--yolo` with an unprovable boundary. Same treatment for `extra-mcp.json` definitions:
65
+ rejected ones are dropped with a warning rather than taking the run with them.
66
+ Every probe now runs in the exact cwd of the eventual Codex process as well,
67
+ so a trusted repo's project-local `.codex/config.toml` cannot appear only
68
+ after a clean-directory survey. Default `$PROHOST_HOME/workspace` and an
69
+ explicit harness `--workdir` both use that same path end to end.
70
+
71
+ Three further things the binary had to be asked rather than assumed. Every
72
+ server goes in **one** `-c mcp_servers={…}`, ProhostAI included: `-c`
73
+ overrides apply in order and a later whole-table assignment *replaces* an
74
+ earlier dotted one, so the obvious two-override shape silently dropped the
75
+ ProhostAI server on every run that disabled anything. An extra whose name
76
+ your Codex config already uses is **refused**, because Codex merges the two
77
+ definitions and the existing server's `Authorization` header would be sent to
78
+ the extra's URL. The extras file and Codex survey are refreshed before every
79
+ run, so deleting a grant or installing a new server takes effect immediately
80
+ in a long-lived daemon. Literal header and stdio environment secrets from the
81
+ Claude-shaped extras file are moved into Codex's child environment instead of
82
+ being exposed in process arguments.
83
+
84
+ One gap stays open and is documented rather than papered over: a `--exec`
85
+ running Codex under a named profile (`-p work`) can have MCP servers defined
86
+ only in `work.config.toml`, and `codex mcp list` cannot be asked about a
87
+ profile at all — `-p`/`--profile` are rejected and `-c profile="…"` is refused
88
+ as a removed legacy form. The CLI now fails the run before spawning Codex
89
+ when it sees a profile flag, including clap's joined `-pwork` spelling. It
90
+ also fails closed on `-c` / `--config`, feature toggles,
91
+ `--ignore-user-config`, and cwd-changing `-C` / `--cd` flags, because those
92
+ can make the real run's plugin/MCP inventory differ from the plain verifier
93
+ command. Finally, an
94
+ unrecognised item inside otherwise-valid `mcp list --json` output now makes
95
+ the entire survey unknown instead of being silently mistaken for no server.
96
+ Recognisable Codex commands containing shell quoting, expansion, glob,
97
+ comment or control syntax now fail before spawn instead of falling through to
98
+ the unshaped path; in particular, quoting an inventory-changing flag cannot
99
+ hide it from the verifier. The final probe must also report the ProhostAI
100
+ server and every accepted extra enabled — a successful empty or partial list
101
+ is no longer accepted as proof. Finally, all environment names referenced by
102
+ every extra are reserved before literal-header secret variables are
103
+ allocated, preventing one extra from naming and reading another extra's
104
+ generated credential, including through differently-cased aliases on Windows.
105
+ Ordinary unquoted Windows executable and workspace paths remain accepted by
106
+ the shell-syntax guard while `cmd.exe` expansion and control syntax still
107
+ fail closed.
108
+ - **Codex reports refused tools.** Also filed under "no equivalent" in 0.2.2.
109
+ Codex has no `permission_denials` field, but a refused call arrives as an
110
+ `mcp_tool_call` item with `status: "failed"` and `user cancelled MCP tool
111
+ call`, which is what every run with nobody at the terminal produces. Those are
112
+ now printed on the same line the Claude Code path uses. A call that failed for
113
+ another reason — a server returning 502 — is deliberately not counted, so the
114
+ log doesn't send you to your permissions over a network problem.
115
+ - **Codex is told how it spells its own tools.** The prompt used to name the MCP
116
+ server and leave the prefix out, because guessing a tool name is the same
117
+ failure as guessing an endpoint. Measured against the real binary by
118
+ registering a stub server and asking the model what it had just called: Codex
119
+ uses the same `mcp__<server>__<tool>` convention as Claude Code, so both
120
+ brains now get the pattern.
121
+
7
122
  ## 0.2.2
8
123
 
9
124
  Two agents paired to the same workspace behaved like different products: the
package/README.md CHANGED
@@ -179,23 +179,88 @@ word of the command *and* print mode is asked for. Anything else — `npx claude
179
179
  redirect, or `&&` — keeps the plain stdin/stdout contract untouched. So
180
180
  `--exec 'sh -c "claude -p"'` is the way to opt out.
181
181
 
182
+ ### Giving the agent other MCP servers
183
+
184
+ One server by default is a boundary, not an oversight: a run executes with
185
+ permission prompts off, on prompt text written by whoever mentioned your agent.
186
+ Every MCP server reachable from that run is a server your workspace teammates
187
+ can drive.
188
+
189
+ If you want to widen it anyway, create `~/.prohost/extra-mcp.json` by hand. The
190
+ CLI never creates this file — its existence is you saying yes on purpose. The
191
+ shape is the same `mcpServers` object Claude Code's `--mcp-config` takes, so you
192
+ can lift a block straight out of an existing `.mcp.json`:
193
+
194
+ ```json
195
+ {
196
+ "mcpServers": {
197
+ "linear": {
198
+ "type": "http",
199
+ "url": "https://mcp.linear.app/mcp",
200
+ "headers": { "Authorization": "Bearer lin_api_…" }
201
+ },
202
+ "notes": {
203
+ "command": "npx",
204
+ "args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/me/notes"]
205
+ }
206
+ }
207
+ }
208
+ ```
209
+
210
+ **Read this before you use it.** Every server in that file is exercisable by
211
+ anyone who can trigger your agent — a teammate, a guest whose message routes to
212
+ it — with permission prompts off and no human in the loop. A filesystem server
213
+ scoped at `/` is a filesystem server they can read. Scope each one as narrowly
214
+ as it will go.
215
+
216
+ Works on both Claude Code and Codex. Details:
217
+
218
+ - The name `prohost` is **reserved**. An entry using it is rejected with a line
219
+ in the log and the ProhostAI server stays pointed where pairing put it.
220
+ - **On Codex, a name your own config already uses is also rejected.** Codex
221
+ merges the two definitions rather than replacing one with the other, so an
222
+ extra that changes only the URL would inherit the configured server's headers
223
+ and send its credential to the new host. Rename the extra to use it.
224
+ - Malformed JSON costs you the extras, not the run: the CLI warns and starts
225
+ without them. The same is true of an entry whose value isn't an object.
226
+ - The file is re-read before every run. Deleting an entry revokes it for the
227
+ next invocation of an already-running daemon; no restart is required.
228
+ - Which extras loaded is printed when the grant set changes, so `agent.log` answers "did my
229
+ server actually load" without guessing.
230
+ - On Codex the non-secret parts of each definition become `-c` overrides, which
231
+ means URLs, commands, and arguments land on a command line other users can
232
+ read via `ps`. Literal `headers`, `http_headers`, and stdio `env` values are
233
+ moved to the child environment instead. On Claude Code the definitions go
234
+ into `~/.prohost/mcp.json`, which is `chmod 600`.
235
+ - Also on Codex: a value with something TOML can't express (an infinity, say)
236
+ makes that server skipped with a warning, rather than emitted as an override
237
+ that would fail to parse and take the ProhostAI server down with it. Any
238
+ server *name* works, dots included.
239
+ - Delete the file to go back to one server. There is no flag to disable it,
240
+ because a file you have to write by hand is already the opt-in.
241
+
182
242
  ### If your agent is Codex
183
243
 
184
244
  When `--exec` starts with `codex` and uses the `exec` subcommand, the CLI does
185
245
  the same three things it does for Claude Code, through Codex's own flags. The
186
- detection is just as narrow: interactive `codex`, a wrapper script, a pipeline,
187
- a command that already says `codex exec resume` / `codex exec review`, or one
188
- carrying its own prompt (`codex exec 'Be terse'`) all keep the plain
246
+ detection is just as narrow: interactive `codex`, a wrapper script, a command
247
+ that already says `codex exec resume` / `codex exec review`, or one
248
+ carrying its own unquoted prompt (`codex exec Be-terse`) keep the plain
189
249
  stdin/stdout contract untouched. That last one matters: Codex takes exactly one
190
- prompt positional, so the CLI cannot add its own alongside yours. Put standing
191
- instructions in the agent's ProhostAI profile instead — they arrive at the top
192
- of the prompt, ahead of everything else.
250
+ prompt positional, so the CLI cannot add its own alongside yours. Shell syntax
251
+ inside a recognisable Codex command — quoting, escapes, expansions, globs,
252
+ comments, redirects, pipelines or lists — instead fails before Codex starts.
253
+ The harness cannot safely make a whitespace scan agree with both POSIX shells
254
+ and `cmd.exe`, and treating such a command as plain would silently drop MCP
255
+ isolation. Ordinary unquoted Windows path separators remain supported. Put
256
+ standing instructions in the agent's ProhostAI profile instead — they arrive at
257
+ the top of the prompt, ahead of everything else.
193
258
 
194
259
  | | Claude Code | Codex |
195
260
  | --- | --- | --- |
196
261
  | Reply read from | `--output-format json` | `--json` (JSONL event stream) |
197
262
  | Conversation kept with | `--resume <id>` | `codex exec resume <thread>` |
198
- | Tools wired via | `--mcp-config` | `-c mcp_servers.prohost={…}` |
263
+ | Tools wired via | `--mcp-config` | one verified `-c mcp_servers={…}` override |
199
264
  | API key travels in | `~/.prohost/mcp.json` (`chmod 600`) | the child's environment |
200
265
  | Tools ungated by | `--dangerously-skip-permissions` | `--yolo` |
201
266
 
@@ -210,22 +275,83 @@ does for Claude Code, and it *removes* a bypass flag you wrote yourself rather
210
275
  than just declining to add one, so the gating it promises is real. (It covers
211
276
  the full bypass only; `--full-auto` still auto-approves inside Codex's sandbox.)
212
277
 
213
- **Two things Codex cannot do that Claude Code can**, both worth knowing before
214
- you leave it running:
215
-
216
- - **Your own MCP servers come along.** There is no Codex equivalent of
217
- `--strict-mcp-config`. `--ignore-user-config` looks like one but drops the
218
- `-c` overrides too, taking the ProhostAI server with them — so a Codex run
219
- also holds whatever servers your `~/.codex/config.toml` defines, with
220
- approvals bypassed. The CLI prints this at start-up so it isn't a surprise.
221
- If that matters, keep `~/.codex/config.toml` free of servers you would not
222
- hand to a teammate's message.
223
- - **Refused tools stay invisible.** Claude Code reports what it was denied and
224
- the CLI prints it; Codex's event stream has no equivalent.
278
+ **Your own MCP servers are switched off for agent runs.** Codex has no
279
+ `--strict-mcp-config`, so the CLI does the same job a different way: before each run
280
+ it asks your binary what servers it has (`codex mcp list --json`) and passes one
281
+ `-c mcp_servers={"<name>"={enabled=false}, …}` covering all of them. A run then
282
+ holds the ProhostAI server, plus anything in your `extra-mcp.json`, and nothing
283
+ else — the same boundary the Claude Code path gets. Each server's own definition
284
+ is left intact; only this invocation sees it disabled.
285
+
286
+ Two mechanisms that look like they should do this do not, and are worth naming
287
+ so nobody reaches for them. Replacing the `mcp_servers` table does not replace
288
+ it — Codex deep-merges config overrides, so your servers survive alongside
289
+ whatever the override adds. And `--ignore-user-config` works, but throws away
290
+ your model, profile and sandbox settings along with your servers.
291
+
292
+ The enumeration asks the binary rather than reading `~/.codex/config.toml`,
293
+ because that file is not the whole story: on a normal install `codex mcp list`
294
+ reports servers that appear nowhere in it. It also handles names a dotted-path
295
+ override cannot address — a server called `fly.io` written as
296
+ `-c mcp_servers.fly.io.enabled=false` addresses a three-segment path and Codex
297
+ rejects the whole config, which is why the name goes inside the value instead.
298
+ Every probe also runs in the exact directory the eventual Codex process will
299
+ use. That matters because a trusted project's `.codex/config.toml` is another
300
+ config layer: probing where the harness was launched and then running in
301
+ `$PROHOST_HOME/workspace` or an explicit `--workdir` would certify one MCP
302
+ inventory and execute another.
303
+
304
+ **The override is checked before it is used**, and one case needs it.
305
+ `{enabled=false}` merges onto a server's existing definition, and a server
306
+ contributed by a **plugin** has no `mcp_servers` entry to merge onto — so the
307
+ override leaves a table with no transport and Codex refuses to start at all.
308
+ The CLI therefore runs the disable past `codex mcp list` first, falls back to
309
+ checking each server individually if the combined form is rejected, and then
310
+ verifies the exact final override. If any configured server cannot be disabled,
311
+ or if the final listing does not show the ProhostAI server and every accepted
312
+ extra enabled, the run fails before Codex starts. Remove that server/plugin or
313
+ use Claude Code.
314
+
315
+ If the enumeration fails outright — an older Codex without `mcp list --json`,
316
+ or output the CLI doesn't recognise — the run fails before Codex starts. A
317
+ paired run never proceeds under `--yolo` when its MCP boundary cannot be proven.
318
+ Nothing here changes what your Codex config does outside agent runs.
319
+
320
+ **One gap that cannot be closed from here: profiles.** If your `--exec` runs
321
+ Codex under a named profile (`codex exec -p work`), that profile's
322
+ `work.config.toml` can define MCP servers of its own — and `codex mcp list` has
323
+ no way to be asked about a profile (`-p` and `--profile` are rejected, and the
324
+ `-c profile="…"` form is refused as a removed legacy spelling). Servers defined
325
+ only inside a profile are therefore invisible to the sweep. The CLI fails the
326
+ run before Codex starts when it sees a profile flag; run the agent without `-p`.
327
+
328
+ The same fail-closed rule applies to `-c` / `--config`, `--enable`, `--disable`,
329
+ `--ignore-user-config`, and `-C` / `--cd` in `--exec`. Those flags can change
330
+ config, plugin loading, or the project config discovery directory for the real
331
+ invocation, while the separate `codex mcp list` verifier cannot safely replay
332
+ an arbitrary shell command's arguments. Use the harness's `--workdir` when you
333
+ need a different directory; dedicated flags such as `--model` and `--sandbox`
334
+ remain supported.
335
+
336
+ The survey is retaken before every invocation, because
337
+ `install-daemon` runs for weeks and Codex rereads its configuration on every
338
+ invocation — a server you install on Tuesday would otherwise be reachable from
339
+ the next run. The log stays quiet unless the answer changes.
340
+
341
+ **Refused tools are reported**, as of 0.2.3. Codex has no `permission_denials`
342
+ field, but a refused call arrives as an `mcp_tool_call` item with
343
+ `status: "failed"` and `user cancelled MCP tool call`, and the CLI prints those
344
+ in the same line the Claude Code path uses. A tool call that failed for any
345
+ other reason — an MCP server that 500s — is deliberately not reported as a
346
+ denial, so the log doesn't send you to your permissions when the problem is
347
+ your network.
225
348
 
226
349
  Your ProhostAI API key is passed to Codex in the child process's environment
227
350
  rather than in the `-c` override, because a command line is readable by anyone
228
- on the machine via `ps` and a process environment is not.
351
+ on the machine via `ps` and a process environment is not. Literal `headers`,
352
+ `http_headers`, and stdio `env` values in `extra-mcp.json` get the same
353
+ treatment: the CLI translates them to Codex's environment-indirection fields,
354
+ so their values do not appear in process arguments.
229
355
 
230
356
  Runs are handled one at a time, and each run executes **at most once on this
231
357
  machine** — even across restarts. Started runs are recorded in
@@ -363,6 +489,7 @@ Everything the CLI stores lives under `~/.prohost` (`$PROHOST_HOME` moves it):
363
489
  | `runs.json` | Runs already executed on this machine, so a redelivery can't run your agent twice. |
364
490
  | `sessions.json` | Conversation → Claude Code session / Codex thread, for continuity. Safe to delete; costs the agent its memory of open threads. |
365
491
  | `mcp.json` | Generated MCP config. Holds your API key — `chmod 600`. |
492
+ | `extra-mcp.json` | Optional, written by you. Extra MCP servers to expose to agent runs — see "Giving the agent other MCP servers". Never created by the CLI. |
366
493
  | `workspace/` | Where your agent command runs by default. |
367
494
  | `agent.log` | Daemon output. |
368
495
 
@@ -87,7 +87,10 @@ export interface ClaudeFlags {
87
87
  * servers in the operator's own configuration with ours, so a run answering a
88
88
  * customer's question would hold whatever MCP servers they use for their day
89
89
  * job — with permission checks off. Ours is the only one this run is entitled
90
- * to.
90
+ * to, plus whatever the operator listed in `extra-mcp.json` — a file they had
91
+ * to write by hand, which is the point: the boundary still holds, and crossing
92
+ * it is a decision somebody made on purpose rather than one their day-job
93
+ * config made for them.
91
94
  */
92
95
  export declare function claudeCommand(base: string, flags?: ClaudeFlags): string;
93
96
  /** What the harness got back from one Claude Code invocation. */
@@ -131,10 +134,25 @@ export declare function looksLikeResumeFailure(output: string, sessionId: string
131
134
  /** Absolute path of the generated MCP config. */
132
135
  export declare function mcpConfigPath(env?: NodeJS.ProcessEnv): string;
133
136
  export interface McpConfigOptions {
134
- /** The MCP endpoint the pairing reported, e.g. `https://connect.prohost.ai/mcp`. */
135
- mcpUrl: string;
137
+ /**
138
+ * The MCP endpoint the pairing reported, e.g. `https://connect.prohost.ai/mcp`.
139
+ *
140
+ * Optional, because a pairing can carry none — and a run with no ProhostAI
141
+ * server is still a run whose operator may have opted into extras. Omitted,
142
+ * the generated config holds only those, and `--strict-mcp-config` still
143
+ * keeps everything else out.
144
+ */
145
+ mcpUrl?: string;
136
146
  /** The paired API key. Written to a `0600` file and nowhere else. */
137
147
  apiKey: string;
148
+ /**
149
+ * Servers the operator opted into via `$PROHOST_HOME/extra-mcp.json`.
150
+ *
151
+ * Merged in *under* ours, never over it: {@link loadExtraMcpServers} already
152
+ * refuses an entry named `prohost`, and writing ours last means even a future
153
+ * bug there cannot end with a run's workspace tools pointed somewhere else.
154
+ */
155
+ extraServers?: Record<string, Record<string, unknown>>;
138
156
  env?: NodeJS.ProcessEnv;
139
157
  }
140
158
  /**
@@ -87,7 +87,10 @@ export function isClaudeExec(command) {
87
87
  * servers in the operator's own configuration with ours, so a run answering a
88
88
  * customer's question would hold whatever MCP servers they use for their day
89
89
  * job — with permission checks off. Ours is the only one this run is entitled
90
- * to.
90
+ * to, plus whatever the operator listed in `extra-mcp.json` — a file they had
91
+ * to write by hand, which is the point: the boundary still holds, and crossing
92
+ * it is a decision somebody made on purpose rather than one their day-job
93
+ * config made for them.
91
94
  */
92
95
  export function claudeCommand(base, flags = {}) {
93
96
  const platform = flags.platform ?? process.platform;
@@ -268,8 +271,11 @@ export function mcpConfigPath(env = process.env) {
268
271
  * the filesystem, and so the header scheme is stated in exactly one place.
269
272
  */
270
273
  export function buildMcpConfig(options) {
274
+ if (!options.mcpUrl)
275
+ return { mcpServers: { ...(options.extraServers ?? {}) } };
271
276
  return {
272
277
  mcpServers: {
278
+ ...(options.extraServers ?? {}),
273
279
  [MCP_SERVER_NAME]: {
274
280
  type: 'http',
275
281
  // Verbatim, exactly as pairing reported it. An earlier revision appended