Restore desktop usage and subscription discovery across harnesses (#5844)

* fix(app): hide the sidebar Usage item while it has no summary

Without a summary window with data, the item fell back to a plain "Usage"
row that duplicated the footer's Usage icon. It now renders nothing, and the
footer rows leave it out so an otherwise empty rows block and its separator
disappear with it.

* fix(desktop): restore built-in usage in packaged apps

The external plugin compiler cannot traverse app.asar, and dependency packaging drops declarations needed by import validation. Ship complete built-in resources beside the archive and verify registration in the packaged desktop smoke check.

* fix(usage): restore login discovery across harnesses

Discover subscription accounts independently of agent sessions and retain each account’s logins in preference order so an unusable credential can fall back to another login for the same account.
This commit is contained in:
Mohamed Boudra
2026-10-01 14:51:11 +01:00
committed by GitHub
parent e10f6d2e2c
commit 70df09daeb
33 changed files with 1580 additions and 223 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
import type { PluginServerContext } from "@getpaseo/plugin/server";
import { inputSchema } from "./shared/input.js";
import { fetchUsage, identify } from "./server/usage.js";
import { discover, fetchUsage, identify } from "./server/usage.js";
export default function contribute(server: PluginServerContext) {
server.registerUsageSource({
@@ -8,7 +8,7 @@ export default function contribute(server: PluginServerContext) {
label: "Codex",
icon: "icon.svg",
input: inputSchema,
discover: async () => [{}],
discover: () => discover(),
identify,
fetch: fetchUsage,
});
@@ -0,0 +1,245 @@
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, expect, test } from "vitest";
import { discover, fetchUsage, identify } from "./usage.js";
let home: string;
beforeEach(async () => {
home = await mkdtemp(join(tmpdir(), "usage-stores-"));
});
afterEach(async () => {
await rm(home, { recursive: true, force: true });
});
function lookup(env: NodeJS.ProcessEnv = {}) {
return { home, env, platform: "linux" as const, now: () => 1000 };
}
async function json(path: string, value: unknown) {
const { dirname } = await import("node:path");
await mkdir(dirname(path), { recursive: true });
await writeFile(path, JSON.stringify(value));
}
async function account(input: Parameters<typeof fetchUsage>[0], env: NodeJS.ProcessEnv = {}) {
return identify(input, lookup(env));
}
async function database(path: string) {
await mkdir((await import("node:path")).dirname(path), { recursive: true });
const { DatabaseSync } = await import("node:sqlite");
const db = new DatabaseSync(path);
db.exec(
"CREATE TABLE auth_credentials (id INTEGER PRIMARY KEY, provider TEXT, credential_type TEXT, data TEXT, disabled_cause TEXT)",
);
const insert = db.prepare("INSERT INTO auth_credentials VALUES (?, ?, ?, ?, ?)");
insert.run(
1,
"openai-codex",
"oauth",
JSON.stringify({ access: "fixture-omp", accountId: "omp-account", expires: 2000 }),
null,
);
insert.run(
2,
"openai-codex",
"oauth",
JSON.stringify({ access: "fixture-expired", expires: 500 }),
null,
);
insert.run(
3,
"openai-codex",
"oauth",
JSON.stringify({ access: "fixture-disabled", expires: 2000 }),
"revoked",
);
insert.run(
4,
"openai-codex",
"api_key",
JSON.stringify({ access: "fixture-key", expires: 2000 }),
null,
);
insert.run(5, "openai-codex", "oauth", "invalid", null);
db.close();
}
test("Pi uses its own OAuth identity and re-reads rotated tokens without writing", async () => {
const path = join(home, ".pi", "agent", "auth.json");
await json(path, {
"openai-codex": { type: "oauth", access: "fixture-first", accountId: "pi-account" },
});
await json(join(home, ".claude.json"), {
oauthAccount: { accountUuid: "other", organizationUuid: "other-org" },
});
const inputs = await discover(lookup());
expect(inputs).toHaveLength(1);
expect(await account(inputs[0]!)).toEqual({ key: "pi-account" });
await json(path, {
"openai-codex": { type: "oauth", access: "fixture-rotated", accountId: "pi-account" },
});
const before = await readFile(path, "utf8");
expect(
(
await fetchUsage(
inputs[0]!,
async (_url, init) => {
expect(new Headers(init?.headers).get("Authorization")).toBe("Bearer fixture-rotated");
return new Response("{}", { status: 401 });
},
lookup(),
)
).status,
).toBe("unavailable");
expect(await readFile(path, "utf8")).toBe(before);
});
test("Pi honors directory override and ignores API keys and malformed files", async () => {
const directory = join(home, "custom-pi");
const env = { PI_CODING_AGENT_DIR: directory };
const path = join(directory, "auth.json");
await json(path, {
"openai-codex": { type: "oauth", access: "fixture-pi", accountId: "pi-account" },
});
expect(await discover(lookup(env))).toEqual([{ route: { store: "pi", path } }]);
await json(path, { "openai-codex": { type: "api_key", key: "fixture-key" } });
expect(await discover(lookup(env))).toEqual([]);
await writeFile(path, "broken");
expect(await discover(lookup(env))).toEqual([]);
});
test("expired Pi tokens are unavailable without calling usage or refreshing", async () => {
await json(join(home, ".pi", "agent", "auth.json"), {
"openai-codex": {
type: "oauth",
access: "fixture-expired",
accountId: "pi-account",
expires: 500,
},
});
const [input] = await discover(lookup());
expect(
(
await fetchUsage(
input!,
async () => {
throw new Error("must not fetch expired token");
},
lookup(),
)
).status,
).toBe("unavailable");
});
for (const location of ["default", "profile", "xdg", "override", "config"]) {
test(`OMP discovers enabled live OAuth rows in ${location} store read-only`, async () => {
let path = join(home, ".omp", "agent", "agent.db");
let env: NodeJS.ProcessEnv = { OMP_AUTH_BROKER_URL: "https://broker.test" };
if (location === "profile") {
env.OMP_PROFILE = "work";
path = join(home, ".omp", "profiles", "work", "agent", "agent.db");
}
if (location === "xdg") {
env.XDG_DATA_HOME = join(home, "data");
env.PI_PROFILE = "work";
path = join(home, "data", "omp", "profiles", "work", "agent.db");
}
if (location === "override") {
env.PI_CODING_AGENT_DIR = join(home, "custom");
path = join(home, "custom", "agent.db");
}
if (location === "config") {
env.PI_CONFIG_DIR = "custom-config";
path = join(home, "custom-config", "agent", "agent.db");
}
await database(path);
const before = await readFile(path);
const inputs = await discover(lookup(env));
expect(inputs).toEqual([{ route: { store: "omp", path, credentialId: 1 } }]);
expect(await account(inputs[0]!, env)).toEqual({ key: "omp-account" });
expect(
(
await fetchUsage(
inputs[0]!,
async (_url, init) => {
expect(new Headers(init?.headers).get("Authorization")).toBe("Bearer fixture-omp");
return new Response("{}", { status: 401 });
},
lookup(env),
)
).status,
).toBe("unavailable");
expect(await readFile(path)).toEqual(before);
});
}
test("missing SQLite skips only OMP", async () => {
await database(join(home, ".omp", "agent", "agent.db"));
await json(join(home, ".pi", "agent", "auth.json"), {
"openai-codex": { type: "oauth", access: "fixture-pi" },
});
const inputs = await discover({ ...lookup(), sqlite: () => undefined });
expect(inputs).toEqual([
{ route: { store: "pi", path: join(home, ".pi", "agent", "auth.json") } },
]);
});
test("Codex and OpenCode discover accounts in preference order with XDG and CODEX_HOME", async () => {
const codex = join(home, "codex");
const data = join(home, "data");
const env = { CODEX_HOME: codex, XDG_DATA_HOME: data };
await json(join(codex, "auth.json"), {
tokens: { access_token: "fixture-codex", account_id: "same" },
});
// Codex CLI never reads ~/.config/codex.
await json(join(home, ".config", "codex", "auth.json"), {
tokens: { access_token: "fixture-config", account_id: "same" },
});
await json(join(home, ".codex", "auth.json"), {
tokens: { access_token: "fixture-legacy", account_id: "same" },
});
await json(join(data, "opencode", "auth.json"), {
openai: { type: "oauth", access: "fixture-open", accountId: "same" },
});
const inputs = await discover(lookup(env));
expect(inputs.map((input) => input.route.store)).toEqual(["codex", "codex", "opencode"]);
for (const input of inputs) expect(await account(input, env)).toEqual({ key: "same" });
await json(join(data, "opencode", "auth.json"), { openai: { type: "api", key: "fixture-key" } });
expect(await discover(lookup(env))).toHaveLength(2);
});
test("OMP fetch re-reads its row after rotation and skips a newly disabled login", async () => {
const path = join(home, ".omp", "agent", "agent.db");
await database(path);
const [input] = await discover(lookup());
const { DatabaseSync } = await import("node:sqlite");
const db = new DatabaseSync(path);
db.prepare("UPDATE auth_credentials SET data = ? WHERE id = 1").run(
JSON.stringify({ access: "fixture-rotated-omp", accountId: "omp-account", expires: 2000 }),
);
expect(
(
await fetchUsage(
input!,
async (_url, init) => {
expect(new Headers(init?.headers).get("Authorization")).toBe(
"Bearer fixture-rotated-omp",
);
return new Response("{}", { status: 401 });
},
lookup(),
)
).status,
).toBe("unavailable");
db.prepare("UPDATE auth_credentials SET disabled_cause = 'revoked' WHERE id = 1").run();
db.close();
expect(
(
await fetchUsage(
input!,
async () => {
throw new Error("disabled login must not fetch");
},
lookup(),
)
).status,
).toBe("unavailable");
});
+132
View File
@@ -0,0 +1,132 @@
import { existsSync } from "node:fs";
import { readFile } from "node:fs/promises";
import { homedir } from "node:os";
import { join, resolve } from "node:path";
import { z } from "zod";
import { routeSchema } from "../shared/input.js";
export interface StoreLookup {
home?: string;
env?: NodeJS.ProcessEnv;
platform?: NodeJS.Platform;
now?: () => number;
sqlite?: () => Sqlite | undefined;
}
interface Database {
prepare(sql: string): { all(...args: string[]): unknown[] };
close(): void;
}
export interface Sqlite {
DatabaseSync: new (path: string, options: { readOnly: boolean }) => Database;
}
export const oauthSchema = z.object({
access: z.string().min(1),
accountId: z.string().optional(),
expires: z.number().optional(),
});
const rowSchema = z.object({ id: z.number().int().positive(), data: z.string() });
export type Route = z.infer<typeof routeSchema>;
export async function readJson(path: string): Promise<unknown> {
try {
return JSON.parse(await readFile(path, "utf8"));
} catch {
return null;
}
}
export function piAuthPath(lookup: StoreLookup): string {
const home = lookup.home ?? homedir();
const env = lookup.env ?? process.env;
const configured = env.PI_CODING_AGENT_DIR?.trim();
if (!configured) return join(home, ".pi", "agent", "auth.json");
if (configured === "~") return join(home, "auth.json");
const directory = configured.startsWith("~/")
? join(home, configured.slice(2))
: resolve(configured);
return join(directory, "auth.json");
}
function ompDatabasePath(lookup: StoreLookup): string {
const home = lookup.home ?? homedir();
const env = lookup.env ?? process.env;
const platform = lookup.platform ?? process.platform;
const profile = (env.OMP_PROFILE ?? env.PI_PROFILE)?.trim() || "default";
const base = join(home, env.PI_CONFIG_DIR || ".omp");
const configRoot = profile === "default" ? base : join(base, "profiles", profile);
const defaultAgentDir = join(configRoot, "agent");
const agentDir =
profile === "default" && env.PI_CODING_AGENT_DIR
? resolve(env.PI_CODING_AGENT_DIR)
: defaultAgentDir;
if (
(platform === "linux" || platform === "darwin") &&
agentDir === defaultAgentDir &&
env.XDG_DATA_HOME
) {
const xdgBase = join(env.XDG_DATA_HOME, "omp");
const xdgRoot = profile === "default" ? xdgBase : join(xdgBase, "profiles", profile);
if (existsSync(xdgRoot)) return join(xdgRoot, "agent.db");
}
return join(agentDir, "agent.db");
}
function builtinSqlite(): Sqlite | undefined {
// Node 20 must still compile/load the plugin; only OMP needs this newer builtin.
const parsed = z
.object({
DatabaseSync: z.custom<Sqlite["DatabaseSync"]>((value) => typeof value === "function"),
})
.safeParse(process.getBuiltinModule?.("node:sqlite"));
return parsed.success ? parsed.data : undefined;
}
function ompRows(path: string, lookup: StoreLookup) {
let db: Database | undefined;
try {
const sqlite = (lookup.sqlite ?? builtinSqlite)();
if (!sqlite) return [];
db = new sqlite.DatabaseSync(path, { readOnly: true });
const rows = db
.prepare(
"SELECT id, data FROM auth_credentials WHERE provider = ? AND credential_type = 'oauth' AND disabled_cause IS NULL ORDER BY id",
)
.all("openai-codex");
return rows.flatMap((value) => {
const row = rowSchema.safeParse(value);
if (!row.success) return [];
try {
const oauth = oauthSchema.parse(JSON.parse(row.data.data));
if (oauth.expires === undefined || oauth.expires <= (lookup.now ?? Date.now)()) return [];
return [{ id: row.data.id, oauth }];
} catch {
return [];
}
});
} catch {
return [];
} finally {
db?.close();
}
}
export function discoverOmp(lookup: StoreLookup): Route[] {
const path = ompDatabasePath(lookup);
return ompRows(path, lookup).map(({ id }) => ({ store: "omp", path, credentialId: id }));
}
export function readOmp(route: Extract<Route, { store: "omp" }>, lookup: StoreLookup) {
return ompRows(route.path, lookup).find((row) => row.id === route.credentialId)?.oauth ?? null;
}
export async function readHarness(
route: Extract<Route, { path: string }>,
lookup: StoreLookup = {},
) {
if (route.store === "omp") return readOmp(route, lookup);
const provider = route.store === "opencode" ? "openai" : "openai-codex";
const parsed = z.record(z.string(), z.unknown()).safeParse(await readJson(route.path));
if (!parsed.success) return null;
const oauth = oauthSchema.extend({ type: z.literal("oauth") }).safeParse(parsed.data[provider]);
return oauth.success ? oauth.data : null;
}
+43 -17
View File
@@ -1,10 +1,14 @@
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, expect, test } from "vitest";
import { fetchUsage, identify } from "./usage.js";
import { inputSchema } from "../shared/input.js";
import { inputSchema, type CodexUsageInput } from "../shared/input.js";
function authInput(directory: string): CodexUsageInput {
return { route: { store: "codex", path: join(directory, "auth.json") } };
}
let fixtureHome: string;
beforeEach(async () => {
@@ -42,7 +46,7 @@ function response(headers: HeadersInit, accountId?: string): Promise<Response> {
);
}
test("default input reads CODEX_HOME auth and preserves the usage request", async () => {
test("explicit route reads Codex auth and preserves the usage request", async () => {
const home = await mkdtemp(join(tmpdir(), "usage-codex-"));
try {
process.env["CODEX_HOME"] = home;
@@ -52,7 +56,7 @@ test("default input reads CODEX_HOME auth and preserves the usage request", asyn
tokens: { access_token: "fixture-default", account_id: "account-default" },
}),
);
const report = await fetchUsage({}, (_url, init) =>
const report = await fetchUsage(authInput(home), (_url, init) =>
response(init?.headers ?? {}, "account-default"),
);
expect(report).toMatchObject({
@@ -65,7 +69,7 @@ test("default input reads CODEX_HOME auth and preserves the usage request", asyn
}
});
test("discovery input rejects explicit credential routes", () => {
test("discovery input requires a route and rejects raw credentials", () => {
for (const input of [
{ codexHome: "/unused" },
{ accessToken: "unused" },
@@ -73,18 +77,20 @@ test("discovery input rejects explicit credential routes", () => {
]) {
expect(inputSchema.safeParse(input).success).toBe(false);
}
expect(inputSchema.parse({})).toEqual({});
expect(() => inputSchema.parse({})).toThrow();
});
test("identify returns a key when fetch finds Codex token credentials", async () => {
await writeAuth("fixture-supplied", "account-supplied");
await fetchUsage({}, (_url, init) => response(init?.headers ?? {}, "account-supplied"));
expect(await identify({})).toEqual({ key: "account-supplied" });
await fetchUsage(authInput(fixtureHome), (_url, init) =>
response(init?.headers ?? {}, "account-supplied"),
);
expect(await identify(authInput(fixtureHome))).toEqual({ key: "account-supplied" });
});
test("coerces credit balance and marks a 96 percent window dangerous", async () => {
const report = await fetchUsage(
{},
authInput(fixtureHome),
async () =>
new Response(
JSON.stringify({
@@ -110,7 +116,7 @@ test("coerces credit balance and marks a 96 percent window dangerous", async ()
test("summarizes the session and weekly windows by default, not code review", async () => {
const report = await fetchUsage(
{},
authInput(fixtureHome),
async () =>
new Response(
JSON.stringify({
@@ -132,7 +138,7 @@ test("summarizes the session and weekly windows by default, not code review", as
test("HTML usage body is unavailable", async () => {
const report = await fetchUsage(
{},
authInput(fixtureHome),
async () => new Response("<html>Login</html>", { status: 200 }),
);
expect(report.status).toBe("unavailable");
@@ -155,7 +161,7 @@ test("401 leaves auth.json byte for byte unchanged and makes no refresh request"
});
await writeFile(authPath, before);
let calls = 0;
const report = await fetchUsage({}, async (url) => {
const report = await fetchUsage(authInput(home), async (url) => {
expect(String(url)).toBe("https://chatgpt.com/backend-api/wham/usage");
calls++;
return new Response(null, { status: 401 });
@@ -168,15 +174,15 @@ test("401 leaves auth.json byte for byte unchanged and makes no refresh request"
}
});
test("default auth account claim survives token rotation", async () => {
test("auth account claim survives token rotation", async () => {
const token = (suffix: string) =>
`header.${Buffer.from(JSON.stringify({ "https://api.openai.com/auth": { chatgpt_account_id: "work-id" }, suffix })).toString("base64url")}.signature`;
await writeAuth(token("first"));
expect(await identify({})).toEqual({ key: "work-id" });
expect(await identify(authInput(fixtureHome))).toEqual({ key: "work-id" });
await writeAuth(token("second"));
expect(await identify({})).toEqual({ key: "work-id" });
expect(await identify(authInput(fixtureHome))).toEqual({ key: "work-id" });
await writeAuth("opaque-token");
expect(await identify({})).toBeNull();
expect(await identify(authInput(fixtureHome))).toBeNull();
});
test("identify reads an email label from auth id_token when access token has no profile", async () => {
@@ -190,7 +196,7 @@ test("identify reads an email label from auth id_token when access token has no
tokens: { account_id: "account-id", access_token: "opaque-token", id_token: idToken },
}),
);
expect(await identify({})).toEqual({
expect(await identify(authInput(home))).toEqual({
key: "account-id",
label: "id-owner@example.test",
});
@@ -198,3 +204,23 @@ test("identify reads an email label from auth id_token when access token has no
await rm(home, { recursive: true, force: true });
}
});
test("discovers a Pi OAuth login independently of the CLI login", async () => {
const { discover } = await import("./usage.js");
const directory = await mkdtemp(join(tmpdir(), "usage-pi-discovery-"));
try {
await mkdir(join(directory, ".pi", "agent"), { recursive: true });
await writeFile(
join(directory, ".pi", "agent", "auth.json"),
JSON.stringify({
"openai-codex": { type: "oauth", access: "fixture-pi", accountId: "pi-account" },
}),
);
const inputs = await discover({ home: directory, env: {}, platform: "linux" });
expect(inputs).toContainEqual({
route: { store: "pi", path: join(directory, ".pi", "agent", "auth.json") },
});
} finally {
await rm(directory, { recursive: true, force: true });
}
});
+55 -25
View File
@@ -1,4 +1,4 @@
import { readFile } from "node:fs/promises";
import { discoverOmp, piAuthPath, readHarness, readJson, type StoreLookup } from "./stores.js";
import { homedir } from "node:os";
import { join } from "node:path";
import {
@@ -32,28 +32,56 @@ const responseSchema = z.object({
credits: z.object({ balance: number.optional() }).nullish(),
});
export async function readAuth(
_input: CodexUsageInput,
): Promise<{ token: string; accountId?: string; idToken?: string } | null> {
const candidates = [
...(process.env["CODEX_HOME"] ? [join(process.env["CODEX_HOME"], "auth.json")] : []),
join(homedir(), ".config", "codex", "auth.json"),
join(homedir(), ".codex", "auth.json"),
interface Auth {
token: string;
accountId?: string;
idToken?: string;
expires?: number;
}
export async function discover(lookup: StoreLookup = {}): Promise<CodexUsageInput[]> {
const env = lookup.env ?? process.env;
const home = lookup.home ?? homedir();
const paths = [
...(env.CODEX_HOME ? [join(env.CODEX_HOME, "auth.json")] : []),
join(home, ".codex", "auth.json"),
];
for (const path of candidates) {
try {
const auth = authSchema.parse(JSON.parse(await readFile(path, "utf8")));
if (auth.tokens?.access_token)
return {
token: auth.tokens.access_token,
accountId: auth.tokens.account_id,
idToken: auth.tokens.id_token,
};
} catch {
continue;
}
const candidates: CodexUsageInput[] = [...new Set(paths)].map((path) => ({
route: { store: "codex", path },
}));
candidates.push(
{
route: {
store: "opencode",
path: join(env.XDG_DATA_HOME || join(home, ".local", "share"), "opencode", "auth.json"),
},
},
{ route: { store: "pi", path: piAuthPath(lookup) } },
...discoverOmp(lookup).map((route) => ({ route })),
);
const present: CodexUsageInput[] = [];
for (const input of candidates) if (await readAuth(input, lookup)) present.push(input);
return present;
}
export async function readAuth(
input: CodexUsageInput,
lookup: StoreLookup = {},
): Promise<Auth | null> {
const route = input.route;
if (route.store !== "codex") {
const oauth = await readHarness(route, lookup);
return oauth
? { token: oauth.access, accountId: oauth.accountId, expires: oauth.expires }
: null;
}
return null;
const auth = authSchema.safeParse(await readJson(route.path));
if (!auth.success || !auth.data.tokens?.access_token) return null;
return {
token: auth.data.tokens.access_token,
accountId: auth.data.tokens.account_id,
idToken: auth.data.tokens.id_token,
};
}
function usageWindow(
@@ -73,9 +101,11 @@ function usageWindow(
export async function fetchUsage(
input: CodexUsageInput,
fetchApi: typeof fetch = fetch,
lookup: StoreLookup = {},
): Promise<UsageReport> {
const auth = await readAuth(input);
if (!auth) return { status: "unavailable", windows: [] };
const auth = await readAuth(input, lookup);
if (!auth || (auth.expires !== undefined && auth.expires <= (lookup.now ?? Date.now)()))
return { status: "unavailable", windows: [] };
const headers: Record<string, string> = {
Authorization: `Bearer ${auth.token}`,
Accept: "application/json",
@@ -151,8 +181,8 @@ function claimString(value: unknown): string | null {
return typeof value === "string" && value.length > 0 ? value : null;
}
export async function identify(input: CodexUsageInput) {
const auth = await readAuth(input);
export async function identify(input: CodexUsageInput, lookup: StoreLookup = {}) {
const auth = await readAuth(input, lookup);
if (!auth) return null;
const access = jwtClaims(auth.token);
const id = jwtClaims(auth.idToken);
+11 -1
View File
@@ -1,4 +1,14 @@
import { z } from "zod";
export const inputSchema = z.object({}).strict();
export const routeSchema = z.discriminatedUnion("store", [
z.object({ store: z.enum(["codex", "opencode", "pi"]), path: z.string().min(1) }).strict(),
z
.object({
store: z.literal("omp"),
path: z.string().min(1),
credentialId: z.number().int().positive(),
})
.strict(),
]);
export const inputSchema = z.object({ route: routeSchema }).strict();
export type CodexUsageInput = z.infer<typeof inputSchema>;