From 5a178f2cac0c87c4764090f9241a85c762be72e6 Mon Sep 17 00:00:00 2001 From: Brave Date: Fri, 16 Oct 2026 12:47:30 +0800 Subject: [PATCH] fix(renderer): lift Desktop 26.1007 combined Codex usage gate Desktop 26.1007 folds the Account rate-limit selector and the reserve active flag into one boolean selector. The gate probe classified it as 'mixed' and rejected the whole owner, so external-Harness Composers kept the native usage restriction (send disabled while Codex quota is exhausted). Accept the merged selector as a new 'combined' gate kind, but only when every field it reads belongs to the known usage-domain whitelist; any unknown field still fails identification and keeps native restrictions. The separate account and combined forms must never appear together. --- .../desktop-control/src/contract-audit.ts | 9 +- .../test/contract-audit.test.ts | 8 +- .../renderer-extension/src/contract-audit.ts | 2 + .../src/renderer-codex-usage-gate.ts | 57 ++++++++- .../test/renderer-codex-usage-gate.test.ts | 113 +++++++++++++++++- 5 files changed, 181 insertions(+), 8 deletions(-) diff --git a/packages/desktop-control/src/contract-audit.ts b/packages/desktop-control/src/contract-audit.ts index 1344445e1..203a5f731 100644 --- a/packages/desktop-control/src/contract-audit.ts +++ b/packages/desktop-control/src/contract-audit.ts @@ -54,6 +54,7 @@ export interface RendererContractAuditInspection { ownerCount: number; reserveGateCount: number; accountGateCount: number; + combinedGateCount: number; }; fork: { annotatedResponseCount: number; @@ -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( diff --git a/packages/desktop-control/test/contract-audit.test.ts b/packages/desktop-control/test/contract-audit.test.ts index a846ca8f9..a66cafff8 100644 --- a/packages/desktop-control/test/contract-audit.test.ts +++ b/packages/desktop-control/test/contract-audit.test.ts @@ -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, diff --git a/packages/renderer-extension/src/contract-audit.ts b/packages/renderer-extension/src/contract-audit.ts index d851e6c82..03b6493b9 100644 --- a/packages/renderer-extension/src/contract-audit.ts +++ b/packages/renderer-extension/src/contract-audit.ts @@ -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; } diff --git a/packages/renderer-extension/src/renderer-codex-usage-gate.ts b/packages/renderer-extension/src/renderer-codex-usage-gate.ts index 49c242d59..e1d497e3a 100644 --- a/packages/renderer-extension/src/renderer-codex-usage-gate.ts +++ b/packages/renderer-extension/src/renderer-codex-usage-gate.ts @@ -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 = 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 { @@ -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; } @@ -244,6 +285,7 @@ function gateSubscriptions( const found: Record & { invalid: boolean } = { account: [], reserve: [], + combined: [], invalid: false, }; for (const hook of hooksOf(owner)) { @@ -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, @@ -270,11 +314,14 @@ function discoverGates( const gates = gateSubscriptions(owner, projections); if (gates.invalid) return null; const found = new Map(); - 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; } @@ -282,6 +329,7 @@ export interface RendererCodexUsageGateInspection { ownerCount: number; reserveGateCount: number; accountGateCount: number; + combinedGateCount: number; } /** Read-only contract audit: counts only, never patches or subscribes. */ @@ -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, }; } diff --git a/packages/renderer-extension/test/renderer-codex-usage-gate.test.ts b/packages/renderer-extension/test/renderer-codex-usage-gate.test.ts index aa6c371fe..91887ff5a 100644 --- a/packages/renderer-extension/test/renderer-codex-usage-gate.test.ts +++ b/packages/renderer-extension/test/renderer-codex-usage-gate.test.ts @@ -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 = { @@ -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 void>>(); const emit = () => { // Model Jotai's propagation to derived signal atoms; React's listener @@ -57,6 +77,8 @@ function desktopStore() { return { store, accountGate, + combinedGate, + combinedAuthProbe, reserveGate, reserveActive, setAuthMethod(value: string) { @@ -83,6 +105,7 @@ function composerFixture( duplicateAccount?: boolean; snapshotWrapper?: boolean; signalReserve?: boolean; + combinedAccount?: boolean; } = {}, ) { const subscribers: Array<{ getSnapshot(): unknown }> = []; @@ -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. @@ -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); @@ -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]); @@ -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);