@prohost/cli 0.4.0 → 0.4.1

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,39 @@ 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.4.1
8
+
9
+ Codex runs work again on machines with a Codex **plugin** installed.
10
+
11
+ - **Plugin-provided MCP servers are now isolated instead of blocking the run.**
12
+ A plugin declared in `~/.codex/config.toml` — `[plugins."github@openai-curated"]`
13
+ — contributes an MCP server (`github`) that the per-server `mcp_servers`
14
+ disable cannot touch: it has no `mcp_servers` entry for `{enabled=false}` to
15
+ merge onto, so the override leaves a table with no transport and Codex
16
+ rejects the whole config with `invalid transport in mcp_servers.github`. The
17
+ isolation verifier correctly refused to start a run it could not prove was
18
+ sealed, which meant **every** message to a Codex-backed agent failed with
19
+ `MCP isolation could not be established`. Such servers are now shut off with
20
+ `-c features.plugins=false`, verified by re-listing exactly like every other
21
+ disable.
22
+ - **Only when a plugin actually supplies a server.** An install whose plugins
23
+ contribute no MCP server issues byte-identical commands to 0.4.0 and keeps
24
+ its plugin skills and hooks; the switch is a bigger hammer than the problem
25
+ on those machines. Classic `mcp_servers` entries are still disabled
26
+ individually, and the ProhostAI server and any `extra-mcp.json` servers keep
27
+ working with plugins off.
28
+ - **Nothing is trusted that wasn't verified.** Every per-plugin form that looks
29
+ like it should work — `plugins."github@openai-curated".enabled=false`, the
30
+ same written as a table, `.disabled=true`, a whole-table `plugins={…}` —
31
+ loads without error and changes nothing at all. A silent no-op is the worst
32
+ shape a security override can have, and only the re-list distinguishes it
33
+ from one that took.
34
+ - **The failure message now carries the way out.** When a server still cannot
35
+ be disabled, the error names the remedy — remove it from
36
+ `~/.codex/config.toml`, or run the daemon with a dedicated `CODEX_HOME`
37
+ holding only `auth.json` — rather than leaving the operator with a run that
38
+ will not start and nothing to do about it.
39
+
7
40
  ## 0.4.0
8
41
 
9
42
  The wall clock stops being the safety mechanism. 0.3.0 raised the run ceiling
package/README.md CHANGED
@@ -301,16 +301,47 @@ config layer: probing where the harness was launched and then running in
301
301
  `$PROHOST_HOME/workspace` or an explicit `--workdir` would certify one MCP
302
302
  inventory and execute another.
303
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.
304
+ **The override is checked before it is used.** `{enabled=false}` merges onto a
305
+ server's existing definition, so an entry Codex cannot make sense of takes the
306
+ whole config down rather than disabling anything. The CLI therefore runs the
307
+ disable past `codex mcp list` first, falls back to checking each server
308
+ individually if the combined form is rejected, and then verifies the exact final
309
+ override. If any configured server cannot be disabled, or if the final listing
310
+ does not show the ProhostAI server and every accepted extra enabled, the run
311
+ fails before Codex starts.
312
+
313
+ **Codex plugins get a second mechanism, because `mcp_servers` cannot reach
314
+ them.** A plugin declared in your config — say `[plugins."github@openai-curated"]`
315
+ — contributes an MCP server (`github`) that has *no* `mcp_servers` entry for
316
+ `{enabled=false}` to merge onto, so forcing one leaves a table with no transport
317
+ and Codex rejects the config outright: `invalid transport in mcp_servers.github`.
318
+ Nothing in `codex mcp list --json` marks a server as plugin-provided, so the CLI
319
+ measures instead of guessing: it lists again with plugins off, and the names that
320
+ vanish were the plugin's. Those are shut off for the run with
321
+ `-c features.plugins=false`, verified by re-listing like any other disable.
322
+
323
+ This is applied **only when a plugin actually supplies an MCP server**. Plugins
324
+ also carry skills and hooks, and switching the system off costs you those, so an
325
+ install whose plugins contribute no server keeps everything and gets exactly the
326
+ command line it got before. Your own `prohost` server and your `extra-mcp.json`
327
+ servers are unaffected either way. Per-plugin spellings are deliberately not
328
+ used: `plugins."github@openai-curated".enabled=false` and every variant of it
329
+ load without error and disable nothing at all, which is precisely the kind of
330
+ override the verification step exists to catch.
331
+
332
+ **If a server still cannot be disabled**, the run fails before Codex starts and
333
+ names it. Two ways out: remove that server or plugin from `~/.codex/config.toml`,
334
+ or give the daemon a `CODEX_HOME` of its own that holds only `auth.json` —
335
+
336
+ ```bash
337
+ mkdir -p ~/.prohost/codex-home
338
+ cp ~/.codex/auth.json ~/.prohost/codex-home/auth.json
339
+ CODEX_HOME=~/.prohost/codex-home prohost agent run --exec 'codex exec'
340
+ ```
341
+
342
+ A dedicated `CODEX_HOME` is the cleanest answer in general: the agent inherits
343
+ your login and nothing else, your own Codex setup is untouched, and no future
344
+ plugin or server you install can change what a paired run can reach.
314
345
 
315
346
  If the enumeration fails outright — an older Codex without `mcp list --json`,
316
347
  or output the CLI doesn't recognise — the run fails before Codex starts. A
@@ -190,7 +190,7 @@ export type CommandRunner = (command: string, cwd?: string) => {
190
190
  * nothing to disable, while `null` means we do not know, and a run that cannot
191
191
  * enumerate must say so rather than claim an isolation it did not apply.
192
192
  */
193
- export declare function listMcpServers(binary: string, runner: CommandRunner, override?: string, cwd?: string): CodexMcpServer[] | null;
193
+ export declare function listMcpServers(binary: string, runner: CommandRunner, overrides?: string | string[], cwd?: string): CodexMcpServer[] | null;
194
194
  /** Disable entries proven to work, and the servers that could not be disabled. */
195
195
  export interface VerifiedDisable {
196
196
  entries: string[];
@@ -216,8 +216,81 @@ export interface VerifiedDisable {
216
216
  * `failed` is the honest part. A server that cannot be disabled is still
217
217
  * reachable from the run, and the caller has to say so out loud rather than
218
218
  * report an isolation it only partly achieved.
219
+ *
220
+ * `baseOverrides` are config overrides the run will also carry — in practice
221
+ * {@link PLUGINS_DISABLED_OVERRIDE}. They have to be present on every probe
222
+ * here, because "does this entry disable this server" has a different answer
223
+ * under a different config, and the answer that matters is the one for the
224
+ * config the run actually gets.
219
225
  */
220
- export declare function disableEntriesThatLoad(binary: string, runner: CommandRunner, targets: string[], cwd?: string): VerifiedDisable;
226
+ export declare function disableEntriesThatLoad(binary: string, runner: CommandRunner, targets: string[], cwd?: string, baseOverrides?: string[]): VerifiedDisable;
227
+ /**
228
+ * The one override that switches off Codex's plugin system for a single run.
229
+ *
230
+ * Needed because a Codex **plugin** contributes MCP servers that the
231
+ * `mcp_servers` mechanism cannot touch. `[plugins."github@openai-curated"]` in
232
+ * `~/.codex/config.toml` puts a `github` server in `codex mcp list` that has no
233
+ * `mcp_servers` entry to merge `{enabled=false}` onto, so the override defines
234
+ * a table with no transport and Codex rejects the entire config: `invalid
235
+ * transport in mcp_servers.github`. That is the failure this harness shipped
236
+ * with — fail-closed and correct, but it meant no run at all on any machine
237
+ * with a Codex plugin installed.
238
+ *
239
+ * Measured against `codex-cli 0.146.0`, and the measurements are the reason
240
+ * this is a whole-system switch rather than the per-plugin one it should be:
241
+ *
242
+ * - `plugins."github@openai-curated".enabled=false`, the same spelled as a
243
+ * table, `plugins.github.enabled=false`, `.disabled=true`, `.mcp_servers={}`
244
+ * and a whole-table `plugins={…}` **all load without error and change
245
+ * nothing** — the server is still listed, still enabled. A silent no-op is
246
+ * the worst shape a security override can have, and it is exactly why
247
+ * {@link disableEntriesThatLoad} verifies instead of trusting.
248
+ * - `features.plugins` is a *stable* Codex feature, on by default. Setting it
249
+ * false removes plugin-contributed servers from `codex mcp list --json`
250
+ * **entirely** (they are absent, not `enabled: false`) while leaving classic
251
+ * `mcp_servers` entries and this harness's own `prohost` server untouched.
252
+ * - The effect is real at runtime, not just in the listing: the same run asked
253
+ * for its tool list reported 28 `mcp__codex_apps__*` tools without this
254
+ * override and none with it.
255
+ * - `--ignore-user-config` also works, and is still the wrong tool: it drops
256
+ * the operator's model, profile and sandbox settings too.
257
+ *
258
+ * Applied only when a plugin actually contributes a server (see
259
+ * {@link isolateConfiguredServers}). Plugins carry skills and hooks as well, so
260
+ * an install whose plugins contribute no MCP servers keeps all of that.
261
+ */
262
+ export declare const PLUGINS_DISABLED_OVERRIDE = "features.plugins=false";
263
+ /** A verified isolation: what to pass Codex, and what could not be shut off. */
264
+ export interface VerifiedIsolation {
265
+ /** `mcp_servers` fragments, for the single {@link mcpServersOverride}. */
266
+ entries: string[];
267
+ /** Whole-config `-c` overrides the run needs on top. Usually empty. */
268
+ configOverrides: string[];
269
+ /** Servers still reachable from the run. Empty is the outcome we want. */
270
+ failed: string[];
271
+ }
272
+ /**
273
+ * Shut off every server the operator's install has, whatever supplies it.
274
+ *
275
+ * Two mechanisms, because Codex has two kinds of MCP server and one lever does
276
+ * not move both: `mcp_servers` entries are disabled individually
277
+ * ({@link disableEntriesThatLoad}), plugin-contributed ones only by
278
+ * {@link PLUGINS_DISABLED_OVERRIDE}. Nothing in `codex mcp list --json` says
279
+ * which is which — a plugin's server is reported with the same fields as any
280
+ * other, no source or plugin marker anywhere in the object — so provenance is
281
+ * *measured* rather than read: list again with plugins off, and the names that
282
+ * vanished were the plugin's.
283
+ *
284
+ * The classic path is tried first and, when it succeeds, returned untouched
285
+ * with no extra override. So an install with no plugin-backed servers issues
286
+ * byte-identical commands to the version before plugins were handled at all,
287
+ * and pays exactly one probe as before.
288
+ *
289
+ * Failing to shut a server off is reported, never worked around. A plugin
290
+ * server that survives even `features.plugins=false` is a mechanism we do not
291
+ * understand yet, and a run that cannot prove the boundary must not start.
292
+ */
293
+ export declare function isolateConfiguredServers(binary: string, runner: CommandRunner, targets: string[], cwd?: string): VerifiedIsolation;
221
294
  /**
222
295
  * The `codex` binary out of the host's `--exec` command.
223
296
  *
@@ -267,6 +340,14 @@ export interface CodexFlags {
267
340
  * {@link buildMcpOverride} for what happens when they don't.
268
341
  */
269
342
  mcpEntries?: string[];
343
+ /**
344
+ * Whole-config `-c` overrides that are not `mcp_servers` — in practice
345
+ * {@link PLUGINS_DISABLED_OVERRIDE}, when the operator has a plugin that
346
+ * contributes MCP servers. They precede the `mcp_servers` override for the
347
+ * same reason they do in the verifier: the run has to be handed the same
348
+ * overrides in the same order the probe certified.
349
+ */
350
+ configOverrides?: string[];
270
351
  /**
271
352
  * Run with approvals and the sandbox bypassed. Default ON — see
272
353
  * {@link codexCommand}. Set false by `agent run --safe-tools`.
@@ -485,8 +485,14 @@ export function mcpServersOverride(entries) {
485
485
  * nothing to disable, while `null` means we do not know, and a run that cannot
486
486
  * enumerate must say so rather than claim an isolation it did not apply.
487
487
  */
488
- export function listMcpServers(binary, runner, override, cwd) {
489
- const command = override ? `${binary} mcp list --json -c ${shellQuote(override, process.platform)}` : `${binary} mcp list --json`;
488
+ export function listMcpServers(binary, runner, overrides, cwd) {
489
+ // Several overrides rather than one, because they land in different config
490
+ // tables: `mcp_servers={…}` cannot also carry `features.plugins=false`. Codex
491
+ // applies repeated `-c` in order, and the run must be given this same list in
492
+ // this same order or the probe certifies a config the run does not get.
493
+ const list = overrides === undefined ? [] : typeof overrides === 'string' ? [overrides] : overrides;
494
+ const flags = list.map((value) => ` -c ${shellQuote(value, process.platform)}`).join('');
495
+ const command = `${binary} mcp list --json${flags}`;
490
496
  let result;
491
497
  try {
492
498
  result = runner(command, cwd);
@@ -537,15 +543,21 @@ export function listMcpServers(binary, runner, override, cwd) {
537
543
  * `failed` is the honest part. A server that cannot be disabled is still
538
544
  * reachable from the run, and the caller has to say so out loud rather than
539
545
  * report an isolation it only partly achieved.
546
+ *
547
+ * `baseOverrides` are config overrides the run will also carry — in practice
548
+ * {@link PLUGINS_DISABLED_OVERRIDE}. They have to be present on every probe
549
+ * here, because "does this entry disable this server" has a different answer
550
+ * under a different config, and the answer that matters is the one for the
551
+ * config the run actually gets.
540
552
  */
541
- export function disableEntriesThatLoad(binary, runner, targets, cwd) {
553
+ export function disableEntriesThatLoad(binary, runner, targets, cwd, baseOverrides = []) {
542
554
  if (targets.length === 0)
543
555
  return { entries: [], failed: [] };
544
556
  const disabled = (names) => {
545
557
  const override = mcpServersOverride(disableMcpEntries(names));
546
558
  if (!override)
547
559
  return [];
548
- const listing = listMcpServers(binary, runner, override, cwd);
560
+ const listing = listMcpServers(binary, runner, [...baseOverrides, override], cwd);
549
561
  // A config that does not load disables nothing, and a server the listing
550
562
  // still reports as enabled was not disabled whatever the override said.
551
563
  if (listing === null)
@@ -570,6 +582,91 @@ export function disableEntriesThatLoad(binary, runner, targets, cwd) {
570
582
  failed: targets.filter((name) => !verified.includes(name)),
571
583
  };
572
584
  }
585
+ /**
586
+ * The one override that switches off Codex's plugin system for a single run.
587
+ *
588
+ * Needed because a Codex **plugin** contributes MCP servers that the
589
+ * `mcp_servers` mechanism cannot touch. `[plugins."github@openai-curated"]` in
590
+ * `~/.codex/config.toml` puts a `github` server in `codex mcp list` that has no
591
+ * `mcp_servers` entry to merge `{enabled=false}` onto, so the override defines
592
+ * a table with no transport and Codex rejects the entire config: `invalid
593
+ * transport in mcp_servers.github`. That is the failure this harness shipped
594
+ * with — fail-closed and correct, but it meant no run at all on any machine
595
+ * with a Codex plugin installed.
596
+ *
597
+ * Measured against `codex-cli 0.146.0`, and the measurements are the reason
598
+ * this is a whole-system switch rather than the per-plugin one it should be:
599
+ *
600
+ * - `plugins."github@openai-curated".enabled=false`, the same spelled as a
601
+ * table, `plugins.github.enabled=false`, `.disabled=true`, `.mcp_servers={}`
602
+ * and a whole-table `plugins={…}` **all load without error and change
603
+ * nothing** — the server is still listed, still enabled. A silent no-op is
604
+ * the worst shape a security override can have, and it is exactly why
605
+ * {@link disableEntriesThatLoad} verifies instead of trusting.
606
+ * - `features.plugins` is a *stable* Codex feature, on by default. Setting it
607
+ * false removes plugin-contributed servers from `codex mcp list --json`
608
+ * **entirely** (they are absent, not `enabled: false`) while leaving classic
609
+ * `mcp_servers` entries and this harness's own `prohost` server untouched.
610
+ * - The effect is real at runtime, not just in the listing: the same run asked
611
+ * for its tool list reported 28 `mcp__codex_apps__*` tools without this
612
+ * override and none with it.
613
+ * - `--ignore-user-config` also works, and is still the wrong tool: it drops
614
+ * the operator's model, profile and sandbox settings too.
615
+ *
616
+ * Applied only when a plugin actually contributes a server (see
617
+ * {@link isolateConfiguredServers}). Plugins carry skills and hooks as well, so
618
+ * an install whose plugins contribute no MCP servers keeps all of that.
619
+ */
620
+ export const PLUGINS_DISABLED_OVERRIDE = 'features.plugins=false';
621
+ /**
622
+ * Shut off every server the operator's install has, whatever supplies it.
623
+ *
624
+ * Two mechanisms, because Codex has two kinds of MCP server and one lever does
625
+ * not move both: `mcp_servers` entries are disabled individually
626
+ * ({@link disableEntriesThatLoad}), plugin-contributed ones only by
627
+ * {@link PLUGINS_DISABLED_OVERRIDE}. Nothing in `codex mcp list --json` says
628
+ * which is which — a plugin's server is reported with the same fields as any
629
+ * other, no source or plugin marker anywhere in the object — so provenance is
630
+ * *measured* rather than read: list again with plugins off, and the names that
631
+ * vanished were the plugin's.
632
+ *
633
+ * The classic path is tried first and, when it succeeds, returned untouched
634
+ * with no extra override. So an install with no plugin-backed servers issues
635
+ * byte-identical commands to the version before plugins were handled at all,
636
+ * and pays exactly one probe as before.
637
+ *
638
+ * Failing to shut a server off is reported, never worked around. A plugin
639
+ * server that survives even `features.plugins=false` is a mechanism we do not
640
+ * understand yet, and a run that cannot prove the boundary must not start.
641
+ */
642
+ export function isolateConfiguredServers(binary, runner, targets, cwd) {
643
+ const direct = disableEntriesThatLoad(binary, runner, targets, cwd);
644
+ if (direct.failed.length === 0)
645
+ return { ...direct, configOverrides: [] };
646
+ // Something resisted. Ask whether turning the plugin system off accounts for
647
+ // it, rather than assuming the survivors are plugin-backed because they look
648
+ // like it.
649
+ const withPluginsOff = listMcpServers(binary, runner, PLUGINS_DISABLED_OVERRIDE, cwd);
650
+ // The override does not load, so it disables nothing and we know nothing.
651
+ if (withPluginsOff === null)
652
+ return { ...direct, configOverrides: [] };
653
+ const remaining = new Set(withPluginsOff.map((server) => server.name));
654
+ const removedByPluginsOff = direct.failed.filter((name) => !remaining.has(name));
655
+ // Plugins were not the problem. Report what the classic path found, so the
656
+ // operator is told about the real survivor instead of a plugin red herring.
657
+ if (removedByPluginsOff.length === 0)
658
+ return { ...direct, configOverrides: [] };
659
+ // Re-verify the classic disables *under* the plugins-off override. They were
660
+ // proven against a different config, and a proof about a config the run does
661
+ // not get is not a proof.
662
+ const classic = targets.filter((name) => remaining.has(name));
663
+ const verified = disableEntriesThatLoad(binary, runner, classic, cwd, [PLUGINS_DISABLED_OVERRIDE]);
664
+ return {
665
+ entries: verified.entries,
666
+ configOverrides: [PLUGINS_DISABLED_OVERRIDE],
667
+ failed: verified.failed,
668
+ };
669
+ }
573
670
  /**
574
671
  * The `codex` binary out of the host's `--exec` command.
575
672
  *
@@ -705,6 +802,8 @@ export function codexCommand(base, flags = {}) {
705
802
  if (!gated && !has(words, ...BYPASS_FLAGS)) {
706
803
  parts.push('--yolo');
707
804
  }
805
+ for (const value of flags.configOverrides ?? [])
806
+ parts.push('-c', quote(value));
708
807
  const override = mcpServersOverride(flags.mcpEntries ?? []);
709
808
  if (override)
710
809
  parts.push('-c', quote(override));
@@ -146,6 +146,8 @@ interface PreparedMcp {
146
146
  mcpConfigPath?: string;
147
147
  /** Codex: entries in the single verified `mcp_servers` override. */
148
148
  codexMcpEntries?: string[];
149
+ /** Codex: verified non-`mcp_servers` overrides, e.g. plugins off. */
150
+ codexConfigOverrides?: string[];
149
151
  /** Credentials moved out of Codex's process argv. */
150
152
  execEnv?: NodeJS.ProcessEnv;
151
153
  /** ProhostAI tools are actually reachable from this invocation. */
package/dist/agent/run.js CHANGED
@@ -27,7 +27,7 @@ import { spawnSync } from 'node:child_process';
27
27
  import WebSocket from 'ws';
28
28
  import { completeRun, postReply } from './api.js';
29
29
  import { MAX_SALVAGE_CHARS, claudeCommand, isClaudeExec, looksLikeResumeFailure, readClaudeStream, writeMcpConfig, } from './claude.js';
30
- import { MCP_API_KEY_ENV_VAR, codexBinary, codexCommand, codexMcpInventoryFlag, codexProfileName, codexShellSyntax, disableEntriesThatLoad, extraMcpEntries, isCodexExec, listMcpServers, mcpServersOverride, prohostMcpEntry, looksLikeCodexResumeFailure, parseCodexOutput, } from './codex.js';
30
+ import { MCP_API_KEY_ENV_VAR, PLUGINS_DISABLED_OVERRIDE, codexBinary, codexCommand, codexMcpInventoryFlag, codexProfileName, codexShellSyntax, extraMcpEntries, isCodexExec, isolateConfiguredServers, listMcpServers, mcpServersOverride, prohostMcpEntry, looksLikeCodexResumeFailure, parseCodexOutput, } from './codex.js';
31
31
  import { EXTRA_MCP_FILENAME, loadExtraMcpServers } from './extras.js';
32
32
  import { MCP_SERVER_NAME, MCP_TOOL_PATTERN } from './mcp.js';
33
33
  import { EVENT_AGENT_RUN_REQUESTED, EVENT_MENTION_CREATED, EVENT_MESSAGE_TEAM_CHAT, RUN_ERROR_CANCELLED_BY_USER, eventData, heartbeatPathFor, parseRunRequest, } from './contract.js';
@@ -329,19 +329,28 @@ function codexMcpEntries(options, extraServers, log, workdir) {
329
329
  if (acceptedExtraEntries.length === 0)
330
330
  acceptedExtraNames.clear();
331
331
  const targets = [...configuredNames].filter((name) => !acceptedExtraNames.has(name));
332
- const { entries: disabled, failed } = disableEntriesThatLoad(binary, probe, targets, workdir);
332
+ const { entries: disabled, configOverrides, failed } = isolateConfiguredServers(binary, probe, targets, workdir);
333
333
  if (failed.length > 0) {
334
- return fail(`these Codex MCP servers could not be disabled: ${failed.join(', ')}`);
334
+ // The remedy belongs in the message. This string is echoed into the thread
335
+ // the teammate is waiting in, and "could not be disabled" on its own leaves
336
+ // them with a run that will not start and nothing to do about it.
337
+ return fail(`these Codex MCP servers could not be disabled: ${failed.join(', ')} — remove them from ` +
338
+ '~/.codex/config.toml, or run the daemon with a dedicated CODEX_HOME containing only auth.json');
335
339
  }
336
340
  entries.push(...disabled);
337
- if (disabled.length > 0) {
341
+ if (targets.length > 0) {
338
342
  log(`${timestamp()} ⚙ your own Codex MCP servers are disabled for agent runs: ${targets.join(', ')}`);
339
343
  }
344
+ if (configOverrides.includes(PLUGINS_DISABLED_OVERRIDE)) {
345
+ log(`${timestamp()} ⚙ Codex plugins are switched off for agent runs — a plugin supplies an MCP ` +
346
+ 'server that cannot be disabled on its own');
347
+ }
340
348
  // Verify the exact combined override, not its pieces. This catches a config
341
349
  // merge that re-enables a server and a server installed between the initial
342
350
  // survey and this final probe.
343
351
  const override = mcpServersOverride(entries);
344
- const finalListing = listMcpServers(binary, probe, override, workdir);
352
+ const runOverrides = [...configOverrides, ...(override ? [override] : [])];
353
+ const finalListing = listMcpServers(binary, probe, runOverrides, workdir);
345
354
  if (finalListing === null)
346
355
  return fail('Codex rejected the final isolated MCP configuration');
347
356
  const allowed = new Set([
@@ -359,6 +368,7 @@ function codexMcpEntries(options, extraServers, log, workdir) {
359
368
  }
360
369
  return {
361
370
  codexMcpEntries: entries,
371
+ codexConfigOverrides: configOverrides.length > 0 ? configOverrides : undefined,
362
372
  execEnv: Object.keys(execEnv).length > 0 ? execEnv : undefined,
363
373
  mcpWired: Boolean(options.credentials.mcp_url),
364
374
  };
@@ -573,6 +583,7 @@ async function runTurn(run, prompt, options, runtime, mcp, control, log) {
573
583
  ? codexCommand(options.exec, {
574
584
  sessionId: resume,
575
585
  mcpEntries: mcp.codexMcpEntries,
586
+ configOverrides: mcp.codexConfigOverrides,
576
587
  skipPermissions: runtime.skipPermissions,
577
588
  })
578
589
  : claudeCommand(options.exec, {
package/dist/version.d.ts CHANGED
@@ -7,6 +7,6 @@
7
7
  * `package.json`, so a release bump that forgets this file fails the suite
8
8
  * instead of shipping a `User-Agent` that lies about which build is calling.
9
9
  */
10
- export declare const CLI_VERSION = "0.4.0";
10
+ export declare const CLI_VERSION = "0.4.1";
11
11
  /** Sent on every HTTP request the CLI makes back into ProhostAI. */
12
- export declare const USER_AGENT = "prohost-cli/0.4.0";
12
+ export declare const USER_AGENT = "prohost-cli/0.4.1";
package/dist/version.js CHANGED
@@ -7,6 +7,6 @@
7
7
  * `package.json`, so a release bump that forgets this file fails the suite
8
8
  * instead of shipping a `User-Agent` that lies about which build is calling.
9
9
  */
10
- export const CLI_VERSION = '0.4.0';
10
+ export const CLI_VERSION = '0.4.1';
11
11
  /** Sent on every HTTP request the CLI makes back into ProhostAI. */
12
12
  export const USER_AGENT = `prohost-cli/${CLI_VERSION}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prohost/cli",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Run your own AI agent as a ProhostAI teammate, and stream your account's webhooks to your laptop.",
5
5
  "type": "module",
6
6
  "license": "MIT",