pennyrouter 0.3.14 → 0.3.15

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pennyrouter",
3
- "version": "0.3.14",
3
+ "version": "0.3.15",
4
4
  "description": "Install and manage PennyRouter local coding-agent integrations.",
5
5
  "homepage": "https://pennyrouter.com",
6
6
  "bugs": {
@@ -51,8 +51,8 @@ export const codexHarness = {
51
51
  openai_subscription_auth: subscriptionAuth,
52
52
  previous_model_provider: previousModelProvider,
53
53
  changes: {
54
- provider: PROVIDER_ID,
55
- auth: subscriptionAuth ? "openai-subscription+penny-header" : "penny-bearer",
54
+ provider: subscriptionAuth ? "openai" : PROVIDER_ID,
55
+ auth: subscriptionAuth ? "openai-subscription+penny-metadata" : "penny-bearer",
56
56
  },
57
57
  };
58
58
  },
@@ -95,9 +95,10 @@ export async function detectCodexChatgptAuth() {
95
95
  * must live here rather than at EOF. This also migrates the old undocumented root-key
96
96
  * install (openai_base_url/openai_api_key).
97
97
  *
98
- * In subscription mode Codex owns and refreshes the OpenAI bearer. The independent
99
- * x-pennyrouter-key header authenticates the account to our gateway. In Penny-only mode
100
- * the pr- key is the provider bearer, matching the isolated probe's existing behavior.
98
+ * In subscription mode keep Codex's reserved built-in OpenAI provider selected. That is what
99
+ * installs its native base instructions, additional_tools packet, and local `exec` dispatcher.
100
+ * Penny authentication travels in bounded Responses metadata, leaving Codex's OAuth bearer
101
+ * untouched. In Penny-only mode the custom provider contract remains unchanged.
101
102
  */
102
103
  export function renderCodexConfig(
103
104
  text,
@@ -112,11 +113,23 @@ export function renderCodexConfig(
112
113
  }
113
114
  const escapedKey = tomlString(apiKey);
114
115
  const escapedUrl = tomlString(`${String(gatewayBaseUrl).replace(/\/+$/, "")}/v1`);
116
+ if (subscriptionAuth) {
117
+ const block = [
118
+ MANAGED_START,
119
+ 'model_provider = "openai"',
120
+ `openai_base_url = ${escapedUrl}`,
121
+ `responses_api_metadata = { pennyrouter_key = ${escapedKey}, pennyrouter_harness = "codex", pennyrouter_surface = ${tomlString(surface)} }`,
122
+ MANAGED_END,
123
+ ].join("\n");
124
+ const lines = clean ? clean.split("\n") : [];
125
+ const firstTable = lines.findIndex((line) => /^\s*\[/.test(line));
126
+ const at = firstTable < 0 ? lines.length : firstTable;
127
+ lines.splice(at, 0, ...(at > 0 && lines[at - 1].trim() ? [""] : []), block,
128
+ ...(at < lines.length && lines[at + 1]?.trim() ? [""] : []));
129
+ return `${lines.join("\n").replace(/\n{3,}/g, "\n\n").trim()}\n`;
130
+ }
115
131
  const authLines = subscriptionAuth
116
- ? [
117
- "requires_openai_auth = true",
118
- `http_headers = { "x-pennyrouter-key" = ${escapedKey}, "x-pennyrouter-harness" = "codex", "x-pennyrouter-surface" = ${tomlString(surface)} }`,
119
- ]
132
+ ? []
120
133
  : [
121
134
  `experimental_bearer_token = ${escapedKey}`,
122
135
  `http_headers = { "x-pennyrouter-harness" = "codex", "x-pennyrouter-surface" = ${tomlString(surface)} }`,
@@ -25,13 +25,13 @@ function testSubscriptionProviderIsRootSafe() {
25
25
  subscriptionAuth: true,
26
26
  });
27
27
 
28
- assert.ok(next.indexOf('model_provider = "pennyrouter"') < next.indexOf("[features]"));
29
- assert.ok(next.includes('[model_providers.pennyrouter]'));
30
- assert.ok(next.includes('base_url = "http://localhost:8400/v1"'));
31
- assert.ok(next.includes("requires_openai_auth = true"));
32
- assert.ok(next.includes('"x-pennyrouter-key" = "pr-test-secret"'));
33
- assert.ok(next.includes('"x-pennyrouter-harness" = "codex"'));
34
- assert.ok(next.includes('"x-pennyrouter-surface" = "codex-terminal"'));
28
+ assert.ok(next.indexOf('model_provider = "openai"') < next.indexOf("[features]"));
29
+ assert.ok(!next.includes('[model_providers.pennyrouter]'));
30
+ assert.ok(next.includes('openai_base_url = "http://localhost:8400/v1"'));
31
+ assert.ok(next.includes('pennyrouter_key = "pr-test-secret"'));
32
+ assert.ok(next.includes('pennyrouter_harness = "codex"'));
33
+ assert.ok(next.includes('pennyrouter_surface = "codex-terminal"'));
34
+ assert.ok(!next.includes("requires_openai_auth"));
35
35
  assert.ok(!next.includes("experimental_bearer_token"));
36
36
  assert.equal((next.match(/# --- PennyRouter managed Codex provider ---/g) || []).length, 1);
37
37
  }
package/src/launch.js CHANGED
@@ -29,7 +29,8 @@ import { atomicWrite, homeDir, readJsonFile, readText } from "./state.js";
29
29
  const CONFIG_VERSION = 1;
30
30
  const PATH_START = "# >>> pennyrouter PATH >>>";
31
31
  const PATH_END = "# <<< pennyrouter PATH <<<";
32
- const CODEX_PROFILE_MARKER = "# PennyRouter per-command profile (managed; no secrets)";
32
+ const CODEX_PROFILE_MARKER = "# PennyRouter per-command profile (managed)";
33
+ const LEGACY_CODEX_PROFILE_MARKER = "# PennyRouter per-command profile (managed; no secrets)";
33
34
  const CLAUDE_COMMAND_MARKER = "<!-- PennyRouter managed Claude command -->";
34
35
  // A stable UUID keeps upgrades idempotent and avoids creating a new visible profile on
35
36
  // every launch. It is deliberately not the user's hand-authored profile id.
@@ -365,16 +366,24 @@ export async function installPennyRuntime({ updatePath = true } = {}) {
365
366
  return { supported: true, ...path, shim_path: pennyShimPath() };
366
367
  }
367
368
 
368
- export function renderCodexLaunchProfile({ gatewayBaseUrl, subscriptionAuth }) {
369
- const authLines = subscriptionAuth
370
- ? [
371
- "requires_openai_auth = true",
372
- 'env_http_headers = { "x-pennyrouter-key" = "PENNYROUTER_API_KEY", "x-pennyrouter-title-session" = "PENNYROUTER_TITLE_SESSION_ID", "x-pennyrouter-harness" = "codex", "x-pennyrouter-surface" = "codex-terminal" }',
373
- ]
374
- : [
375
- 'env_key = "PENNYROUTER_API_KEY"',
376
- 'http_headers = { "x-pennyrouter-title-session" = "PENNYROUTER_TITLE_SESSION_ID", "x-pennyrouter-harness" = "codex", "x-pennyrouter-surface" = "codex-terminal" }',
377
- ];
369
+ export function renderCodexLaunchProfile({ gatewayBaseUrl, subscriptionAuth, apiKey }) {
370
+ if (subscriptionAuth) {
371
+ if (!apiKey) throw new Error("PennyRouter credential is required for Codex subscription mode.");
372
+ return [
373
+ CODEX_PROFILE_MARKER,
374
+ 'model_provider = "openai"',
375
+ `openai_base_url = ${JSON.stringify(`${String(gatewayBaseUrl).replace(/\/+$/, "")}/v1`)}`,
376
+ `responses_api_metadata = { pennyrouter_key = ${JSON.stringify(apiKey)}, pennyrouter_harness = "codex", pennyrouter_surface = "codex-terminal" }`,
377
+ "",
378
+ "[tui]",
379
+ "terminal_title = []",
380
+ "",
381
+ ].join("\n");
382
+ }
383
+ const authLines = [
384
+ 'env_key = "PENNYROUTER_API_KEY"',
385
+ 'http_headers = { "x-pennyrouter-title-session" = "PENNYROUTER_TITLE_SESSION_ID", "x-pennyrouter-harness" = "codex", "x-pennyrouter-surface" = "codex-terminal" }',
386
+ ];
378
387
  return [
379
388
  CODEX_PROFILE_MARKER,
380
389
  'model_provider = "pennyrouter"',
@@ -615,12 +624,12 @@ export async function createCodexDesktopHome({ config, realHome }) {
615
624
  if (!config.api_key) throw new Error("No PennyRouter key found. Run `npx pennyrouter install` first.");
616
625
  if (!config.gateway_base_url) throw new Error("No PennyRouter gateway configured. Run `npx pennyrouter install` first.");
617
626
  const nativeConfig = await readText(join(realHome, "config.toml"), "");
618
- const desktopConfig = setCodexDesktopDefault(renderCodexConfig(removeCodexPennyProvider(nativeConfig), {
627
+ const desktopConfig = renderCodexConfig(removeCodexPennyProvider(nativeConfig), {
619
628
  apiKey: config.api_key,
620
629
  gatewayBaseUrl: config.gateway_base_url,
621
630
  subscriptionAuth: Boolean(config.harnesses?.codex?.subscription_auth),
622
631
  surface: "codex-desktop",
623
- }));
632
+ });
624
633
  await privateWrite(join(overlay, "config.toml"), desktopConfig);
625
634
  return overlay;
626
635
  } catch (error) {
@@ -629,15 +638,6 @@ export async function createCodexDesktopHome({ config, realHome }) {
629
638
  }
630
639
  }
631
640
 
632
- function setCodexDesktopDefault(text) {
633
- const lines = String(text).split("\n");
634
- const firstTable = lines.findIndex((line) => /^\s*\[/.test(line));
635
- const rootEnd = firstTable < 0 ? lines.length : firstTable;
636
- const root = lines.slice(0, rootEnd).filter((line) => !/^\s*model_provider\s*=/.test(line));
637
- root.unshift('model_provider = "pennyrouter"');
638
- return [...root, ...lines.slice(rootEnd)].join("\n");
639
- }
640
-
641
641
  function removeCodexPennyProvider(text) {
642
642
  const lines = String(text).split("\n");
643
643
  const start = lines.findIndex((line) => /^\s*\[model_providers\.pennyrouter\]\s*$/.test(line));
@@ -773,15 +773,19 @@ async function writeClaudeUserSkill() {
773
773
  }
774
774
  }
775
775
 
776
- async function writeCodexLaunchProfile({ gatewayBaseUrl, subscriptionAuth, profile }) {
776
+ async function writeCodexLaunchProfile({ gatewayBaseUrl, subscriptionAuth, apiKey, profile }) {
777
777
  const path = codexLaunchProfilePath(profile);
778
778
  const existing = await readText(path, "");
779
- if (existing.trim() && !existing.startsWith(CODEX_PROFILE_MARKER)) {
779
+ if (existing.trim() && !isManagedCodexLaunchProfile(existing)) {
780
780
  throw new Error(
781
781
  `${path} already exists and is not managed by PennyRouter. Rename it before installing.`,
782
782
  );
783
783
  }
784
- await privateWrite(path, renderCodexLaunchProfile({ gatewayBaseUrl, subscriptionAuth }));
784
+ await privateWrite(path, renderCodexLaunchProfile({ gatewayBaseUrl, subscriptionAuth, apiKey }));
785
+ }
786
+
787
+ function isManagedCodexLaunchProfile(text) {
788
+ return text.startsWith(CODEX_PROFILE_MARKER) || text.startsWith(LEGACY_CODEX_PROFILE_MARKER);
785
789
  }
786
790
 
787
791
  export async function configurePennyHarness(harness, {
@@ -813,6 +817,7 @@ export async function configurePennyHarness(harness, {
813
817
  await writeCodexLaunchProfile({
814
818
  gatewayBaseUrl,
815
819
  subscriptionAuth,
820
+ apiKey,
816
821
  profile: config.active_profile,
817
822
  });
818
823
  config.harnesses.codex = {
@@ -859,7 +864,7 @@ export async function removePennyHarness(harness) {
859
864
  for (const name of listPennyProfiles(config)) {
860
865
  const path = codexLaunchProfilePath(name);
861
866
  const existing = await readText(path, "");
862
- if (!existing.startsWith(CODEX_PROFILE_MARKER)) continue;
867
+ if (!isManagedCodexLaunchProfile(existing)) continue;
863
868
  await unlink(path).catch((error) => {
864
869
  if (error.code !== "ENOENT") throw error;
865
870
  });
@@ -183,19 +183,19 @@ async function main() {
183
183
  realHome: path.join(home, ".codex"),
184
184
  });
185
185
  const desktopConfig = fs.readFileSync(path.join(desktopHome, "config.toml"), "utf8");
186
- assert.match(desktopConfig, /^model_provider = "pennyrouter"/);
187
- assert.match(desktopConfig, /\[model_providers\.pennyrouter\]/);
188
- assert.match(desktopConfig, /"x-pennyrouter-key" = "pr-super-secret"/);
189
- assert.match(desktopConfig, /"x-pennyrouter-surface" = "codex-desktop"/);
186
+ assert.match(desktopConfig, /^model_provider = "openai"/m);
187
+ assert.doesNotMatch(desktopConfig, /\[model_providers\.pennyrouter\]/);
188
+ assert.match(desktopConfig, /pennyrouter_key = "pr-super-secret"/);
189
+ assert.match(desktopConfig, /pennyrouter_surface = "codex-desktop"/);
190
190
  assert.equal(fs.readFileSync(path.join(home, ".codex", "config.toml"), "utf8"), nativeCodex);
191
191
  fs.rmSync(desktopHome, { recursive: true, force: true });
192
192
  const codexProfile = fs.readFileSync(codexLaunchProfilePath(), "utf8");
193
- assert.ok(codexProfile.includes('"x-pennyrouter-key" = "PENNYROUTER_API_KEY"'));
193
+ assert.match(codexProfile, /^# PennyRouter per-command profile \(managed\)\nmodel_provider = "openai"/);
194
+ assert.match(codexProfile, /openai_base_url = "https:\/\/gateway\.example\/v1"/);
195
+ assert.match(codexProfile, /pennyrouter_key = "pr-super-secret"/);
194
196
  assert.ok(codexProfile.includes("[tui]\nterminal_title = []"));
195
- assert.ok(codexProfile.includes('"x-pennyrouter-title-session" = "PENNYROUTER_TITLE_SESSION_ID"'));
196
- assert.ok(codexProfile.includes('"x-pennyrouter-harness" = "codex"'));
197
- assert.ok(codexProfile.includes('"x-pennyrouter-surface" = "codex-terminal"'));
198
- assert.ok(!codexProfile.includes("pr-super-secret"));
197
+ assert.ok(codexProfile.includes('pennyrouter_harness = "codex"'));
198
+ assert.ok(codexProfile.includes('pennyrouter_surface = "codex-terminal"'));
199
199
  assert.ok(!rc.includes("pr-super-secret"));
200
200
 
201
201
  const claude = buildPennyInvocation("claude", ["--resume", "abc"], config);