pennyrouter 0.3.5 → 0.3.6

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.5",
3
+ "version": "0.3.6",
4
4
  "description": "Install and manage PennyRouter local coding-agent integrations.",
5
5
  "homepage": "https://pennyrouter.com",
6
6
  "bugs": {
@@ -37,6 +37,7 @@ export const codexHarness = {
37
37
  apiKey,
38
38
  gatewayBaseUrl,
39
39
  subscriptionAuth,
40
+ surface: "codex-terminal",
40
41
  });
41
42
 
42
43
  await atomicWrite(CONFIG_PATH, next);
@@ -98,7 +99,10 @@ export async function detectCodexChatgptAuth() {
98
99
  * x-pennyrouter-key header authenticates the account to our gateway. In Penny-only mode
99
100
  * the pr- key is the provider bearer, matching the isolated probe's existing behavior.
100
101
  */
101
- export function renderCodexConfig(text, { apiKey, gatewayBaseUrl, subscriptionAuth }) {
102
+ export function renderCodexConfig(
103
+ text,
104
+ { apiKey, gatewayBaseUrl, subscriptionAuth, surface = "codex-terminal" },
105
+ ) {
102
106
  const clean = removeManagedCodexConfig(text, { removeRootProvider: true }).trimEnd();
103
107
  if (new RegExp(`^\\s*\\[model_providers\\.${escapeRegex(PROVIDER_ID)}\\]\\s*$`, "m").test(clean)) {
104
108
  throw new Error(
@@ -111,9 +115,12 @@ export function renderCodexConfig(text, { apiKey, gatewayBaseUrl, subscriptionAu
111
115
  const authLines = subscriptionAuth
112
116
  ? [
113
117
  "requires_openai_auth = true",
114
- `http_headers = { "x-pennyrouter-key" = ${escapedKey} }`,
118
+ `http_headers = { "x-pennyrouter-key" = ${escapedKey}, "x-pennyrouter-harness" = "codex", "x-pennyrouter-surface" = ${tomlString(surface)} }`,
115
119
  ]
116
- : [`experimental_bearer_token = ${escapedKey}`];
120
+ : [
121
+ `experimental_bearer_token = ${escapedKey}`,
122
+ `http_headers = { "x-pennyrouter-harness" = "codex", "x-pennyrouter-surface" = ${tomlString(surface)} }`,
123
+ ];
117
124
  const block = [
118
125
  MANAGED_START,
119
126
  `model_provider = "${PROVIDER_ID}"`,
@@ -29,7 +29,9 @@ function testSubscriptionProviderIsRootSafe() {
29
29
  assert.ok(next.includes('[model_providers.pennyrouter]'));
30
30
  assert.ok(next.includes('base_url = "http://localhost:8400/v1"'));
31
31
  assert.ok(next.includes("requires_openai_auth = true"));
32
- assert.ok(next.includes('http_headers = { "x-pennyrouter-key" = "pr-test-secret" }'));
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"'));
33
35
  assert.ok(!next.includes("experimental_bearer_token"));
34
36
  assert.equal((next.match(/# --- PennyRouter managed Codex provider ---/g) || []).length, 1);
35
37
  }
@@ -51,6 +53,7 @@ function testLegacyMigrationAndIdempotence() {
51
53
  assert.ok(!once.includes("openai_base_url"));
52
54
  assert.ok(!once.includes("openai_api_key"));
53
55
  assert.ok(once.includes('experimental_bearer_token = "pr-new"'));
56
+ assert.ok(once.includes('"x-pennyrouter-surface" = "codex-terminal"'));
54
57
  assert.ok(!once.includes("requires_openai_auth"));
55
58
  }
56
59
 
package/src/launch.js CHANGED
@@ -295,11 +295,11 @@ export function renderCodexLaunchProfile({ gatewayBaseUrl, subscriptionAuth }) {
295
295
  const authLines = subscriptionAuth
296
296
  ? [
297
297
  "requires_openai_auth = true",
298
- 'env_http_headers = { "x-pennyrouter-key" = "PENNYROUTER_API_KEY", "x-pennyrouter-title-session" = "PENNYROUTER_TITLE_SESSION_ID", "x-pennyrouter-harness" = "codex" }',
298
+ '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" }',
299
299
  ]
300
300
  : [
301
301
  'env_key = "PENNYROUTER_API_KEY"',
302
- 'http_headers = { "x-pennyrouter-title-session" = "PENNYROUTER_TITLE_SESSION_ID", "x-pennyrouter-harness" = "codex" }',
302
+ 'http_headers = { "x-pennyrouter-title-session" = "PENNYROUTER_TITLE_SESSION_ID", "x-pennyrouter-harness" = "codex", "x-pennyrouter-surface" = "codex-terminal" }',
303
303
  ];
304
304
  return [
305
305
  CODEX_PROFILE_MARKER,
@@ -545,6 +545,7 @@ export async function createCodexDesktopHome({ config, realHome }) {
545
545
  apiKey: config.api_key,
546
546
  gatewayBaseUrl: config.gateway_base_url,
547
547
  subscriptionAuth: Boolean(config.harnesses?.codex?.subscription_auth),
548
+ surface: "codex-desktop",
548
549
  }));
549
550
  await privateWrite(join(overlay, "config.toml"), desktopConfig);
550
551
  return overlay;
@@ -971,7 +972,9 @@ export async function runPenny(argv = process.argv.slice(2)) {
971
972
  stdio: "inherit",
972
973
  shell: process.platform === "win32",
973
974
  });
974
- const stopTerminalTitle = ["claude", "codex", "opencode"].includes(harness)
975
+ // Claude Code owns its own terminal title updates while a turn is running. Do not
976
+ // compete with it from the parent process; Codex and OpenCode remain Penny-owned.
977
+ const stopTerminalTitle = ["codex", "opencode"].includes(harness)
975
978
  ? startPennyTerminalTitle({
976
979
  gatewayBaseUrl: config.gateway_base_url,
977
980
  apiKey: config.api_key,
@@ -1019,7 +1022,10 @@ async function runCodexDesktop(config, { logs = false, mode = "gateway" } = {})
1019
1022
  }
1020
1023
  if (mode === "native") {
1021
1024
  const child = spawn(codexDesktopExecutable(), [], {
1022
- cwd: process.cwd(),
1025
+ // Desktop launches must not inherit a project directory. When PennyRouter itself is
1026
+ // started from ~/Documents, that makes macOS attribute Codex's access to Documents to
1027
+ // PennyRouter and trigger a protected-folder prompt.
1028
+ cwd: homeDir(),
1023
1029
  env: process.env,
1024
1030
  stdio: logs ? "inherit" : "ignore",
1025
1031
  detached: !logs,
@@ -1042,7 +1048,8 @@ async function runCodexDesktop(config, { logs = false, mode = "gateway" } = {})
1042
1048
  "__codex-desktop-supervisor",
1043
1049
  overlay,
1044
1050
  ], {
1045
- cwd: process.cwd(),
1051
+ // Keep the detached supervisor out of the caller's protected project directory too.
1052
+ cwd: homeDir(),
1046
1053
  env: process.env,
1047
1054
  stdio: "ignore",
1048
1055
  detached: true,
@@ -1057,7 +1064,7 @@ async function runCodexDesktop(config, { logs = false, mode = "gateway" } = {})
1057
1064
 
1058
1065
  export async function runCodexDesktopSupervisor(overlay) {
1059
1066
  const child = spawn(codexDesktopExecutable(), [], {
1060
- cwd: process.cwd(),
1067
+ cwd: homeDir(),
1061
1068
  env: { ...process.env, CODEX_HOME: overlay },
1062
1069
  stdio: "inherit",
1063
1070
  shell: process.platform === "win32",
@@ -179,7 +179,8 @@ async function main() {
179
179
  const desktopConfig = fs.readFileSync(path.join(desktopHome, "config.toml"), "utf8");
180
180
  assert.match(desktopConfig, /^model_provider = "pennyrouter"/);
181
181
  assert.match(desktopConfig, /\[model_providers\.pennyrouter\]/);
182
- assert.match(desktopConfig, /http_headers = \{ "x-pennyrouter-key" = "pr-super-secret" \}/);
182
+ assert.match(desktopConfig, /"x-pennyrouter-key" = "pr-super-secret"/);
183
+ assert.match(desktopConfig, /"x-pennyrouter-surface" = "codex-desktop"/);
183
184
  assert.equal(fs.readFileSync(path.join(home, ".codex", "config.toml"), "utf8"), nativeCodex);
184
185
  fs.rmSync(desktopHome, { recursive: true, force: true });
185
186
  const codexProfile = fs.readFileSync(codexLaunchProfilePath(), "utf8");
@@ -187,6 +188,7 @@ async function main() {
187
188
  assert.ok(codexProfile.includes("[tui]\nterminal_title = []"));
188
189
  assert.ok(codexProfile.includes('"x-pennyrouter-title-session" = "PENNYROUTER_TITLE_SESSION_ID"'));
189
190
  assert.ok(codexProfile.includes('"x-pennyrouter-harness" = "codex"'));
191
+ assert.ok(codexProfile.includes('"x-pennyrouter-surface" = "codex-terminal"'));
190
192
  assert.ok(!codexProfile.includes("pr-super-secret"));
191
193
  assert.ok(!rc.includes("pr-super-secret"));
192
194