Skip to content

[WRONG BRANCH] chore: temporary dev delta extraction for #1206 rebase - #1234

Closed
Wibias wants to merge 56 commits into
agent/rebase-1206-oldbase-tmpfrom
dev
Closed

[WRONG BRANCH] chore: temporary dev delta extraction for #1206 rebase#1234
Wibias wants to merge 56 commits into
agent/rebase-1206-oldbase-tmpfrom
dev

Conversation

@Wibias

@Wibias Wibias commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Temporary maintainer-only PR used to extract the exact current-dev delta from #1206's previous base. Do not review or merge.

lidge-jun and others added 30 commits August 7, 2026 19:50
The space after the colon is optional in text/event-stream, so `data:{...}` is
as valid as `data: {...}`. Six parsers hardcoded the spaced form and silently
dropped every frame from a producer that omits it. To the user that looked like
a completed turn with no content.

The same wire format was already accepted on the relay path
(`sse-decoder.ts`, `relay.ts`, `google.ts`) and rejected on the adapter path.
That split is the bug.

Adds two primitives beside the decoder whose rule they mirror:

- `sseFieldValue(line, field)` for the five string-slicing parsers
- `sseFieldOffset(text, start, end, field)` for the live Claude relay, whose
  translator-budget accounting reserves bytes by offset — materializing the
  line first would allocate the string the budget exists to bound

Call sites fixed: `adapters/openai-chat.ts`, `chat/outbound.ts`,
`claude/outbound.ts` (two independent parsers, both `event` and `data`),
`web-search/parse.ts`, `server/claude-messages.ts`.

Both helpers strip at most one leading space, matching the decoder, so a
payload that legitimately begins with whitespace keeps the rest of it. Neither
trims: callers own that choice and some intentionally keep trailing bytes.

Deferred deliberately, not fixed here: `\r\n\r\n` frame delimiting and
multiline `data` joining without the spec's `\n` separator. Both are
frame-level rather than field-level and carry a different blast radius.

Verification: each new test was confirmed to fail with the fix reverted.
…ll six call sites

Audit found two real gaps in the previous commit.

`sseFieldValue("data", "data")` returned null while `decodeServerSentEvents`
treats a colonless field as an empty value (`colon < 0` -> valueStart =
line.length, sse-decoder.ts:240). Two helpers that mirror the decoder must not
disagree with it. Both now return the empty value, and `sseFieldOffset` returns
the end-of-line offset for the same case.

Regression coverage was also incomplete: `chat/outbound.ts` and
`claude/outbound.ts` had no unspaced test, so two of the six fixed call sites
were unverified. Both now have one, and each was confirmed to fail with its fix
reverted.

Also corrects the remaining stale "5 call sites" claims in the plan unit.
…elds (#1170)

The offset-based parser inside responsesSseToAnthropicSse was covered only
indirectly. This drives it head-on: the same six Responses frames in spaced and
unspaced form, asserting identical event names, identical translated text, and
identical budget accounting.

The budget assertion compares the two paths rather than asserting zero. This
translator leaves a small residue at stream end on the spaced path too (51
bytes for this fixture), so zero would be asserting something that was never
true. Equality is the contract the offset arithmetic must satisfy.

Confirmed to fail when the offset fix is reverted.
Add model-scoped reasoning-summary capability defaults for the registry-backed DeepSeek V4 and evidence-backed GLM models so Codex keeps sending the selected reasoning object.

Merge the registry map per key, with defaults spread first and user configuration second. This preserves an explicit false for one model without suppressing defaults for every other model, and creates a detached object instead of aliasing registry metadata.

Cover the catalog contract, partial override behavior, conservative no-opt-in default, and red-green production ablation for #1100.
On a machine where icacls is slow — Defender real-time scanning, a roaming
profile, a domain-controller round trip — a complete ACL sequence could not
finish inside the 5-second envelope. The harden failed closed, the native-main
owner published a permanent `unavailable`, and every native request returned
503 until the user restarted.

One correction to the issue's framing: PR #1135's retry is not the problem.
Owner-level recovery calls hardenSecret again and does receive a fresh
deadline. The defect is that one complete sequence — `/grant:r`,
`/inheritance:r`, `/remove:g`, plus the conditional `/findsid` verification —
had only five seconds for all of it.

Raises the default to 30s and keeps everything else: the 60s cap, the
OPENCODEX_ACL_TIMEOUT_MS override, the clamp, and the shared-envelope
structure. Independent per-command budgets were rejected: with /findsid
fallbacks they multiply into the multi-minute startup stall the shared budget
was introduced to prevent.

The cost is stated in the source comment rather than hidden. Because
loadConfig hardens three paths sequentially, the timeout-path worst case at
load becomes ~90s, and the owner path ~60.25s. Both need icacls to be
pathologically slow on every call; a healthy machine finishes in milliseconds.
A slow start is recoverable, a permanent 503 is not, and the failure stays
fail-closed either way.

Four existing tests depended on the 5s default while actually asserting
something else — envelope sharing, fresh-budget-on-second-call, recovery
cardinality. Each now pins OPENCODEX_ACL_TIMEOUT_MS explicitly so it tests its
real subject, and beforeEach/afterEach isolate the variable so a stray value in
a developer's environment cannot change what any of them assert.

The new test deliberately does not pin: it exercises the shipped default with
13s of slow-but-successful work. Confirmed to fail with the default reverted
to 5s.
…ff disk (#1100)

Audit found the first commit fixed the canonical provider ids but missed the
shape the issue was actually reported against.

REACHING CUSTOM PROVIDERS. `enrichProviderFromRegistry` matches on the provider
NAME. The reporter's row is a hand-added provider literally called "GLM"
pointing at a vendor endpoint we recognize. Routing worked, so the row looked
healthy, but no registry id is called "GLM" — so every piece of registry
metadata was skipped, the effort ladder was advertised with summaries left
false, and Codex dropped the inbound reasoning object. Exactly the bug, on the
exact configuration that was reported.

On the name-lookup miss we now fall back to
`registryEntryForProviderDestination`, which already answers "which vendor
endpoint is this row talking to" and is restricted to fixed key destinations —
no templated or overridable base URL can be claimed by it. Scope is deliberately
one field: a custom row keeps its own identity for everything else.

PER-KEY, EVERYWHERE. The fallback first bailed whenever the user had any map,
which recreated the whole-record bug the per-key merge exists to prevent: one
model's flag would have suppressed every sibling default. Both paths now share
`applyReasoningSummaryDefaults`, so an explicit value — including `false` —
wins for its own key and nothing else.

NOT PERSISTED. `enrichProviderFromCatalog` feeds a config about to be written
to disk. Writing today's registry defaults there would freeze them as the
user's own overrides, so a later correction — learning a model's backend
rejects summary delivery — would never reach anyone who created their provider
first, and they would keep getting 400s with no way to know why. It now
restores exactly what the caller submitted. Catalog gathering enriches a
detached runtime clone, so the defaults still apply where they matter.

KNOWN REMAINING GAP: the reporter's endpoint, open.bigmodel.cn/api/coding/paas/v4,
is in no registry entry — only /api/paas/v4 is. Closing that route needs a new
registry entry with its own id (glm/glm-cn are bound in FREE_PROVIDER_DIRECTORY),
its own evidence-backed model set, and registry-parity updates. That is a
provider addition, not a bug fix, so it stays out of this stack and is recorded
in the plan unit instead.

Each new test was confirmed to fail with its production change reverted.
…el coding endpoint

The first implementation passed its tests and was still wrong about the
reported configuration: enrichment matches on provider NAME, and the reporter's
row is a hand-added provider called "GLM". Recording the failure mode because
it generalizes — canonical-id tests were green against a configuration no user
had.

Also records why the BigModel coding endpoint is deferred rather than fixed
here, with the safety analysis for adding it later.
…was actually reported on

The destination fallback added here matched two GLM routes: Z.AI's coding
plan and BigModel's pay-as-you-go `/api/paas/v4`. The configuration in the
issue is neither. It is `https://open.bigmodel.cn/api/coding/paas/v4` — a
third endpoint with no registry row — so the lookup found nothing,
`modelSupportsReasoningSummaries` stayed unset, and Codex kept dropping the
reasoning object. Effort still displayed as `-`.

The test hid this. It was captioned "the reporter's actual shape" and used
provider name "GLM" and model glm-5.2, both correct, but substituted Z.AI's
host. It passed against a route that already worked while the reported one
stayed broken. Found in pre-merge review, not by the suite.

A prefix match on `open.bigmodel.cn` would have covered both endpoints in
one line. It is also how a config pointed at one vendor route inherits
another route's metadata, which is the failure the exact-endpoint rule
exists to prevent — so this is a separate row.

Two details that are not arbitrary. The id is not `glm-cn`, which the
free-provider directory already binds to this same path; registering it
here would let routedProviderConfig() canonicalize a saved `glm-cn` config
onto our baseUrl. And the model list follows Z.AI's coding-plan set rather
than the pay-as-you-go one, because this endpoint is the subscription
product and glm-5.2 only exists on that side.

Ablation: pointing the new row's baseUrl elsewhere makes the reproduction
test red.

Refs #1100
ocx serves gui/dist, which is generated and gitignored. A fast-forward
advances gui/src while dist stays at whatever was last built, so the
dashboard keeps rendering the OLD bundle and nothing in git status hints
at why. That cost a real debugging session today: the symlink and the
source were both correct while the served bundle was seven hours stale,
still drawing sidebar rows the merge had deleted.

post-merge runs build:gui only when the merge range touched gui/, using
the same slash-guarded match as lint-gui-if-changed so all three agree
on what counts. No usable ORIG_HEAD means skip rather than rebuild — a
hook that taxes every unrelated pull gets disabled, and a stale dist is
one command to fix. A failed build never fails the merge; it prints the
command to run.

setup-hooks now installs each hook independently. The old single-hook
early return would have stopped post-merge from installing whenever
pre-push was already current.
The #875 bounded-JSON force (modelResponsesUpstreamStreaming) delayed every
byte of a deepseek-v4-flash turn until generation finished — 28-46 s of
silence on long turns, which users read as a hang. The official Responses
guide documents a response.completed/incomplete/failed terminal with no
data: [DONE] sentinel, live probes (including the tool-result replay shape
behind the original stall) close on the terminal, and the relay's
terminal-output boundary already cuts the stream there and synthesizes
[DONE]. Drop the deepseek registry opt-in; keep the mechanism suite-reachable
through a synthetic-registry fixture, add streamed #938 id-repair
integration coverage, and record the supersession in the #1065 RCA.

Unit: devlog/_plan/260807_deepseek_responses_streaming/
#938)

Live streaming re-exposed a leak the bounded-JSON era masked: DeepSeek wraps
streamed reasoning text in content parts, and content_part.* events were mapped
to the message id table only, so their item_id kept the raw upstream UUID while
the parent reasoning item was repaired. A live tool-call probe showed 13 leaked
UUID item_ids per turn. rewriteItemIdField now falls back to the sibling table —
an output_index identifies exactly one item — and the streamed #938 regression
test pins a content_part frame riding a reasoning item.

Live re-probe: 0 UUID leaks; function_call ids remain untouched.
…g the product code

The serialization test excluded two pre-approval race outcomes and treated
everything else as a seam failure. A macOS CI run produced a third:
`SQLiteError: database is locked` on stderr, which failed the build.

That contradicts the runtime. `configGenerationFailureReason` classifies
that exact message as "busy" rather than a database fault, and the storage
and history paths do the same. So the product already treats it as ordinary
contention while this test called it a defect.

Found by a red CI run rather than by the suite, which is the part worth
noting: the test enumerated the races it had seen instead of the races the
code recognizes, so a third one was a matter of timing rather than of
whether it could happen.

The two-process seam itself is unchanged and still fails on a genuine
convergence error.
…replacement, round 2)

Audit found four defects in the first cut, two of which would have shipped a
feature worse than the bug it fixes.

BUDGET EXHAUSTION IS NOT FAILURE. `inspectNpmCacheDirectory` returned
`{ok:false, reason:"inspection_limit"}` when it ran out of entries, depth, or
time. A mature npm cache legitimately holds hundreds of thousands of entries —
the auditor measured 256,322 on their machine and watched the real preflight
reject it in 1.13s. Every one of those users would have been locked out of
updating. "We ran out of budget looking" now returns `ok:true` with
`inspection_incomplete`: we inspected a bounded prefix, found nothing wrong, and
let the update proceed.

NESTED SYMLINKS ARE SKIPPED BEFORE THE OWNERSHIP CHECK. npm creates symlinks
constantly below `_npx`, `node_modules`, and `.bin`. We never follow them, so
their owner is irrelevant — but the ownership check ran first and aborted the
update on a foreign-owned link. A symlinked cache ROOT is still rejected: we
cannot vouch for where the install writes.

SANITIZATION SURVIVES WRAPPED PATHS. npm and the OS wrap long paths, and the
line-bound regexes let `C:\Users\<newline>Jane Doe\...` through with the
username intact. Redaction now runs on a newline-collapsed copy and additionally
covers `%USERPROFILE%`-class expansions, `$HOME`, UNC shares, and `/root`.

GATE ORDERING IS TESTED BY BEHAVIOR. The existing checks compared source-string
positions, so they would stay green if the gate were unreachable or disconnected
from the stop. `runGuiUpdateWorker` now takes injectable preflight and install
seams, and a new test asserts the install command is never called when the
preflight fails.

The symlink test also needed a real seam: `!stat.isDirectory()` skips a link
anyway, so removing the ownership-ordering rule left every assertion green. An
injected `uidOf` binds the assertion to ownership specifically. Each of the four
fixes was confirmed to fail its test when reverted.
…t wrapped paths (round 3)

Audit round 2 found the previous commit's headline fix was inert.

THE PROTOCOL REJECTED ITS OWN SUCCESS. `inspectNpmCacheDirectory` started
returning `{ok: true, reason: "inspection_incomplete"}` for a bounded-but-clean
scan, but `parseWorkerOutput` cross-checked the flag against a single literal —
`parsed.ok !== (parsed.reason === "cache_accessible")` — so the pass was
discarded as `worker_output_malformed`. Every large cache still failed, now with
a misleading reason. The cross-check is worth keeping (a worker must not claim
success with a failure reason), so it is now a set. Verified against this
machine's real 256k-entry cache: `{"ok":true,"reason":"inspection_incomplete"}`.
`inspection_limit` became unreachable and is removed.

WRAPPED PATHS STILL LEAKED. Rejoining wrapped lines was the wrong shape: joining
aggressively enough to catch a wrap inside the username also merged genuinely
separate log entries, and joining conservatively enough to keep them apart let
`C:\Us<newline>ers\Zoe [Admin]+` through. Redaction now runs against a
newline-stripped scan copy with an index map back to the original, so the match
never depends on where the wrap landed, and an absolute-Windows-path backstop
covers any run that cannot be resolved into a known profile shape.

THE GATE TEST NEVER REACHED THE GATE. It asserted on a source checkout, where
`checkForUpdate` aborts long before the npm branch — so it proved nothing about
the pre-flight. `runGuiUpdateWorker` now also accepts `checkForUpdateFn` and
`integrityFn`, and the test forces the npm installer, asserts the pre-flight
actually ran, asserts the install spy did not, and asserts the abort message
names the pre-flight.

Two source-position tests were updated to match the new seam strings. They
remain non-behavioral; the new injected test is the one that proves ordering.

Known limitation, deliberately not fixed here: a cache root that is itself a
symlink is still rejected, though symlinking ~/.npm to another volume is
legitimate. Resolving the root target safely is a separate change.
…linked cache root (round 4)

INDENTED CONTINUATIONS STILL LEAKED. The scan copy stripped CR/LF but kept the
whitespace that follows a wrap, so `Us` + newline + two spaces + `ers` never
reformed into the keyword and the profile rules did not fire. Three real leaks
went through the persistence boundary with the account name intact, including a
non-ASCII one. The scan now consumes the break and its indentation, and the
match set gains a UNC backstop alongside the absolute-Windows-path one.

Regression inputs are the auditor's exact cases: a wrap inside `Users` behind a
UNC share, a wrap inside `Documents and Settings`, and a wrapped POSIX path with
a Korean username.

A SYMLINKED CACHE ROOT IS NO LONGER REJECTED. Pointing ~/.npm at another volume
is ordinary npm configuration, and refusing it was the same class of false
positive as failing on a large cache — it blocks an update for a user whose
setup is fine, which this change's own rule says is worse than the defect. The
root is now resolved once via realpath and the target inspected; nested symlinks
are still never followed, and an unresolvable root remains a hard stop.

Both fixes confirmed to fail their tests when reverted.
… it (round 5)

The scan copy removed every line boundary, so the `[^\r\n]*` backstops ran to
the end of the text: one redacted path consumed every following log entry.
Privacy was intact; the diagnostics were destroyed. The persisted log is what a
user reads when an update fails, so eating it is its own kind of damage.

Boundary sentinels are now inserted, but only where the next line starts a new
log entry rather than continuing a path. Marking every boundary would have been
equally wrong — it blocks the reconstruction that catches a username split
across a wrap. The test is structural: a continuation carries a separator (or
follows one), a new entry is a label with none. That distinction is what lets
`Mary Jane van der Berg\Documents\...` still reconstruct while
`KEEP diagnostic code E42` survives untouched.

Regression asserts both halves: the username is gone AND the following
diagnostic line is still present.
… continuations (round 6)

The separator heuristic could not work, and the audit proved it with two inputs
that fail in opposite directions:

  C:\Users\Z / "  oë [Admin]+"        continuation with NO separator -> leaked
  ...\Users\Jane\x / "npm ERR! /usr/…" new record WITH a separator    -> swallowed

Nothing in the text distinguishes those two cases, so any rule keyed on
separators trades one failure for the other. Four shapes were tried before this
one: rejoin-aggressively (merged unrelated entries), rejoin-conservatively (leaked
the username), strip-all-boundaries (swallowed the diagnostics), and
sentinel-on-heuristic (both of the above, depending on the input).

The redaction is now line-aware with one carry bit. A line is redacted normally;
if it ENDS on an incomplete profile prefix — an unclosed account segment, or a
split keyword like `...\Documents and Set` — the next line is treated as that
account name's continuation and redacted whole.

This is deliberately asymmetric. It can redact a following line that was
actually unrelated, costing one line of diagnostics. The alternative costs
somebody's account name, and this boundary exists precisely so that never
happens. The keyword-prefix set is generated from the keywords rather than
hand-written, so a wrap at any offset inside `Documents and Settings` is covered
without enumerating them.

Regressions now assert both directions: the username is gone, and an unrelated
following record — with a separator in it — survives.
Six rounds of redaction, six new leaks. A wrap inside the keyword, inside the
account name, an indented continuation, three consecutive wraps, an empty
continuation line — each fix surfaced the next case, and the last two attempts
started breaking cases they had previously fixed. That is not a tuning problem.
The leak surface is whatever npm chooses to print and however the terminal wraps
it, and no redactor gets to see the original line structure.

Taking the auditor's second recommendation:

- `runLoggedCommand` no longer persists stdout/stderr. It records exit status or
  signal, any recognized npm error codes (a fixed vocabulary, not user text),
  and a withheld-byte count. Detailed output stays ephemeral.
- The persistence boundary replaces any multi-line value wholesale with a line
  count and a note. Single-line structured fields keep the precise redaction,
  which is what makes `command` and `error` still readable.

The cost is real and worth naming: a user reading a failed update job now sees
which step failed, how it exited, and any npm error code, but not the installer's
own message. That is a genuine diagnostic loss. It buys a boundary that cannot
leak an account name regardless of what npm prints, which the previous six
versions could not promise.

The auditor's five leaking inputs are kept as regressions. They now pass
structurally rather than by pattern-matching.
…ingle-line path leaks (round 8)

Three more findings, all real.

THE "FIXED VOCABULARY" WAS A SHAPE PATTERN. `E[A-Z]{3,}` matches `ERROR`, so
`npm ERR! path C:\Users\ERROR\.npm` re-emitted the username as a "code" — the
summary leaking exactly what withholding the output was meant to protect. It is
now an explicit Set of recognized npm/libc codes, extracted only from npm's
canonical `code <CODE>` position rather than scanned out of free text.

SINGLE-LINE PATHS STILL LEAKED. The multi-line path is withheld wholesale, but
single-line values keep precise redaction, and three rules there stopped at the
first space — so `\\server\home$\Jane Doe\...` and `D:\Profiles\Mary Jane\...`
kept the surname. Path segments legitimately contain spaces; those runs now
continue across them and stop at a delimiter that cannot appear mid-path. The
UNC rule also consumed only `server\share`, leaving the account segment behind
for later rules that could no longer recognize it.

BYTE COUNT WAS A CODE-UNIT COUNT. `Buffer.byteLength(..., "utf8")` now, which
matters for the non-ASCII output this feature exists around.
… does not (round 9)

Eight rounds of redaction failed in both directions at once, and the audit
proved it with one input each:

  D:\Profiles\Mary O'Connor\...   leaked — an apostrophe was a terminator
  installed at C:\x and then ...  over-redacted — a path run has no reliable end

Both come from the same mistake: guessing which characters belong to a path in
text we did not produce. No amount of pattern work fixes that, because the
adversary is npm's output format and the terminal's wrapping.

The boundary now asks a question it can answer — is this value KNOWN safe? —
and withholds everything else. Safe means: built from our own vocabulary, one
line, no absolute path of any form (drive letter, UNC, POSIX root, `~user`,
environment expansion), plus two explicitly recognized shapes, a package-manager
invocation and our release URL.

Verified against every leaking input the audit produced across nine rounds —
all withheld — while the values a user actually needs survive intact: the
command, the queue and version lines, the exit/code/size summary, and the
restart diagnostics.

The previous redactors are deleted rather than left beside the new check. Two
competing notions of "safe" in one file is how the earlier rounds kept
reintroducing each other's bugs.
…held error text (round 10)

The previous "allow-list" defaulted to `return true`, which makes it a denylist
wearing an allowlist's name — and the audit walked straight through the
exception carved out for our own endpoint: `probe /healthz?path=/Users/Jane-Doe`
passed.

Three changes, following the audit's provenance recommendation:

FIELD-SCOPED. Only `command`, `error`, `log`, and `releaseNotesUrl` go through
the check. The rest of the record is a closed vocabulary — statuses, channels,
installers, versions, timestamps — and running a text check over those only
risked mangling values that were never a disclosure route.

RENDERED, NOT FILTERED. `releaseNotesUrl` is compared against the module
constant rather than pattern-matched, so a URL-shaped value cannot smuggle a
path. `command` is rebuilt from a recognized shape: a known tool, fixed
subcommands and flags, our own package spec, and `<path>` placeholders for
absolute arguments. Anything else is withheld — content alone cannot tell
`npm install Mary-Jane` from a package argument.

ERROR TEXT IS DESCRIBED, NOT COPIED. Every site that interpolated an
`Error.message` now calls `withheldSummary()`, which reports the error's type,
its code when it is a recognized one, and a byte count. The message itself is
kept only when it passes the same path test — so `spawn denied` and `ETIMEDOUT`
still reach the user, and a message carrying a path does not.

Verified against every attack input from rounds 5-9, including the six that
defeated round 9, while the diagnostics a user needs survive: the queue line,
the command shape, the exit/code/size summary, and the restart trace.
… provenance code (round 11)

The audit caught something worse than a bug: I built a provenance mechanism and
never wired it up. `ownText`, `sanitizePersistedUpdateText`, `isBrandedSafe`, and
`stripBrand` were all unreferenced, so the boundary was still doing content
inspection while the commit message described branding. Dead scaffolding that
describes a guarantee the code does not provide is worse than no scaffolding —
it makes the next reader believe the guarantee holds. All of it is deleted.

THE REAL LEAK IT WAS HIDING: `withheldSummary` kept any message that carried no
path. That sounds reasonable and is wrong — `spawn denied for Jane Doe` has no
path in it and still names a person. There is no test on message CONTENT that
separates a diagnostic from an identity, so message text no longer crosses the
boundary at all. The record keeps the error's type, a recognized code, and a
byte count.

Also closed:
- A raw `err.message` catch in the GUI worker that never went through any check.
- `error.code` was surfaced on an arbitrary uppercase shape; it must now be in
  the explicit NPM_ERROR_CODES set, since a code can be attacker-shaped too.
- The npm code extractor is anchored to a complete canonical line
  (`^npm ERR! code <CODE>$`, multiline) rather than matching `code` anywhere in
  free text — closing `npm ERR! path code EACCES\private`.

Cost, stated plainly: a user no longer sees npm's own error text. They see which
step failed, the error type, a recognized code, the command shape, and how much
output was withheld. Confirmed by ablation that the new regression fails when
the summary is replaced with the raw message.
…on at entry (round 12)

Two more channels, both external text reaching disk through a field that looked
like ours.

`Error.name` IS EXTERNAL. It is writable, so `error.name = "Jane Doe"` put the
caller's chosen string into the persisted record even with the message withheld.
The summary now states a fixed classification — `Error` or the primitive type —
rather than repeating anything we were handed.

`/healthz` VERSION IS EXTERNAL. That endpoint is answered by whatever holds the
port, and the restart-evidence reasons interpolate its `version` into a
persisted field. A responder returning `{version: "Jane Doe"}` persisted it. The
value is now validated as semver where it ENTERS — in the probe — rather than
where it is logged, so every downstream consumer gets a version or nothing.

Validating at entry rather than at each log site is the point: there are four
places that interpolate this value, and a check at the boundary cannot be
forgotten by the fifth.
The design changed twice under audit. The first proposal was an opt-in
switch that would have admitted loopback socket peers on a remote bind;
it rode resolveApiAuth into eight endpoints unrelated to #1102, and a
public listener's peer address only proves the last transport hop, which
Docker Desktop, host-network containers, WSL mirrored networking and
tunnels all terminate locally.

The shipped design leaves public admission untouched and opens a
separate 127.0.0.1-bound listener, so the kernel refuses remote
connections instead of us judging addresses.

Later rounds closed the implementation contracts: a fixed port (an
ephemeral one would manufacture the restart breakage the issue claimed
and we disproved), a per-request auth/CORS policy view narrow enough
that it cannot masquerade as business config, one startup transaction
across both binds, composite stop that completes cleanup AND propagates
failure, and GET /v1/models on the allowlist because Codex falls back to
it when no catalog is installed.

The last blocker was the sharpest: the /v1/models ablation would not
have gone red, because the models-manager catches refresh failures and
returns its bundled list. The acceptance test now turns on a
runtime-generated unique routed model that no bundled catalog can
synthesize.
…ound 13)

Shape validation was not enough: `2.7.41-JaneDoe` is valid semver, so the
mismatch reason echoed it straight into a persisted field. `/healthz` is
answered by whatever holds the port, which makes its version external input no
matter how well-formed it looks.

Mismatch reasons now state THAT the reported version did not match and name only
the version we expected — which is ours. On a match the reported value equals
the expectation by definition, so the trusted one is rendered instead.

This closes the last channel the audit's persistence inventory found. Regression
drives the hostile value from ingress through to the evidence reason and asserts
the name is absent while our own version still appears.
…pawn Codex (#1102)

A `codex app-server` launched by a host that resolves the entrypoint
directly never passes through the generated shim, so it never inherits
OPENCODEX_API_AUTH_TOKEN. On a wildcard bind every model call then 401s
at admission, before any SSE frame.

The tempting fix — exempt callers whose socket peer looks like loopback
— is unsound. `requestIP()` proves only the last transport hop, and
Docker Desktop port forwarding, host-network containers, WSL mirrored
networking and tunnel terminators all re-open remote connections
locally. It would also have ridden `resolveApiAuth` into eight
endpoints unrelated to this issue.

Instead a second listener binds 127.0.0.1. The kernel refuses remote
connections outright, so there is no address to judge, and the public
listener's admission policy is byte-for-byte unchanged.

The parts that are load-bearing:

Auth and CORS read a `RequestPolicyView` — a Pick of hostname,
corsAllowOrigins and apiKeys — chosen per request from the receiving
listener. Rewriting the whole config and holding it would go stale on
the next management change; adding an `allowUnauthenticated` parameter
to the resolvers would put an admission bypass on the wrong side of the
boundary. The narrow type also means a policy view that leaks into a
routing path fails to typecheck.

The bind is loopback but the boundary is not the bind alone: an
attacker page can make a victim's browser connect to 127.0.0.1. The
listener therefore takes the same Host/Origin branch a plain loopback
bind always has. A test asserts the same hostile Origin that the
loopback policy rejects would be accepted under the public policy.

The port is required in config, never OS-assigned. An ephemeral port
would change across restarts while running app-servers kept the old
base_url — the exact symptom this issue reported for token rotation,
which does not actually happen.

`GET /v1/models` is on the four-route allowlist because when catalog
materialization fails, `syncCodex` injects with `catalogPath: null` and
Codex falls back to an online model manager that refreshes through it.

Both binds are one startup transaction, and composite stop completes
cleanup on both listeners while still propagating failure — swallowing
it would let drainAndShutdown report success while a socket is held.

Off by default. When on, every local process can spend account quota
and paid provider credentials, which the startup warning and the docs
say plainly.

Ablation: reverting the policy view to the shared config makes 3 tests
red including the hostile-Host case; dropping the auth-header rule
makes 1 red; removing the port validator makes 2 red; rewriting the
public bind to a literal 127.0.0.1 makes the F4 symmetry guard red.

Refs #1102
… 14)

Withholding the whole stream was too blunt. A user whose update fails deserves
to know why, and `exit 1 · 359 bytes withheld` tells them nothing.

The insight I missed for thirteen rounds: npm's failure output is STRUCTURED,
not prose. It prints `npm error <field> <value>`, one field per line. That means
the useful parts can be read BY NAME instead of reconstructed from text — which
is what made every redaction attempt fail, since it had to guess where a path
started and ended.

Kept fields, each because its value cannot be a local path: `code`, `syscall`,
`errno`, `notarget`, and the HTTP-status lines (`404`, `401`, `403`, `409`,
`429`) whose value is a registry URL. Explicitly not kept: `path`, `dest`,
`file`, `stack`, the bare `Error:` line, and the debug-log location — every one
of those is a filesystem path by definition.

Each kept value still passes the path test before use, is length-capped, and
`code` must additionally be in the recognized vocabulary. Convention is not a
guarantee.

Node exceptions get the same treatment: `syscall` and `errno` are named
properties, shape-validated (a short lowercase identifier, an integer), so an
error summary now reads `Error EACCES · syscall: mkdir · errno: -13` instead of
a byte count.

Measured against real npm failures:

  before  exit 1 · 366 bytes withheld
  after   exit 1 · code: E404 · 404: The requested resource '…' could not be found

  before  exit 1 · 359 bytes withheld
  after   exit 1 · code: EACCES · syscall: mkdir · errno: -13

  before  exit 1 · 208 bytes withheld
  after   exit 1 · code: ETARGET · notarget: No matching version found for left-pad@99.99.99

The regression drives a real EACCES dump containing `/Users/Jane Doe/...` and
asserts the cause survives while the account name and paths do not. Ablation
confirmed.
estelledc and others added 25 commits August 8, 2026 02:09
Allowlisting the field name and leaving its value free-form just moved the leak
one level in. `npm error syscall janedoe` was kept verbatim, because `syscall`
was a recognized field and nothing ever checked what followed it.

Every kept field is now RENDERED from a validated value:

  code      must be in the recognized npm/libc vocabulary
  syscall   must be in an explicit POSIX syscall set — a shape check accepts `janedoe`
  errno     must parse as an integer, and is re-rendered from the parsed number
  notarget  reduced to `no matching version for <pkg>@<version>`, or the bare fact;
            the surrounding prose is never borrowed
  HTTP 4xx  parsed as a URL, rendering only the registry HOST — the path can name a
            private scope and userinfo is a credential

Node exceptions use the same syscall vocabulary rather than the shape check.

This also fixes a bug the audit found in the previous round: the HTTP diagnostic
never actually worked, because the raw line contains `https:/` and the path test
read that as a drive letter. Parsing the URL fixes the false positive and the
disclosure risk in one move.

Verified against every forged input the audit produced — `syscall janedoe`,
`errno JaneDoe`, `notarget ... Jane Doe`, mixed-case `NpM ErRoR`, a private
scope in the URL path, and userinfo — while the real failures stay legible:

  exit 1 · code: EACCES · syscall: mkdir · errno: -13
  exit 1 · code: E404 · 404: HTTP 404 from registry.npmjs.org
  exit 1 · code: ETARGET · notarget: no matching version for left-pad@99.99.99

Ablation confirmed: restoring the shape check fails the new test.
…ch the real inject wiring

Two of the previous tests were watching nothing, and the audit was right
about both.

The allowlist check probed POST routes with GET. Chat Completions,
Messages, Images, search and Live all 404 on method mismatch inside
their handlers, so widening the allowlist to admit one of them would
have kept the assertion green. Each route is now requested with the
method its handler accepts, plus two cases proving the allowlisted paths
still reject methods they do not serve. Ablation: adding /v1/messages to
the allowlist now fails it.

The injection test handed the loopback port straight to
buildProviderTableBlock, so deleting the substitution inside
injectCodexConfig changed nothing it observed. It now runs the real
injector in a subprocess — CODEX_CONFIG_PATH resolves at module load, so
an in-process CODEX_HOME would write somewhere else — passes the PUBLIC
port the way every caller does, and reads the written config.toml.
Ablation: removing the substitution fails it.

Also added: POST /v1/responses and /v1/responses/compact admitted on the
loopback listener and 401 on the public one, asserted as neither 401 nor
404 because "not 401" alone would survive removing the route; a
Responses WebSocket handshake on both listeners; and a rollback test
that binds a fixed public port and rebinds it after the throw, since
throwing while leaving the public listener up is the failure the
rollback exists to prevent.

Two honest gaps are recorded rather than papered over. The WebSocket
test cannot defend requestServer.upgrade over server.upgrade: that
ablation stayed green because this Bun version accepts an upgrade issued
from a sibling Bun.serve. And composite stop's failure propagation has
no test, because the composite captures the underlying stop at
construction and there is no seam to inject a rejection through; its
sibling property, cleanup across both listeners, is covered.

Port selection fixes from the same review: the ephemeral redraw is now a
bounded loop behind an injectable allocator rather than unbounded async
recursion, and it has tests that actually reach the redraw branch. An
explicit --port collision with the reserved port is rejected before the
60-second reclaim path instead of after it, with a message naming the
real cause. runAdmittedHttpTurn now takes the policy explicitly, so no
CORS-emitting helper falls back to the shared config.

Refs #1102
…(round 16)

Two things I treated as "safe shapes" that were not.

A PACKAGE SPEC IS NOT A SAFE SHAPE. `name@version` also matches
`jane.doe@example.com` and `JaneDoe@2.7.41`, so extracting "the spec" from a
`notarget` line was itself a disclosure channel. This updater resolves exactly
one package, so a spec is echoed only when it IS ours; everything else reports
the bare fact.

A HOSTNAME IS NOT A SAFE SHAPE. `^[\w.-]+$` accepts `janedoe.example`, a numeric
host, and a punycode host. Knowing whether a 404 came from the public registry
or somewhere else is the useful part, and that fits in an allowlist — four known
registry hosts. Anything else reports the status alone.

Also fixes the spec matcher itself: `PKG` is scoped (`@bitkyc08/opencodex`), so
it needed escaping and a boundary that works with a leading `@` — `\b` does not.
Verified our own spec is kept while an email is not.

Every attack input from this round is a regression test.
Adopted from PR #1159 by @jonathanli12, rebuilt on the current stack. Original
closed in favor of this commit.

Discovery normalizes Cursor's optional `cursor-` prefix so catalog matching can
compare canonical ids. Requests then inherited that prefix-free form, so regular
Grok 4.5 went out as `grok-4.5-{tier}` when Cursor's live discovery advertises
`cursor-grok-4.5-{tier}`.

The fix keeps the two paths separate rather than changing normalization:
`cursorRequestWireModelIdWithEffort` composes the request-side id and leaves
`cursorWireModelIdWithEffort` alone for discovery. Touching normalization would
have fixed the request and broken catalog matching in the same edit.

Grok Fast is untouched: it keeps the canonical `grok-4.5` model id with
`effort` and `fast=true` as separate parameters.

Scope note: only regular Grok 4.5 gets the prefix. This does not change
Claude-family ids, and it does not address #1162 (Cursor Claude-family
resource_exhausted), which has no code-level cause identified yet.

Both tests confirmed to fail with the request-builder change reverted.
Xiaomi MiMo's paid endpoint answers the Responses wire for plain turns, so a
user configuring it by hand picks `openai-responses` — MiMo documents Responses
support. But its gateway rejects `type: "custom"` tools with
`400 responses_feature_not_supported`, and `apply_patch` is a custom tool. The
result is a provider where chat works and every agentic turn fails.

Only `xiaomi` (Anthropic wire) and `mimo-free` (free tier, own adapter) existed,
so token-plan users had no preset to start from. This adds one pinned to
`openai-chat`, which is the wire the reporter confirmed works end to end. The
Chat path already lowers custom tools to `{input: string}` functions and
restores them as `custom_tool_call`, so the capability survives intact.

Stripping the custom tools instead would stop the 400 and disable the Codex
agent loop — a provider that no longer errors and no longer edits files.

Reasoning tiers above `high` are clamped: the gateway validates the ladder
strictly and rejects anything higher.

`preserveCustomDestination` is set because a user may already have a hand-rolled
provider under this id. Without it, routing canonicalizes their base URL onto
ours and sends their key to a host they never chose — the hazard the
`zhipu-bigmodel` comment documents.

That last property is the one worth testing rather than asserting: the preset
shape assertions do not exercise it at all, since the mechanism only engages
when endpoint, adapter, or auth differ. A routing-level regression covers it,
and both it and the adapter pin were confirmed to fail when reverted.
Resolve the current token SID instead of trusting USERDOMAIN and USERNAME on workgroup hosts. Keep identity lookup failures separate from icacls timeouts.\n\nRefs #1149
…two blind seams

The shutdown orchestration held two properties that pull against each
other — keep cleaning up after a failure, yet still report it — and
neither was testable in place, because the composite captures the
underlying stop at construction. Extracted to `runListenerShutdown`,
which now has four cases including both failure shapes. Ablation:
removing the per-step catch fails 2, swallowing the collected failures
fails 3.

Two seams have no runtime oracle on this Bun version, and both would
regress silently. Swapping `requestServer.upgrade` for `server.upgrade`
stays green because this Bun accepts an upgrade issued from a sibling
Bun.serve; another version is not promised to, and the loopback listener
would then fail to upgrade at all. And the connection-refused test
degrades to a warning on a host with no external IPv4, so the 0.0.0.0
ablation would pass there. Source assertions are a weak instrument, but
one aimed at a known blind spot beats a comment nobody runs — and the
upgrade assertion does go red on that swap.

Smaller review fixes: the WebSocket handshake helper now clears its
timer and settles once, so a late timeout cannot fire into the next
test; and the rollback test draws its public port with the loopback port
reserved, since two back-to-back freePort() calls can return the same
port and the test would squat itself.

Refs #1102
…reverting concurrent changes

Adopts #1203 by @estelledc — the first three commits are theirs, cherry-picked
with authorship intact. The approach was right: expose the existing
`providers.<id>.contextWindow` / `modelContextWindows` contract at the
management and UI layers without touching catalog derivation, which already
materializes those values when upstream metadata is absent.

Four corrections, all found by independent audit.

**Only the selected model was saved.** The drafts map held edits for every
model but the PATCH was keyed on `contextModelId`, so a value typed into
model A and then abandoned by switching to B vanished — no error, no warning.
The PR's own test pinned that as correct. It now sends every model the user
typed into.

**But "every model that differs" would have been wrong the other way.** The
10s poll can refresh a field while the modal is open; diffing drafts against
live state would then call an untouched field dirty and revert someone else's
change. Two conditions are required: the user touched it, AND the value
differs from what the modal opened with. Both apply to the provider default
too, which was previously sent unconditionally and would stamp a stale number
over a concurrent update.

The snapshot holds canonical numbers, not the raw text. Retyping 64000 as
"64,000" is not an edit, and treating it as one would resurrect the same
stale-write. When nothing survives the comparison, no PATCH is sent at all
and the feedback says so rather than claiming an update.

**`Number.isInteger(1e100)` is true.** Both the management validator and the
form accepted it; it would persist and serialize into the catalog as an
enormous number that can make Codex reject the file. Both now require a safe
integer. The default is only validated when touched, so a value inherited
from a hand-edited config cannot block an unrelated per-model save.

**An override for a model that left live discovery was unreachable.** It sat
in the drafts map, absent from the picker, impossible to inspect or clear.

Tests: the exact #1073 reproduction is split in two, because a single case
setting `modelContextWindows` keeps passing with the provider-wide fallback
deleted. Ablations were driven red in their real defect shape rather than as
artificially strong mutants — notably, comparing against live `groups` while
keeping the touched guard is only visible when a field is edited, reverted,
and changed server-side, which the suite now covers.

Translated provider docs (ko/ja/ru/zh-cn) described both fields as caps only,
which reads as the opposite of the fix for non-English users.

Co-authored-by: zhouxun <zhouxun.13@bytedance.com>
Closes #1073
Pinning the package NAME to our own still left the VERSION free:
`@bitkyc08/opencodex@99.99.99-JaneDoe` is a valid-looking spec, and a semver
prerelease identifier can encode anything. That is the same lesson the
`/healthz` version taught in round 13 — I applied it there and not here.

There is no trusted resolved version available at this call site, so the spec is
not rendered at all. `code: ETARGET · notarget: no matching version` already
tells a user their requested version does not exist, which is the diagnostic
that matters.

Both attack inputs are regression tests.
Adopted from PR #1171 by @byongshintv, rebuilt on the current stack. Original
closed in favor of this commit.

An unlimited A6API key reports zero finite credit totals. Finite-total
validation then treated that as a terminal failure and returned before the key
could be represented at all, so a perfectly working key looked dead in the
dashboard.

The unlimited branch now runs ahead of that validation and emits the generic
`customWindows` row the GUI and CLI already consume, preserving expiry. It
accepts `true`, `1`, and `"true"` for the upstream flag.

Two known limitations, stated rather than discovered later: `creditsUsd` and
its expiry are not yet surfaced by the GUI — visibility comes from
`customWindows` — and neither changes existing behavior for finite keys.

Confirmed to fail with the unlimited branch disabled.
… plan

Audit found resolveTrustedWindowsPowerShellExe() already resolves and validates
the executable through GetSystemDirectoryW. Writing a third SystemRoot/PATH
lookup would reintroduce the substitution surface the plan exists to close.
…dable

Builds on the contributor fix by luvs01 (#1180), which replaced the
USERDOMAIN\USERNAME ACL principal with the effective token SID. Three
things that fix left open:

The synthetic principal POSIX CI needs lived in windows-secret-acl.ts and
was chosen before the injected runner. That ordering made a lookup
FAILURE unreachable outside Windows, so the two cases that defend the
fail-closed boundary and the timedOutPaths isolation were guarded with
`if (process.platform !== "win32") return;` and never ran on Linux or
macOS. A test that silently returns on two of three CI platforms is not
coverage of a security boundary. The synthetic value moves to the
resolver as its own seam, runner selection becomes explicit > synthetic
> default, and both guards are gone.

sanitizedAclError re-attaches only allow-listed codes, and EACLIDENTITY
was not among them. A required-mode harden therefore threw with the
cause in the message but `error.code === undefined`, so no caller could
branch on "the SID could not be resolved" versus "icacls stalled". The
existing test matched the message and hid this.

The absence of a name-shaped fallback is now stated as the fix rather
than left as an omission. `DOMAIN\User` has a valid shape, but shape is
not evidence of the token's subject, and both variables are writable by
whatever launched us. runIcacls grants the principal Full Control and
then removes inheritance, so a wrong principal either leaves another
account holding the secret or strands the file with no usable ACE. An
independent audit rejected an earlier draft of this change that restored
that fallback for the optional read path.

Coverage now runs the sync and async paths across required and optional
on every platform, and asserts zero icacls invocations when the
environment names a plausible-looking account.

Ablation: reverting the runner ordering makes identityCalls 0 and the
required harden succeed (2 red); dropping EACLIDENTITY from the
allow-list makes both toMatchObject assertions fail (2 red).

Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>
Closes #1149
The audit named this the ready gate, and it was right to. Every existing
test proves a piece — admission, the route allowlist, CORS, the bind scope,
the injected port — and none of them prove the thing the feature exists
for: that a real `codex app-server`, spawned the way a third-party host
spawns it, reaches the proxy without a credential. That seam is between two
processes.

Not a `bun test` file. The repository does not depend on `@openai/codex`,
so a test that skips when it is absent would report green on machines that
never ran it. This fails loudly and its output is the evidence.

The oracle is a routed model id generated at run time. Codex caches model
lists and falls back to a bundled catalog when a refresh fails, so "did
model/list succeed" proves nothing — a broken path looks identical to a
working one. A name no bundled catalog can contain can only have arrived
through our listener.

Writing it surfaced two things worth recording. `model/list` reads
`model_catalog_json`; it does not call the provider's `/v1/models`, so the
first version watched Codex return its five bundled ids while never
touching the listener — the exact false-negative shape the unique id exists
to expose, pointed at the harness instead of the feature. And a hand-written
catalog fixture is a liability: Codex rejects the whole file on any schema
mismatch and silently falls back, so the catalog is now built with our own
serializer, which also means the script exercises the bytes `ocx sync`
writes.

Result on Codex 0.146.0, isolated CODEX_HOME with no models_cache.json and
OPENCODEX_API_AUTH_TOKEN stripped from the child environment: 9/9, ending
with POST /v1/responses observed on the loopback listener.

Ablation: pointing base_url at a dead port makes the last check red, so the
harness is watching the hop rather than asserting its own setup.

Refs #1102
react-doctor's prefer-module-scope-pure-function, and it is right: the
function closes over nothing, so rebuilding it on every render is wasted
work. The prepush doctor gate rejected the push over it.
…t_index tables

Final-gate audit found the cross-table fallback could hand a reasoning
content_part the MESSAGE canonical id when both tables held the same
output_index, and an out-of-order part event passed through unrepaired.
Track raw upstream id -> canonical id at item registration and rewrite
part/delta events by exact raw-id match first; the index table remains only
for events without a known raw id. A reused index can no longer borrow the
sibling item's id, and function_call part events stay untouched.
…d drop the positional guess

Final-audit round 2 reproduced two defects in the raw-id rewrite: a flat
raw-id map collapsed items sharing one placeholder id into the last item's
canonical id, and the index fallback could still hand a function_call part
event (or an already-canonical id) the sibling message's identity on a
reused index. The map is now keyed by (output_index, rawId), and events
that carry an item_id are rewritten only on an exact key match — an unknown
id is left alone instead of guessed by position. The index table serves
only item_id-less events behind repairMissingTerminalIds, the pre-existing
contract. Regression tests pin all three reproductions.
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 486f6080-76d4-4020-be9d-ba443ffa19d7

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Deterministic PR hygiene checks passed.

@github-actions github-actions Bot changed the title chore: temporary dev delta extraction for #1206 rebase [WRONG BRANCH] chore: temporary dev delta extraction for #1206 rebase Aug 7, 2026
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

⏳ DRAFT

  • wrong target branch (agent/rebase-1206-oldbase-tmp); retarget to dev. PR description needs work (thin).

What to do

  • Retarget this PR to dev — all contributions go to dev.
  • Add a real Summary and Test plan to the PR description.

Its title has been prefixed with [WRONG BRANCH].
This pull request was already a draft. Its draft status will be preserved after every issue above is resolved.

@Wibias Wibias closed this Aug 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants