Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion packages/desktop-control/src/contract-audit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export interface RendererContractAuditInspection {
ownerCount: number;
reserveGateCount: number;
accountGateCount: number;
combinedGateCount: number;
};
fork: {
annotatedResponseCount: number;
Expand Down Expand Up @@ -214,7 +215,13 @@ export function validateRendererContractAuditInspection(
),
codexUsageGate: integerRecord(
value.codexUsageGate,
["composerCount", "ownerCount", "reserveGateCount", "accountGateCount"] as const,
[
"composerCount",
"ownerCount",
"reserveGateCount",
"accountGateCount",
"combinedGateCount",
] as const,
"Renderer Codex usage gate contract",
),
fork: integerRecord(
Expand Down
8 changes: 7 additions & 1 deletion packages/desktop-control/test/contract-audit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,13 @@ const validInspection = {
textBodyCount: 2,
textBodyOwnerCount: 2,
},
codexUsageGate: { composerCount: 1, ownerCount: 1, reserveGateCount: 1, accountGateCount: 1 },
codexUsageGate: {
composerCount: 1,
ownerCount: 1,
reserveGateCount: 1,
accountGateCount: 1,
combinedGateCount: 0,
},
fork: { annotatedResponseCount: 0, candidateButtonCount: 0, verifiedButtonCount: 0 },
production: {
bindingPresent: false,
Expand Down
2 changes: 2 additions & 0 deletions packages/renderer-extension/src/contract-audit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,14 @@ function codexUsageGateCounts(
ownerCount: 0,
reserveGateCount: 0,
accountGateCount: 0,
combinedGateCount: 0,
};
for (const composer of editable) {
const inspection = inspectComposerCodexUsageGate(composer);
counts.ownerCount += inspection.ownerCount;
counts.reserveGateCount += inspection.reserveGateCount;
counts.accountGateCount += inspection.accountGateCount;
counts.combinedGateCount += inspection.combinedGateCount;
}
return counts;
}
Expand Down
57 changes: 53 additions & 4 deletions packages/renderer-extension/src/renderer-codex-usage-gate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,36 @@ type Subscriber = {
type Hook = { memoizedState?: unknown; queue?: unknown; next?: Hook | null };
type Fiber = { memoizedProps?: unknown; memoizedState?: unknown; alternate?: unknown };
type Instance = { value: unknown; getSnapshot(): unknown };
type GateKind = "account" | "reserve";
/**
* `account` and `reserve` are the two separately-verified Desktop gates.
* `combined` is Desktop 26.1007's merged selector: it folds the Account
* rate-limit gate and the reserve `active` flag into one boolean. It is only
* accepted when every field it reads belongs to the known usage-domain
* whitelist below; anything else keeps the native restriction.
*/
type GateKind = "account" | "reserve" | "combined";

/**
* Fields a verified `combined` gate may read. A combined selector touching
* anything outside these usage-domain fields is treated as unidentifiable
* (native restrictions stay), so unrelated blockers can never be lifted.
*/
const COMBINED_GATE_FIELDS: ReadonlySet<string> = new Set([
"auth.authMethod",
"auth.accountId",
"auth.accountLoading",
"auth.accountStructure",
"auth.authLoading",
"auth.authenticatedAccountId",
"auth.plan",
"auth.userId",
"limit.allowed",
"limit.primary_window",
"limit.secondary_window",
"reserve.active",
"reserve.hardBlocked",
"reserve.eligible",
]);

/** One Desktop `useSyncExternalStore` subscription: memo, instance and effect hooks. */
interface Subscription {
Expand Down Expand Up @@ -203,9 +232,21 @@ function gateKind({ store, atom }: Subscription): GateKind | "mixed" | null {
}
const reserve = read.has("reserve.hardBlocked") && !read.has("reserve.active");
const account = read.has("auth.authMethod") && read.has("limit.allowed");
// A combined selector cannot be separated into the two verified gates.
if (account && (read.has("reserve.hardBlocked") || read.has("reserve.active"))) return "mixed";
// Desktop 26.1007 folds the Account rate-limit gate and the reserve `active`
// flag into one selector. Accept it only when every field it reads is a known
// usage-domain field; anything outside the whitelist is unidentifiable.
const combined =
account &&
read.has("reserve.active") &&
!read.has("reserve.hardBlocked") &&
[...read].every((field) => COMBINED_GATE_FIELDS.has(field));
// A selector combining the verified gates with unrecognized fields cannot be
// separated safely, so the native restriction stays.
if (account && !combined && (read.has("reserve.hardBlocked") || read.has("reserve.active"))) {
return "mixed";
}
if (reserve) return "reserve";
if (combined) return "combined";
if (account) return "account";
return null;
}
Expand Down Expand Up @@ -244,6 +285,7 @@ function gateSubscriptions(
const found: Record<GateKind, Subscription[]> & { invalid: boolean } = {
account: [],
reserve: [],
combined: [],
invalid: false,
};
for (const hook of hooksOf(owner)) {
Expand All @@ -262,6 +304,8 @@ function gateSubscriptions(
* Gate subscriptions by kind; `null` when a kind is ambiguous. The account gate
* returns early without reading usage when it cannot block (for example API-key
* sign-in), so it may legitimately be absent until it reads `rate_limit`.
* On Desktop 26.1007 the account gate is folded into the `combined` selector;
* at most one of the two forms may be present, never both.
*/
function discoverGates(
owner: Fiber,
Expand All @@ -270,18 +314,22 @@ function discoverGates(
const gates = gateSubscriptions(owner, projections);
if (gates.invalid) return null;
const found = new Map<GateKind, Subscription>();
for (const kind of ["account", "reserve"] as const) {
for (const kind of ["account", "reserve", "combined"] as const) {
const subscriptions = gates[kind];
if (subscriptions.length > 1) return null;
if (subscriptions[0]) found.set(kind, subscriptions[0]);
}
// The separate account selector and the merged combined selector are two
// generations of the same gate; both present means an unknown contract.
if (found.has("account") && found.has("combined")) return null;
return found;
}

export interface RendererCodexUsageGateInspection {
ownerCount: number;
reserveGateCount: number;
accountGateCount: number;
combinedGateCount: number;
}

/** Read-only contract audit: counts only, never patches or subscribes. */
Expand All @@ -292,6 +340,7 @@ export function inspectComposerCodexUsageGate(composer: Element): RendererCodexU
ownerCount: owners.length,
reserveGateCount: gates?.reserve.length ?? 0,
accountGateCount: gates?.account.length ?? 0,
combinedGateCount: gates?.combined.length ?? 0,
};
}

Expand Down
113 changes: 111 additions & 2 deletions packages/renderer-extension/test/renderer-codex-usage-gate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@ function desktopStore() {
let authMethod = "chatgpt";
let allowed = false;
let hardBlocked = false;
const auth: Atom = { read: () => ({ authMethod, authenticatedAccountId: "account" }) };
const auth: Atom = {
read: () => ({
authMethod,
authenticatedAccountId: "account",
}),
};
const usage: Atom = { read: () => ({ data: { rate_limit: { allowed } } }) };
const reserve: Atom = { read: () => ({ active: false, eligible: true, hardBlocked }) };
const accountGate: Atom = {
Expand All @@ -34,6 +39,21 @@ function desktopStore() {
read: (get) => (get(reserve) as { hardBlocked: boolean }).hardBlocked,
};
const reserveActive: Atom = { read: (get) => (get(reserve) as { active: boolean }).active };
// Desktop 26.1007 folds the Account gate and the reserve `active` flag into
// one selector over the same usage-domain fields.
const combinedGate: Atom = {
read: (get) => {
const account = accountGate.read(get);
const active = (get(reserve) as { active: boolean }).active;
const authSnapshot = get(auth) as { authMethod: string };
return Boolean(account) || (authSnapshot.authMethod === "chatgpt" && active);
},
};
// Negative-fixture probe: auth-shaped snapshot carrying a field outside the
// verified whitelist.
const combinedAuthProbe: Atom = {
read: () => ({ authMethod: "chatgpt", scopedBeta: false }),
};
const listeners = new Map<Atom, Set<() => void>>();
const emit = () => {
// Model Jotai's propagation to derived signal atoms; React's listener
Expand All @@ -57,6 +77,8 @@ function desktopStore() {
return {
store,
accountGate,
combinedGate,
combinedAuthProbe,
reserveGate,
reserveActive,
setAuthMethod(value: string) {
Expand All @@ -83,6 +105,7 @@ function composerFixture(
duplicateAccount?: boolean;
snapshotWrapper?: boolean;
signalReserve?: boolean;
combinedAccount?: boolean;
} = {},
) {
const subscribers: Array<{ getSnapshot(): unknown }> = [];
Expand All @@ -91,8 +114,9 @@ function composerFixture(
const rerender = vi.fn();
for (const selector of [
source.reserveActive,
source.accountGate,
...(options.combinedAccount ? [] : [source.accountGate]),
...(options.omitReserve ? [] : [source.reserveGate]),
...(options.combinedAccount ? [source.combinedGate] : []),
...(options.duplicateAccount ? [source.accountGate] : []),
]) {
// Desktop 26.928 wraps parameterized selectors in a readonly signal atom.
Expand Down Expand Up @@ -211,6 +235,7 @@ describe("Codex usage gate for external Harness Composers", () => {
ownerCount: 1,
reserveGateCount: 1,
accountGateCount: 1,
combinedGateCount: 0,
});
expect(source.store.sub.mock.calls).toHaveLength(calls);
expect(external.blocked()).toBe(true);
Expand All @@ -228,6 +253,89 @@ describe("Codex usage gate for external Harness Composers", () => {
expect(external.instances.map((instance) => instance.getSnapshot)).toEqual(nativeSnapshots);
});

it("lifts the 26.1007 combined usage selector for this Composer only", () => {
const source = desktopStore();
const external = composerFixture(source, {
combinedAccount: true,
snapshotWrapper: true,
signalReserve: true,
});
const codex = composerFixture(source, {
combinedAccount: true,
snapshotWrapper: true,
signalReserve: true,
});
const listeners = source.listenerCount();
const originalSub = source.store.sub;

expect(inspectComposerCodexUsageGate(external.composer)).toEqual({
ownerCount: 1,
reserveGateCount: 1,
accountGateCount: 0,
combinedGateCount: 1,
});
expect(external.blocked()).toBe(true);
expect(source.store.get(source.combinedGate)).toBe(true);
expect(external.gate.update(true)).toBe("bypassed");
expect(external.blocked()).toBe(false);
expect(codex.blocked()).toBe(true);
expect(source.store.get(source.combinedGate)).toBe(true);
expect(source.store.set).not.toHaveBeenCalled();
expect(source.store.sub).toBe(originalSub);
expect(source.listenerCount()).toBe(listeners);

expect(external.gate.update(false)).toBe("native");
expect(external.blocked()).toBe(true);
});

it("keeps a combined selector that reads unknown fields native", () => {
const source = desktopStore();
// Let the Account part be false so the selector's second operand (reading
// an auth-shaped field outside the verified whitelist) is actually
// evaluated on the replay path.
source.setAllowed(true);
const combinedRead = source.combinedGate.read;
source.combinedGate.read = (get) => {
const result = combinedRead(get);
const authSnapshot = get(source.combinedAuthProbe) as { scopedBeta?: boolean };
return Boolean(result) || authSnapshot.scopedBeta === true;
};
const f = composerFixture(source, {
combinedAccount: true,
snapshotWrapper: true,
signalReserve: true,
});
expect(f.gate.update(true)).toBe("unsupported");
expect(f.blocked()).toBe(false);
});

it("rejects a Composer exposing both the account and combined gates", () => {
const source = desktopStore();
const both = composerFixture(source, {
snapshotWrapper: true,
signalReserve: true,
});
// Inject the combined gate's subscription trio alongside the account gate.
const combined = composerFixture(source, {
combinedAccount: true,
snapshotWrapper: true,
signalReserve: true,
});
const memoIndex = combined.hooks.findIndex(
(hook) =>
Array.isArray(hook.memoizedState) &&
(hook.memoizedState as [{ getSnapshot(): unknown }, unknown[]])[0] ===
required(combined.subscribers[2]),
);
expect(memoIndex).toBeGreaterThanOrEqual(0);
const trio = combined.hooks.slice(memoIndex, memoIndex + 3);
const previousTail = both.hooks[both.hooks.length - 1];
if (!previousTail) throw new Error("Missing fixture hook chain");
previousTail.next = trio[0] ?? null;
expect(both.gate.update(true)).toBe("unsupported");
expect(both.blocked()).toBe(true);
});

it("keeps unrelated or mismatched snapshot wrappers native", () => {
const f = composerFixture(undefined, { snapshotWrapper: true, signalReserve: true });
const subscriber = required(f.subscribers[1]);
Expand Down Expand Up @@ -414,6 +522,7 @@ describe("Codex usage gate for external Harness Composers", () => {
ownerCount: 1,
reserveGateCount: 1,
accountGateCount: 1,
combinedGateCount: 0,
});
expect(f.blocked()).toBe(true);
expect(f.source.store.sub).toBe(sub);
Expand Down
Loading