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 +1 -1
- package/src/harnesses/codex.js +22 -9
- package/src/harnesses/codex.test.js +7 -7
- package/src/launch.js +31 -26
- package/src/launch.test.js +9 -9
package/package.json
CHANGED
package/src/harnesses/codex.js
CHANGED
|
@@ -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-
|
|
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
|
|
99
|
-
*
|
|
100
|
-
*
|
|
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 = "
|
|
29
|
-
assert.ok(next.includes('[model_providers.pennyrouter]'));
|
|
30
|
-
assert.ok(next.includes('
|
|
31
|
-
assert.ok(next.includes(
|
|
32
|
-
assert.ok(next.includes('
|
|
33
|
-
assert.ok(next.includes('
|
|
34
|
-
assert.ok(next.includes(
|
|
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
|
|
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
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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 =
|
|
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
|
|
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
|
|
867
|
+
if (!isManagedCodexLaunchProfile(existing)) continue;
|
|
863
868
|
await unlink(path).catch((error) => {
|
|
864
869
|
if (error.code !== "ENOENT") throw error;
|
|
865
870
|
});
|
package/src/launch.test.js
CHANGED
|
@@ -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 = "
|
|
187
|
-
assert.
|
|
188
|
-
assert.match(desktopConfig, /
|
|
189
|
-
assert.match(desktopConfig, /
|
|
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.
|
|
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('
|
|
196
|
-
assert.ok(codexProfile.includes('
|
|
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);
|