Repository navigation
Conversation
Rebuilds the provider access layer as a headless Playwright-Chromium
service. No GUI, no REST, no WebSocket server, no Electron import.
The existing automation client attaches unchanged over the documented
loopback contract: TCP 127.0.0.1:19222, newline-delimited JSON,
{requestId, action, provider, data} in and {requestId, success, ...} out.
Components:
state-store settings (preference) vs port fact (authority)
cookie-store domain+name auth detection, backup, injection
session-manager per-provider persistent contexts, A6-preserving unload
engine-loader init-script injection with presence probing
ipc-server framing, dispatch, loopback enforcement
actions the action contract
Engines carry all provider protocol knowledge; the gateway carries none.
Tests: 53 unit + integration, plus 10 real-Chromium tests that skip
cleanly when Playwright is absent. CI runs unit, real-browser and a
syntax check in parallel.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
All three were test bugs, not product bugs:
- browser: a re-injection test never registered its init script, so the
global was genuinely undefined after reload rather than stale
- browser: Playwright's cookies() filter needs a full URL, not a bare
domain ('claude.ai' -> 'https://claude.ai')
- integration: the A8 placeholder-retry test raced a setTimeout, so it
passed locally and failed on a slow runner. Replaced with a queued
response list, which also pins the exact attempt count.
Adds a second A8 case asserting a run of placeholders reports failure
rather than surfacing a placeholder as an answer.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Coverage was 86.18 line / 77.85 branch / 76.38 funcs - branch and funcs under the 80 bar. Now 92.70 / 84.53 / 88.64 across 88 tests. Adds contract.test.js: 34 tests covering the actions and error paths the report showed as untouched - executeScript, navigate, debugDOM, getTypingStatus, waitForSendButton, getCookies, setSetting, multi-provider unload/purge, attachment gating, engine error classification, and the session lifecycle. Product fix found while covering engine-loader: call() returned the in-page __error sentinel to the caller as if it were a legitimate result, so a missing engine method would look like a successful call returning nonsense. It now raises a typed EngineError. Regression test added. Adds scripts/check-coverage.cjs, a real gate: it parses the report, fails below 85/80/80, names the weakest files, and refuses to pass when the report is unparseable or the totals row is missing. CI runs it after the suite, so coverage cannot silently regress. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The gateway holds session cookies and provider bearer tokens, and those values reach error paths: a Playwright failure can quote the URL it was fetching, and a provider can echo request headers back in an error body. src/redact.js - regex for known shapes (bearer, JWT, provider cookies, sensitive query params) plus truncation for opaque values. Diagnostics are preserved: action names, requestIds, provider names, status codes and error text survive; only secret-shaped values are masked. Cookie names are kept so a stale session is still diagnosable. containsSecret() makes a regression detectable rather than merely improbable. src/logger.js - redaction at the sink, not the source, because call sites should not have to know which values are secret. There is deliberately no raw console path. toSafeError() is the single normaliser for internal errors crossing to the wire. Applied to: action dispatch, IPC framing, bind failures, startup and uncaught-exception stacks. Also removed the reference engine's practice of reading a failure body into the error, since a provider can echo the credential there. Two product bugs found and fixed while testing this: - toSafeError did not set success:false, so the IPC catch path returned a response with no success field at all - a caller would read the missing field as 'probably fine'. - The engines README-per-provider set shipped Perplexity looking ready while the other four refused. Perplexity's endpoint was equally unverified, so it now carries the same guard. Having one engine that looks finished would misrepresent the state of the work. Engine scaffolds for chatgpt, claude, gemini and qwen via scripts/make-engine-scaffold.cjs. Each is contract-complete - state handling, failure classification, empty-response discipline - and each REFUSES to issue a request while verified=false, so a guessed endpoint cannot masquerade as a working one. Tests: 131, all passing. Coverage 94.83 line / 83.26 branch / 87.58 funcs. redact/leak/engines suites prove secrets do not survive any boundary. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
An independent review found real defects, all confirmed by reproducing them before fixing. Two were named invariants that the code did not actually hold while the tests and the API claimed it did. CRITICAL - A6 broken on the primary login path. unloadProvider reported authPreserved:true while logging the operator out: the Playwright context is non-persistent, and the cookie jar was flushed only by setCookies and flushAll, never before a context was closed. An interactive login existed only in the context, so the first unload discarded it. The existing test passed because it set up through setCookies, the one path that already flushed - the test verified the path that was not broken. Now flushes before close and derives authPreserved from disk instead of hardcoding true, with a warning when preservation cannot be confirmed. CRITICAL - session cookies in plaintext on the wire. Only the failure branch of the IPC response was redacted, so getCookies, debugDOM and executeScript returned secrets verbatim. redactCookies was written for exactly this and called from nowhere. Now applied at the IPC boundary and by getCookies directly - the generic redactor cannot see a bare cookie value, which has no name=value shape. CRITICAL - the Authorization rule leaked. It required a value that could not contain a space and excluded 'basic', so 'Authorization: Basic <creds>' passed through untouched, while 'ApiKey <creds>' had its scheme destroyed and kept its credential - looking redacted while leaking. A scheme allowlist cannot be correct for an unknown scheme space. Now generic, value always masked, scheme preserved, and genuinely idempotent (matching the marker as an alternative rather than excluding it with a lookahead, which does not survive backtracking). Also fixed: sensitive-cookie names derived from the provider registry so __cf_bm can no longer drift out of the redaction list; 'no response' and nullish literals recognised as placeholders; getResponse no longer returns a placeholder as success; purgeProvider no longer reports a no-op as success on the one destructive action; capture retry clamped in both directions so a caller cannot hold a connection's queue for hours; navigate surfaces engine/transport failure instead of reporting success and failing confusingly later; three context-leak paths closed (unclosed context on failed init, concurrent first-touch, double browser launch); credential files written with the mode on the write via temp+rename, and the primary auth store protected at all - with a warning on win32 where POSIX 0600 is not enforceable rather than silently claiming it is. Tests: 155, all passing. Coverage 96.21 line / 83.27 branch / 91.57 funcs. regression.test.js pins each finding by the invariant it protects. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The gateway had a daemon and an IPC contract but no way for a person to reach it: no argv, no stdin, no prompts. Cookie login existed and was unreachable. Adds plain commands, deliberately not a REPL - the automation client is the interactive consumer, and a human logs in a couple of times a month. proxima-gw check is it running, on what port proxima-gw status per-provider load and auth state proxima-gw login <p> --cookies <f> inject a cookie export proxima-gw login <p> --headed real window, log in by hand proxima-gw logout <p> destroy that auth on disk Plain uncoloured output so it pipes cleanly, non-zero exit codes because a script trusts them, and no stack traces at the user. PowerShell wrappers proxima-gw.ps1 (CLI) and proxima-gw-daemon.ps1 (-Start/-Stop, which waits for the port fact instead of racing startup). --cookies --stdin also works, so this composes: Get-Content .\cookies\claude.json -Raw | proxima-gw login claude --stdin Two interop defects fixed, both of which a CLI would surface immediately: The port fact went to the gateway's own state dir, but the existing client reads ipc-port.json from the legacy Electron userData dir. The client could never have found this gateway and would have reported 'Proxima is not running' against a healthy process. The fact is now written to both locations - profiles and cookie backups stay private to the gateway's state dir, but the port is a shared fact. A test pins the path against the same resolution the client uses, so the two cannot drift apart again. Port collision silently relocated to port+1. That is worse than failing: the fact then points a client at whichever process won 19222, and a client probing candidates in order will talk to the wrong one - meaning the legacy Electron app answering a gateway request. Collision now fails loudly and names the other pid; relocating is opt-in via PROXIMA_GATEWAY_PORT=1 and warns when it happens. Tests: 8 files. The CLI suite drives real argv, real sockets and real exit codes against a stub gateway, because a CLI returning 0 on failure is worse than no CLI. CI also parses the PowerShell wrappers and asserts the CLI exits non-zero when nothing is running. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GitHub's default Linux shell is 'bash -e {0}', which has no pipefail. So
'npm run test:coverage | tee coverage.txt' returned tee's exit code, and a
failing suite reported success. A green job had 1 of 157 tests failing in
it - 'port collision retries +1', a test asserting the auto-relocate
behaviour that was intentionally removed a commit earlier.
'shell: bash' is 'bash -eo pipefail', so the pipeline now propagates the
test result. This has been latent since the tee was introduced.
Also fixed the stale test itself to assert the new contract (fail loudly by
default, relocate only on opt-in), and wrapped it in try/finally: a thrown
assert left the squatter's server handle open, which timed out the whole
file and buried the real failure under an unrelated one.
The CLI suite needed more than the 20s per-test budget, so it now has 60s.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Writing the CLI tests found defects in the CLI, not just in the tests.
parseArgs could not tell a flag value from a command name, so
'--headless logout qwen' parsed as headless='logout' with one positional
and the command never ran. Value-taking flags are now an explicit list
(only --cookies), so everything else is boolean.
The CLI crashed on every socket-level error: call() returns {ok, error}
with no response field on a connection failure, and the handlers read
res.response.error unconditionally - so the exact moment you most need a
message (gateway down) produced 'Cannot read properties of undefined'.
All four sites now fall back to the transport error.
FALLBACK_ATTEMPTS was 0, which made allowRelocate a flag that promised
something the code never did: opting in relocated nowhere. It is 1, and
the default remains to refuse.
The test harness deadlocked: runCli used spawnSync while the stub gateway
ran in the same process, so the child connected but the stub could never
reply and every end-to-end test hung to its timeout. runCli is async now.
Per-call CLI timeouts dropped from 30-90s to 15s - a CLI answers a human
and must not look like it is hanging.
Tests were also writing the port fact into the real %APPDATA%\proxima,
because that is now the gateway's fact location. Five test files started
servers concurrently and both polluted the actual user profile and raced
each other over one fact file, which is what made 'port fact beats stale
preference' fail intermittently. Each file now redirects the fact path to
a temp file, and nothing touches the real profile.
Tests: 177, all passing. Coverage 94.51 line / 80.26 branch / 91.43 funcs.
Branch is close to the floor - proxima-gw.js is the weakest at 81.9% line,
so the next change there should come with tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
They exited 2, a usage error, so 'proxima-gw --help && next-thing' would skip the next thing - asking for help looked like a failure. 'help' and '--help' now agree on 0, while no arguments stays 2 because being asked to do nothing is not success. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t, not a Proxima gap GPT-6 shipped on this account. /backend-api/models now reports default_model_slug gpt-6, and the picker's latest version is GPT-6: Instant gpt-6-instant Medium gpt-6-thinking thinking_effort 'standard' High gpt-6-thinking thinking_effort 'extended' Measured on the live engine, with controls: gpt-6-thinking @Extended served as asked gpt-6-thinking @max served as asked, max recorded on the reply gpt-6 @Extended served as asked gpt-6.1-sol-wm @max REWRITTEN -> gpt-6 gpt-6-astra-wm @max REWRITTEN -> gpt-6 gpt-6-nonexistent-zz echoed (control: an unknown slug is echoed, so only a rewrite counts as evidence) The wanted order was Astra, then GPT-6.1 Sol. Neither is available. This also corrects the 2026-09-25 conclusion that the /f/conversation + prepare/conduit_token handshake was what stood between Proxima and Astra. The user's own thread, sent by the real web app through that exact flow, asked for gpt-6-astra-wm at max and was served gpt-5-6. The plan is 'personal', and Astra is reported as Pro-tier. No handshake changes that. 'max' is not a picker preset, but it is accepted and recorded. Whether it reasons harder than 'extended' is NOT established: on an easy question all three levels finished in about 1s. It is the default because max was requested. Reviews follow the change through a new reviewThinkingEffort config key. The name-derived resolveThinkingEffort() can only ever produce 'extended', so 'max' was unreachable from configuration before. This takes effect on the next review with no restart: reviews name the model explicitly, and the running engine already serves gpt-6-thinking at max. The engine DEFAULT, used by ask_chatgpt, needs a Proxima restart.
…ve sanitized capture
chatgpt-audio.cjs sends an audio file to ChatGPT by driving the real web app inside
Proxima's ChatGPT tab: it opens a new chat, attaches the file through the app's
general file input, types, sends, and reads the reply back through the API. It
needs no Proxima restart.
Why the app and not the engine. Measured 2026-10-10, same mp3, same question, on
gpt-6-thinking:
real app, through its own composer heard it (WORD=pineapple NUMBER=47)
engine, with the app's exact upload bodies CANNOT_HEAR
engine, use_case 'multimodal' CANNOT_HEAR
The server stored all three user turns identically (multimodal_text, then text,
then audio_asset_pointer). So the cause is neither the upload nor the message. It
is how the turn is invoked. The app posts to /backend-api/f/conversation behind a
prepare/conduit_token handshake and a two-step sentinel; the engine still uses
/backend-api/conversation, where the model can see the file but not hear it. This
corrects the earlier claim that ChatGPT cannot process audio at all. That claim
rested on /backend-api/models accepted_mime_types, which turned out to list only
the document path: image/* is absent from it too, and images demonstrably work.
Verified three times end to end, 15-19s each.
The capture was the method, not a side effect. An in-page fetch/XHR recorder
replaced the manual DevTools HAR export, and it can be re-run with --capture.
Sanitizing happens inside the page, before anything leaves it.
Two sanitizer bugs found by scanning the output instead of trusting the label:
- Response BODIES were not URL-scrubbed. upload_url is an Azure SAS whose sig= is
the credential, and the first capture shipped one intact. (It was a short-lived,
single-blob write URL, now re-cleaned.)
- The first fix consumed the backslash of a JSON-escaped \" and corrupted the
file. A JSON.parse guard caught it before anything was written.
Both are pinned by chatgpt-audio-sanitizer-test, which runs the sanitizer exactly
as the tool generates it. It was negative-tested: with the SAS scrub removed, it
fails on both leak cases.
Also hardened: right after a reload the composer is visible before the file input
is wired up, so the attach is confirmed by an upload actually starting, and retried
if not. One run had sat for 90s with no upload.
capture.cjs records what a site's web app actually sends, from inside any loaded Proxima tab (chatgpt, claude, qwen, gemini, perplexity), and writes it as a sanitized HAR. This replaces "open DevTools, reproduce the flow, Save all as HAR, send the file over". No part of it is specific to one site: the page's own origin decides what is first-party, and fetch and XHR are wrapped identically everywhere. --seconds N | --stop-file F drains to disk every 2s and reinstalls after a reload --start / --stop single-page flows only; a page reload ends it There is ONE sanitizer: lib/page-capture.cjs. Its functions are injected into the page via Function.toString, and the same functions run again in Node as a second pass. chatgpt-audio.cjs now uses it as well, in place of the copy it carried. A copied sanitizer drifts from its original, and the drifted ChatGPT-only version is the one that leaked an Azure SAS sig= from a response body. Every write goes through the same gate: second-pass scrub, then a JSON.parse guard, then a leak scan across 7 checks. If anything still looks like a credential or an email address, nothing is written and the exit code is 3. Verified live on chatgpt, claude and qwen. The test traffic was read-only but deliberately secret-heavy: session tokens, account emails, auth headers. All three files scanned clean. Then came the step the scanner cannot do: listing every unmasked string of 16+ characters by key name. That found personal fields the patterns did not know, Claude's full_name and display_name and ChatGPT's deviceId and region. They are now masked as <pii>. Opaque ids are kept, because protocol work needs them. Gemini and perplexity are supported through --init, which loads the tab first, but they are untested: neither was loaded. chatgpt-audio.cjs was re-verified after the refactor: 4/4 heard, and its capture of 23 requests scanned clean. page-capture-test pins 21 assertions: Azure SAS raw and JSON-in-JSON, Alibaba OSS signatures, Gemini at= and f.sid, JWTs, secret-named fields, emails, personal fields, sensitive headers, a scanner that must see raw leaks, and the things that must survive (own-origin queries, model names, words like "format"). It replaces chatgpt-audio-sanitizer-test, which tested the now-removed copy. docs/ORCHESTRATION_LOOP.md is the orchestrator-facing loop description, now listing both scripts.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Clean-room rebuild of the provider access layer as a headless Playwright-Chromium service. No GUI, no REST, no WebSocket server, no Electron import.
Why
The upstream runtime is an Electron app, but the real dependency is a persistent per-provider Chromium profile plus an authenticated same-origin fetch(). Nothing in the browser layer needs Electron - the 12 distinct Electron APIs in use each have a first-class Playwright equivalent.
The gateway speaks the loopback contract the existing automation client already uses, so that client attaches unchanged: TCP 127.0.0.1:19222, newline-delimited JSON.
Layout
Engines carry all provider protocol knowledge; the gateway carries none.
Tests
CI runs unit, real-browser and a syntax check in parallel.
Decisions made without asking
Known gaps