multicorn-shield 1.9.2 → 1.9.4

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
@@ -9,11 +9,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  - Bump `version` in `package.json` before publishing to npm.
11
11
 
12
+ ## [1.9.4] - 2026-05-13
13
+
14
+ ### Fixed
15
+
16
+ - Double "Bearer" prefix in Codex CLI hosted proxy TOML snippet (was outputting `Bearer Bearer mcs_...`)
17
+ - Codex CLI hosted proxy now auto-writes MCP server config to `~/.codex/config.toml` instead of asking users to paste manually
18
+ - PreToolUse hook now prints the consent/approval URL to stderr so users know where to approve
19
+
20
+ ### Changed
21
+
22
+ - Codex CLI hosted proxy next-steps updated: "Restart Codex CLI to load the new MCP server config"
23
+ - Added copy-pasteable "Try it out" prompt to Codex CLI hosted proxy next-steps
24
+
25
+ ## [1.9.3] - 2026-05-13
26
+
27
+ ### Fixed
28
+
29
+ - Codex CLI hosted proxy snippet uses `http_headers` with inline API key instead of `bearer_token_env_var` (no env var setup needed)
30
+ - Removed "Set MULTICORN_API_KEY environment variable" instruction from hosted proxy next-steps
31
+ - Added `/mcp` verification step to Codex CLI hosted proxy next-steps
32
+
12
33
  ## [1.9.2] - 2026-05-12
13
34
 
14
35
  ### Fixed
15
36
 
16
- - Codex CLI hosted proxy next-steps now includes /mcp verification step
37
+ - Include `plugins/codex-cli` in published npm package (missing from `files` in package.json)
17
38
 
18
39
  ## [1.9.1] - 2026-05-12
19
40
 
@@ -947,6 +947,68 @@ function getCodexCliHooksInstallDir() {
947
947
  function getCodexConfigTomlPath() {
948
948
  return join(homedir(), ".codex", "config.toml");
949
949
  }
950
+ function escapeTomlDoubleQuotedScalar(value) {
951
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
952
+ }
953
+ function stripCodexMcpServerTomlBlocks(content, serverKey) {
954
+ const lines = content.split(/\r?\n/);
955
+ const mainHeader = `[mcp_servers.${serverKey}]`;
956
+ const headersHeader = `[mcp_servers.${serverKey}.http_headers]`;
957
+ const out = [];
958
+ let state = "idle";
959
+ for (const line of lines) {
960
+ const t = line.trim();
961
+ if (state === "idle") {
962
+ if (t === mainHeader || t === headersHeader) {
963
+ state = t === headersHeader ? "skip_headers" : "skip_main";
964
+ continue;
965
+ }
966
+ out.push(line);
967
+ } else if (state === "skip_main") {
968
+ if (t.startsWith("[") && t.endsWith("]")) {
969
+ if (t === headersHeader) {
970
+ state = "skip_headers";
971
+ } else {
972
+ state = "idle";
973
+ out.push(line);
974
+ }
975
+ }
976
+ } else {
977
+ if (t.startsWith("[") && t.endsWith("]")) {
978
+ state = "idle";
979
+ out.push(line);
980
+ }
981
+ }
982
+ }
983
+ return out.join("\n").trimEnd();
984
+ }
985
+ async function mergeCodexHostedMcpIntoToml(shortName, proxyUrl, apiKey) {
986
+ const configPath = getCodexConfigTomlPath();
987
+ let existing = "";
988
+ try {
989
+ existing = await readFile(configPath, "utf8");
990
+ } catch (err) {
991
+ if (!(isErrnoException(err) && err.code === "ENOENT")) {
992
+ const detail = err instanceof Error ? err.message : String(err);
993
+ throw new Error(`Could not read Codex CLI config at ${configPath}: ${detail}`);
994
+ }
995
+ }
996
+ const stripped = stripCodexMcpServerTomlBlocks(existing, shortName);
997
+ const urlEsc = escapeTomlDoubleQuotedScalar(proxyUrl);
998
+ const tokenEsc = escapeTomlDoubleQuotedScalar(apiKey);
999
+ const block = `[mcp_servers.${shortName}]
1000
+ url = "${urlEsc}"
1001
+
1002
+ [mcp_servers.${shortName}.http_headers]
1003
+ Authorization = "Bearer ${tokenEsc}"
1004
+ `;
1005
+ const trimmedBase = stripped.trimEnd();
1006
+ const full = (trimmedBase.length > 0 ? `${trimmedBase}
1007
+
1008
+ ` : "") + block;
1009
+ await mkdir(dirname(configPath), { recursive: true });
1010
+ await writeFile(configPath, full, SECRET_JSON_FILE_OPTIONS);
1011
+ }
950
1012
  function getCodexHooksJsonPath() {
951
1013
  return join(homedir(), ".codex", "hooks.json");
952
1014
  }
@@ -2153,7 +2215,20 @@ async function applyHostedProxyMcpConfig(platform, proxyUrl, shortName, apiKey,
2153
2215
  printHostedProxyJsonParseWarning(join(workspacePath, "opencode.json"));
2154
2216
  }
2155
2217
  } else if (platform === "codex-cli") {
2156
- printPlatformSnippet(platform, proxyUrl, shortName, apiKey);
2218
+ let codexTomlWritten = false;
2219
+ try {
2220
+ await mergeCodexHostedMcpIntoToml(shortName, proxyUrlWithKeyWhenNeeded, apiKey);
2221
+ codexTomlWritten = true;
2222
+ process.stderr.write(
2223
+ style.green("\u2713 ") + "MCP server config written to " + style.cyan(getCodexConfigTomlPath()) + "\n"
2224
+ );
2225
+ } catch (err) {
2226
+ process.stderr.write(
2227
+ `${style.yellow("!")} Could not auto-write config: ${err instanceof Error ? err.message : String(err)}
2228
+ `
2229
+ );
2230
+ }
2231
+ printPlatformSnippet(platform, proxyUrl, shortName, apiKey, codexTomlWritten);
2157
2232
  return;
2158
2233
  } else if (platform === "continue-dev") {
2159
2234
  result = await mergeContinueHostedMcp(
@@ -2279,7 +2354,7 @@ async function mergeGooseConfig(shortName, proxyUrl, apiKey) {
2279
2354
  writeMcpAddedLine(shortName, filePath);
2280
2355
  return "ok";
2281
2356
  }
2282
- function printPlatformSnippet(platform, routingToken, shortName, apiKey) {
2357
+ function printPlatformSnippet(platform, routingToken, shortName, apiKey, codexCliTomlWritten) {
2283
2358
  const hostedInlinePlatforms = /* @__PURE__ */ new Set([
2284
2359
  "cursor",
2285
2360
  "claude-desktop",
@@ -2393,9 +2468,12 @@ mcpServers:
2393
2468
  2
2394
2469
  );
2395
2470
  } else if (platform === "codex-cli") {
2471
+ const bearerToken = authHeader.startsWith("Bearer ") ? authHeader.slice("Bearer ".length) : authHeader;
2396
2472
  snippetText = `[mcp_servers.${shortName}]
2397
2473
  url = "${urlInSnippet}"
2398
- bearer_token_env_var = "MULTICORN_API_KEY"
2474
+
2475
+ [mcp_servers.${shortName}.http_headers]
2476
+ Authorization = "Bearer ${bearerToken}"
2399
2477
  `;
2400
2478
  } else {
2401
2479
  const urlKey = platform === "windsurf" ? "serverUrl" : "url";
@@ -2441,11 +2519,15 @@ bearer_token_env_var = "MULTICORN_API_KEY"
2441
2519
  ) + "\n\n"
2442
2520
  );
2443
2521
  } else if (platform === "codex-cli") {
2444
- process.stderr.write(
2445
- "\n" + style.dim(
2446
- "Add this to ~/.codex/config.toml (create the file if it does not exist). Set the MULTICORN_API_KEY environment variable to your Shield API key. Restart Codex CLI after saving."
2447
- ) + "\n\n"
2448
- );
2522
+ if (codexCliTomlWritten === true) {
2523
+ process.stderr.write("\n" + style.dim("Added to ~/.codex/config.toml:") + "\n\n");
2524
+ } else {
2525
+ process.stderr.write(
2526
+ "\n" + style.dim(
2527
+ "Add this to ~/.codex/config.toml (create the file if it does not exist). Restart Codex CLI after saving."
2528
+ ) + "\n\n"
2529
+ );
2530
+ }
2449
2531
  } else if (platform === "github-copilot") {
2450
2532
  process.stderr.write(
2451
2533
  "\n" + style.dim(
@@ -3239,11 +3321,6 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
3239
3321
  apiKey,
3240
3322
  initWorkspacePath
3241
3323
  );
3242
- process.stderr.write(
3243
- "\n" + style.dim(
3244
- "Add the TOML snippet above to ~/.codex/config.toml. Then set the environment variable:"
3245
- ) + "\n\n " + style.cyan(`export MULTICORN_API_KEY="${apiKey}"`) + "\n\n" + style.dim("Restart Codex CLI after saving config.toml.") + "\n"
3246
- );
3247
3324
  configuredAgents.push({
3248
3325
  selection,
3249
3326
  platform: selectedPlatform,
@@ -3580,8 +3657,9 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
3580
3657
  );
3581
3658
  }
3582
3659
  if (codexHostedConfigured) {
3660
+ const codexLabel = mcpPromptLabel2("codex-cli");
3583
3661
  blocks.push(
3584
- "\n" + style.bold("Codex CLI (hosted)") + "\n \u2192 Set the MULTICORN_API_KEY environment variable to your Shield API key\n \u2192 Restart Codex CLI after saving config.toml\n \u2192 Verify it's connected: run /mcp in Codex CLI to see your active MCP servers\n \u2192 Try it: make a request that uses an MCP tool through Shield\n"
3662
+ "\n" + style.bold("Codex CLI (hosted)") + "\n \u2192 Restart Codex CLI to load the new MCP server config\n \u2192 Verify it's connected: run /mcp in Codex CLI to see your active MCP servers\n \u2192 Try it - paste this into Codex:\n Use the " + codexLabel + " MCP server to list available tools\n"
3585
3663
  );
3586
3664
  }
3587
3665
  if (configuredPlatforms.has("other-mcp")) {
@@ -4660,7 +4738,7 @@ var init_package = __esm({
4660
4738
  "package.json"() {
4661
4739
  package_default = {
4662
4740
  name: "multicorn-shield",
4663
- version: "1.9.2",
4741
+ version: "1.9.4",
4664
4742
  description: "The control layer for AI agents: permissions, consent, spending limits, and audit logging.",
4665
4743
  license: "MIT",
4666
4744
  author: "Multicorn AI Pty Ltd",
@@ -961,6 +961,68 @@ function getCodexCliHooksInstallDir() {
961
961
  function getCodexConfigTomlPath() {
962
962
  return join(homedir(), ".codex", "config.toml");
963
963
  }
964
+ function escapeTomlDoubleQuotedScalar(value) {
965
+ return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
966
+ }
967
+ function stripCodexMcpServerTomlBlocks(content, serverKey) {
968
+ const lines = content.split(/\r?\n/);
969
+ const mainHeader = `[mcp_servers.${serverKey}]`;
970
+ const headersHeader = `[mcp_servers.${serverKey}.http_headers]`;
971
+ const out = [];
972
+ let state = "idle";
973
+ for (const line of lines) {
974
+ const t = line.trim();
975
+ if (state === "idle") {
976
+ if (t === mainHeader || t === headersHeader) {
977
+ state = t === headersHeader ? "skip_headers" : "skip_main";
978
+ continue;
979
+ }
980
+ out.push(line);
981
+ } else if (state === "skip_main") {
982
+ if (t.startsWith("[") && t.endsWith("]")) {
983
+ if (t === headersHeader) {
984
+ state = "skip_headers";
985
+ } else {
986
+ state = "idle";
987
+ out.push(line);
988
+ }
989
+ }
990
+ } else {
991
+ if (t.startsWith("[") && t.endsWith("]")) {
992
+ state = "idle";
993
+ out.push(line);
994
+ }
995
+ }
996
+ }
997
+ return out.join("\n").trimEnd();
998
+ }
999
+ async function mergeCodexHostedMcpIntoToml(shortName, proxyUrl, apiKey) {
1000
+ const configPath = getCodexConfigTomlPath();
1001
+ let existing = "";
1002
+ try {
1003
+ existing = await readFile(configPath, "utf8");
1004
+ } catch (err) {
1005
+ if (!(isErrnoException(err) && err.code === "ENOENT")) {
1006
+ const detail = err instanceof Error ? err.message : String(err);
1007
+ throw new Error(`Could not read Codex CLI config at ${configPath}: ${detail}`);
1008
+ }
1009
+ }
1010
+ const stripped = stripCodexMcpServerTomlBlocks(existing, shortName);
1011
+ const urlEsc = escapeTomlDoubleQuotedScalar(proxyUrl);
1012
+ const tokenEsc = escapeTomlDoubleQuotedScalar(apiKey);
1013
+ const block = `[mcp_servers.${shortName}]
1014
+ url = "${urlEsc}"
1015
+
1016
+ [mcp_servers.${shortName}.http_headers]
1017
+ Authorization = "Bearer ${tokenEsc}"
1018
+ `;
1019
+ const trimmedBase = stripped.trimEnd();
1020
+ const full = (trimmedBase.length > 0 ? `${trimmedBase}
1021
+
1022
+ ` : "") + block;
1023
+ await mkdir(dirname(configPath), { recursive: true });
1024
+ await writeFile(configPath, full, SECRET_JSON_FILE_OPTIONS);
1025
+ }
964
1026
  function getCodexHooksJsonPath() {
965
1027
  return join(homedir(), ".codex", "hooks.json");
966
1028
  }
@@ -2246,7 +2308,20 @@ async function applyHostedProxyMcpConfig(platform, proxyUrl, shortName, apiKey,
2246
2308
  printHostedProxyJsonParseWarning(join(workspacePath, "opencode.json"));
2247
2309
  }
2248
2310
  } else if (platform === "codex-cli") {
2249
- printPlatformSnippet(platform, proxyUrl, shortName, apiKey);
2311
+ let codexTomlWritten = false;
2312
+ try {
2313
+ await mergeCodexHostedMcpIntoToml(shortName, proxyUrlWithKeyWhenNeeded, apiKey);
2314
+ codexTomlWritten = true;
2315
+ process.stderr.write(
2316
+ style.green("\u2713 ") + "MCP server config written to " + style.cyan(getCodexConfigTomlPath()) + "\n"
2317
+ );
2318
+ } catch (err) {
2319
+ process.stderr.write(
2320
+ `${style.yellow("!")} Could not auto-write config: ${err instanceof Error ? err.message : String(err)}
2321
+ `
2322
+ );
2323
+ }
2324
+ printPlatformSnippet(platform, proxyUrl, shortName, apiKey, codexTomlWritten);
2250
2325
  return;
2251
2326
  } else if (platform === "continue-dev") {
2252
2327
  result = await mergeContinueHostedMcp(
@@ -2372,7 +2447,7 @@ async function mergeGooseConfig(shortName, proxyUrl, apiKey) {
2372
2447
  writeMcpAddedLine(shortName, filePath);
2373
2448
  return "ok";
2374
2449
  }
2375
- function printPlatformSnippet(platform, routingToken, shortName, apiKey) {
2450
+ function printPlatformSnippet(platform, routingToken, shortName, apiKey, codexCliTomlWritten) {
2376
2451
  const hostedInlinePlatforms = /* @__PURE__ */ new Set([
2377
2452
  "cursor",
2378
2453
  "claude-desktop",
@@ -2486,9 +2561,12 @@ mcpServers:
2486
2561
  2
2487
2562
  );
2488
2563
  } else if (platform === "codex-cli") {
2564
+ const bearerToken = authHeader.startsWith("Bearer ") ? authHeader.slice("Bearer ".length) : authHeader;
2489
2565
  snippetText = `[mcp_servers.${shortName}]
2490
2566
  url = "${urlInSnippet}"
2491
- bearer_token_env_var = "MULTICORN_API_KEY"
2567
+
2568
+ [mcp_servers.${shortName}.http_headers]
2569
+ Authorization = "Bearer ${bearerToken}"
2492
2570
  `;
2493
2571
  } else {
2494
2572
  const urlKey = platform === "windsurf" ? "serverUrl" : "url";
@@ -2534,11 +2612,15 @@ bearer_token_env_var = "MULTICORN_API_KEY"
2534
2612
  ) + "\n\n"
2535
2613
  );
2536
2614
  } else if (platform === "codex-cli") {
2537
- process.stderr.write(
2538
- "\n" + style.dim(
2539
- "Add this to ~/.codex/config.toml (create the file if it does not exist). Set the MULTICORN_API_KEY environment variable to your Shield API key. Restart Codex CLI after saving."
2540
- ) + "\n\n"
2541
- );
2615
+ if (codexCliTomlWritten === true) {
2616
+ process.stderr.write("\n" + style.dim("Added to ~/.codex/config.toml:") + "\n\n");
2617
+ } else {
2618
+ process.stderr.write(
2619
+ "\n" + style.dim(
2620
+ "Add this to ~/.codex/config.toml (create the file if it does not exist). Restart Codex CLI after saving."
2621
+ ) + "\n\n"
2622
+ );
2623
+ }
2542
2624
  } else if (platform === "github-copilot") {
2543
2625
  process.stderr.write(
2544
2626
  "\n" + style.dim(
@@ -3333,11 +3415,6 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
3333
3415
  apiKey,
3334
3416
  initWorkspacePath
3335
3417
  );
3336
- process.stderr.write(
3337
- "\n" + style.dim(
3338
- "Add the TOML snippet above to ~/.codex/config.toml. Then set the environment variable:"
3339
- ) + "\n\n " + style.cyan(`export MULTICORN_API_KEY="${apiKey}"`) + "\n\n" + style.dim("Restart Codex CLI after saving config.toml.") + "\n"
3340
- );
3341
3418
  configuredAgents.push({
3342
3419
  selection,
3343
3420
  platform: selectedPlatform,
@@ -3674,8 +3751,9 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
3674
3751
  );
3675
3752
  }
3676
3753
  if (codexHostedConfigured) {
3754
+ const codexLabel = mcpPromptLabel2("codex-cli");
3677
3755
  blocks.push(
3678
- "\n" + style.bold("Codex CLI (hosted)") + "\n \u2192 Set the MULTICORN_API_KEY environment variable to your Shield API key\n \u2192 Restart Codex CLI after saving config.toml\n \u2192 Verify it's connected: run /mcp in Codex CLI to see your active MCP servers\n \u2192 Try it: make a request that uses an MCP tool through Shield\n"
3756
+ "\n" + style.bold("Codex CLI (hosted)") + "\n \u2192 Restart Codex CLI to load the new MCP server config\n \u2192 Verify it's connected: run /mcp in Codex CLI to see your active MCP servers\n \u2192 Try it - paste this into Codex:\n Use the " + codexLabel + " MCP server to list available tools\n"
3679
3757
  );
3680
3758
  }
3681
3759
  if (configuredPlatforms.has("other-mcp")) {
@@ -4583,7 +4661,7 @@ async function restoreClaudeDesktopMcpFromBackup() {
4583
4661
 
4584
4662
  // package.json
4585
4663
  var package_default = {
4586
- version: "1.9.2"};
4664
+ version: "1.9.4"};
4587
4665
 
4588
4666
  // src/package-meta.ts
4589
4667
  var PACKAGE_VERSION = package_default.version;
@@ -22517,7 +22517,7 @@ async function writeExtensionBackup(claudeDesktopConfigPath, mcpServers) {
22517
22517
 
22518
22518
  // package.json
22519
22519
  var package_default = {
22520
- version: "1.9.2"};
22520
+ version: "1.9.4"};
22521
22521
 
22522
22522
  // src/package-meta.ts
22523
22523
  var PACKAGE_VERSION = package_default.version;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "multicorn-shield",
3
- "version": "1.9.2",
3
+ "version": "1.9.4",
4
4
  "description": "The control layer for AI agents: permissions, consent, spending limits, and audit logging.",
5
5
  "license": "MIT",
6
6
  "author": "Multicorn AI Pty Ltd",
@@ -164,7 +164,7 @@ function openBrowser(url) {
164
164
  function sleep(ms) {
165
165
  return new Promise((resolve) => setTimeout(resolve, ms));
166
166
  }
167
- async function pollApprovalStatus(config, approvalId) {
167
+ async function pollApprovalStatus(config, approvalId, consentLink) {
168
168
  let lastProgressWrite = Date.now();
169
169
  for (let i = 0; i < MAX_APPROVAL_POLLS; i++) {
170
170
  if (i > 0) {
@@ -172,9 +172,8 @@ async function pollApprovalStatus(config, approvalId) {
172
172
  }
173
173
  const now = Date.now();
174
174
  if (now - lastProgressWrite >= 3e4) {
175
- process.stderr.write(
176
- "[Shield] Waiting for approval... (open the consent screen in your browser)\n",
177
- );
175
+ process.stderr.write(`[Shield] Waiting for approval... ${consentLink}
176
+ `);
178
177
  lastProgressWrite = now;
179
178
  }
180
179
  let statusCode;
@@ -235,13 +234,16 @@ async function handlePendingWithConsentAndPoll(
235
234
  actionType,
236
235
  approvalsUrl,
237
236
  ) {
237
+ const consentLink = consentUrl(config.baseUrl, config.agentName, service, actionType);
238
+ process.stderr.write(`[Shield] Action requires approval. Open: ${consentLink}
239
+ `);
238
240
  if (hasConsentMarker(config.agentName)) {
239
241
  process.stderr.write(
240
242
  `[Shield] Waiting for approval (up to 5 min)...
241
243
  Approve in the Shield dashboard: ${approvalsUrl}
242
244
  `,
243
245
  );
244
- const approved2 = await pollApprovalStatus(config, approvalId);
246
+ const approved2 = await pollApprovalStatus(config, approvalId, consentLink);
245
247
  if (approved2) {
246
248
  process.exit(0);
247
249
  }
@@ -251,13 +253,12 @@ async function handlePendingWithConsentAndPoll(
251
253
  );
252
254
  process.exit(0);
253
255
  }
254
- const url = consentUrl(config.baseUrl, config.agentName, service, actionType);
255
256
  writeConsentMarker(config.agentName);
256
- openBrowser(url);
257
+ openBrowser(consentLink);
257
258
  process.stderr.write(
258
259
  "[Shield] Opening Shield consent screen... Waiting for approval (up to 5 min).\n",
259
260
  );
260
- const approved = await pollApprovalStatus(config, approvalId);
261
+ const approved = await pollApprovalStatus(config, approvalId, consentLink);
261
262
  if (approved) {
262
263
  process.exit(0);
263
264
  }