Skip to content

Commit eca4e43

Browse files
authored
feat: add Codex beta provider with API-equivalent cost (#93)
Add privacy-safe local Codex usage indexing, exact request token attribution, API-equivalent cost trends, weekly allowance estimates, a shared responsive dashboard, and related Claude profile and transcript-deduplication fixes.\n\nValidated by the complete Node, UI, and release-package gates.\n\nFixes #89\n\nCo-authored-by: OpenAI Codex <215057067+openai-codex[bot]@users.noreply.github.com>\nCo-authored-by: Claude <noreply@anthropic.com>
1 parent d3b4b71 commit eca4e43

143 files changed

Lines changed: 43026 additions & 1316 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import * as assert from 'node:assert/strict';
2+
import { spawn } from 'node:child_process';
3+
import { get } from 'node:http';
4+
import { test } from 'node:test';
5+
import { fileURLToPath } from 'node:url';
6+
7+
const repositoryRoot = fileURLToPath(new URL('../../', import.meta.url));
8+
const serverPath = fileURLToPath(
9+
new URL('../../tests/ui/support/server.mjs', import.meta.url),
10+
);
11+
const uiTestPort = Number(process.env.CCU_UI_TEST_PORT ?? 4173);
12+
13+
function request(path) {
14+
return new Promise((resolve, reject) => {
15+
const call = get({ hostname: '127.0.0.1', port: uiTestPort, path }, (response) => {
16+
response.setEncoding('utf8');
17+
let body = '';
18+
response.on('data', (chunk) => { body += chunk; });
19+
response.on('end', () => resolve({ status: response.statusCode, body }));
20+
});
21+
call.on('error', reject);
22+
});
23+
}
24+
25+
async function waitForServer(child, stderr) {
26+
for (let attempt = 0; attempt < 50; attempt += 1) {
27+
if (child.exitCode !== null) {
28+
throw new Error(`UI server exited before health check: ${stderr()}`);
29+
}
30+
try {
31+
const response = await request('/health');
32+
if (response.status === 200) {
33+
await new Promise((resolve) => setTimeout(resolve, 50));
34+
if (child.exitCode !== null) {
35+
throw new Error(`UI server exited after health check: ${stderr()}`);
36+
}
37+
return;
38+
}
39+
} catch (error) {
40+
if (child.exitCode !== null) throw error;
41+
// Server startup is asynchronous; retry the real health endpoint.
42+
}
43+
await new Promise((resolve) => setTimeout(resolve, 50));
44+
}
45+
throw new Error(`UI server health check timed out: ${stderr()}`);
46+
}
47+
48+
test('UI harness returns a generic 500 body without stack or local path disclosure', async (t) => {
49+
let stderr = '';
50+
const child = spawn(process.execPath, [serverPath], {
51+
cwd: repositoryRoot,
52+
stdio: ['ignore', 'ignore', 'pipe'],
53+
});
54+
child.stderr.setEncoding('utf8');
55+
child.stderr.on('data', (chunk) => { stderr += chunk; });
56+
t.after(() => {
57+
if (child.exitCode === null) child.kill('SIGTERM');
58+
});
59+
60+
await waitForServer(child, () => stderr);
61+
await t.test('dark rendering exposes the VS Code foreground token', async () => {
62+
const response = await request('/?theme=dark');
63+
assert.equal(response.status, 200);
64+
assert.match(response.body, /<body class="vscode-dark\b/);
65+
assert.match(response.body, /--vscode-foreground:#CCCCCC/);
66+
});
67+
await t.test('light rendering exposes the VS Code foreground token', async () => {
68+
const response = await request('/?theme=light');
69+
assert.equal(response.status, 200);
70+
assert.match(response.body, /<body class="vscode-light\b/);
71+
assert.match(response.body, /--vscode-foreground:#616161/);
72+
});
73+
const response = await request('//[');
74+
assert.equal(response.status, 500);
75+
assert.equal(response.body, 'Internal Server Error');
76+
assert.doesNotMatch(response.body, /\/private\//);
77+
assert.doesNotMatch(response.body, /\bat .*server\.mjs/);
78+
});

‎.github/scripts/verify-vsix.mjs‎

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { createHash } from 'node:crypto';
2+
import { execFileSync } from 'node:child_process';
3+
import { readFileSync } from 'node:fs';
4+
import { resolve } from 'node:path';
5+
import { fileURLToPath } from 'node:url';
6+
7+
import {
8+
assertSafeVsixEntries,
9+
assertCodexBundleMarkers,
10+
} from './vsixPolicy.mjs';
11+
12+
const MAX_UNZIP_OUTPUT = 16 * 1024 * 1024;
13+
14+
function parseRuntimeMain(main, label) {
15+
if (typeof main !== 'string' || !main.startsWith('./out/') || !main.endsWith('.js') ||
16+
main.includes('\\') || main.includes('\0')) {
17+
throw new Error(`invalid ${label} main: ${String(main)}`);
18+
}
19+
const relativeMain = main.slice(2);
20+
if (relativeMain.split('/').some((segment) => segment === '' || segment === '.' || segment === '..')) {
21+
throw new Error(`invalid ${label} main: ${main}`);
22+
}
23+
return relativeMain;
24+
}
25+
26+
export function assertPackagedManifest(manifest, expectedVersion, expectedMain, entries) {
27+
if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) {
28+
throw new Error('invalid packaged manifest');
29+
}
30+
if (manifest.version !== expectedVersion) {
31+
throw new Error(
32+
`VSIX version mismatch: expected ${expectedVersion}, got ${String(manifest.version)}`,
33+
);
34+
}
35+
36+
parseRuntimeMain(expectedMain, 'source');
37+
const relativeMain = parseRuntimeMain(manifest.main, 'VSIX');
38+
if (manifest.main !== expectedMain) {
39+
throw new Error(
40+
`VSIX main mismatch: expected ${expectedMain}, got ${manifest.main}`,
41+
);
42+
}
43+
44+
const mainEntry = `extension/${relativeMain}`;
45+
if (!entries.includes(mainEntry)) {
46+
throw new Error(`missing VSIX main entry: ${mainEntry}`);
47+
}
48+
return mainEntry;
49+
}
50+
51+
function unzipText(vsixPath, entry) {
52+
return execFileSync('unzip', ['-p', vsixPath, entry], {
53+
encoding: 'utf8',
54+
maxBuffer: MAX_UNZIP_OUTPUT,
55+
});
56+
}
57+
58+
export function verifyVsix(vsixPath, expectedVersion, expectedMain) {
59+
const entries = execFileSync('unzip', ['-Z1', vsixPath], {
60+
encoding: 'utf8',
61+
maxBuffer: MAX_UNZIP_OUTPUT,
62+
}).split(/\r?\n/).filter((entry) => entry.length > 0);
63+
64+
assertSafeVsixEntries(entries);
65+
const packagedManifest = JSON.parse(unzipText(vsixPath, 'extension/package.json'));
66+
const main = assertPackagedManifest(packagedManifest, expectedVersion, expectedMain, entries);
67+
const javascriptEntries = entries.filter((entry) => /^extension\/out\/.*\.js$/.test(entry));
68+
const bundle = javascriptEntries.map((entry) => unzipText(vsixPath, entry)).join('\n');
69+
assertCodexBundleMarkers(bundle);
70+
71+
const bytes = readFileSync(vsixPath);
72+
return {
73+
path: vsixPath,
74+
version: packagedManifest.version,
75+
main,
76+
files: entries.length,
77+
bytes: bytes.length,
78+
sha256: createHash('sha256').update(bytes).digest('hex'),
79+
};
80+
}
81+
82+
function main() {
83+
const [, , requestedPath, requestedVersion] = process.argv;
84+
if (!requestedPath) {
85+
throw new Error('usage: node .github/scripts/verify-vsix.mjs <file.vsix> [expected-version]');
86+
}
87+
88+
const vsixPath = resolve(requestedPath);
89+
const sourceManifest = JSON.parse(readFileSync(resolve('package.json'), 'utf8'));
90+
const expectedVersion = requestedVersion ?? sourceManifest.version;
91+
process.stdout.write(`${JSON.stringify(
92+
verifyVsix(vsixPath, expectedVersion, sourceManifest.main),
93+
null,
94+
2,
95+
)}\n`);
96+
}
97+
98+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
99+
main();
100+
}

‎.github/scripts/vsixPolicy.mjs‎

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
const ROOT_METADATA_ENTRIES = new Set([
2+
'[Content_Types].xml',
3+
'extension.vsixmanifest',
4+
]);
5+
6+
const REQUIRED_ENTRIES = [
7+
...ROOT_METADATA_ENTRIES,
8+
'extension/package.json',
9+
'extension/out/extension.js',
10+
'extension/out/webview.js',
11+
'extension/out/codexView.js',
12+
];
13+
14+
const FORBIDDEN_ENTRY_PATTERNS = [
15+
/^extension\/(?:src|tests|docs|\.github|\.codex|\.claude|\.agents|\.superpowers)(?:\/|$)/i,
16+
/^extension\/out\/test(?:\/|$)/i,
17+
/^extension\/AGENTS(?:\.zh-CN)?\.md$/i,
18+
/^extension\/node_modules(?:\/|$)/i,
19+
/^extension\/issue87-.*\.cjs$/i,
20+
/^extension\/out\/claudeIncrementalPrototype\.js$/i,
21+
/(?:^|\/)\.env(?:\.|$)/i,
22+
/\.(?:pem|key|p12|pfx)$/i,
23+
];
24+
25+
const REQUIRED_BUNDLE_MARKERS = [
26+
{ label: "provider === 'codex'", present: (bundle) => bundle.includes("provider === 'codex'") },
27+
{ label: 'renderTodayData(provider)', present: (bundle) => bundle.includes('renderTodayData(provider)') },
28+
{ label: 'renderSessionData(provider)', present: (bundle) => bundle.includes('renderSessionData(provider)') },
29+
{ label: 'renderSettingsPanel(provider)', present: (bundle) => bundle.includes('renderSettingsPanel(provider)') },
30+
{ label: 'data-provider-target', present: (bundle) => bundle.includes('data-provider-target') },
31+
];
32+
33+
const RETIRED_BUNDLE_MARKERS = [
34+
'ccu.codex.ui.v1',
35+
'data-codex-tab-button="recent"',
36+
'data-codex-tab-button="7d"',
37+
'data-codex-tab-button="30d"',
38+
'data-codex-tab-button="all"',
39+
'data-codex-tab-button="threads"',
40+
'data-codex-tab-button="projects"',
41+
'data-codex-tab-button="behavior"',
42+
'data-codex-tab-button="settings"',
43+
'data-codex-page="overview"',
44+
'data-codex-page="explore"',
45+
'data-codex-page="recommendations"',
46+
'data-codex-action',
47+
'hostState.codexUi = nextState',
48+
];
49+
50+
function assertCanonicalEntry(entry) {
51+
if (typeof entry !== 'string' || entry.length === 0 || entry.includes('\0') ||
52+
entry.includes('\\') || entry.startsWith('/') || /^[A-Za-z]:/.test(entry) ||
53+
entry.endsWith('/')) {
54+
throw new Error(`unsafe VSIX entry: ${String(entry)}`);
55+
}
56+
if (entry.split('/').some((segment) => segment === '' || segment === '.' || segment === '..')) {
57+
throw new Error(`unsafe VSIX entry: ${entry}`);
58+
}
59+
if (!ROOT_METADATA_ENTRIES.has(entry) && !entry.startsWith('extension/')) {
60+
throw new Error(`unsafe VSIX entry: ${entry}`);
61+
}
62+
}
63+
64+
export function assertSafeVsixEntries(entries) {
65+
if (!Array.isArray(entries)) {
66+
throw new Error('VSIX entries must be an array');
67+
}
68+
69+
const seen = new Set();
70+
for (const entry of entries) {
71+
assertCanonicalEntry(entry);
72+
const collisionKey = entry.toLowerCase();
73+
if (seen.has(collisionKey)) {
74+
throw new Error(`duplicate VSIX entry: ${entry}`);
75+
}
76+
seen.add(collisionKey);
77+
if (FORBIDDEN_ENTRY_PATTERNS.some((pattern) => pattern.test(entry))) {
78+
throw new Error(`forbidden VSIX entry: ${entry}`);
79+
}
80+
}
81+
82+
for (const required of REQUIRED_ENTRIES) {
83+
if (!seen.has(required.toLowerCase())) {
84+
throw new Error(`missing required VSIX entry: ${required}`);
85+
}
86+
}
87+
}
88+
89+
export function assertCodexBundleMarkers(bundle) {
90+
if (typeof bundle !== 'string') {
91+
throw new Error('bundle must be text');
92+
}
93+
for (const marker of REQUIRED_BUNDLE_MARKERS) {
94+
if (!marker.present(bundle)) {
95+
throw new Error(`missing bundle marker: ${marker.label}`);
96+
}
97+
}
98+
for (const marker of RETIRED_BUNDLE_MARKERS) {
99+
if (bundle.includes(marker)) {
100+
throw new Error(`retired bundle marker: ${marker}`);
101+
}
102+
}
103+
}

0 commit comments

Comments
 (0)