Skip to content

feat(agent-setup): configure Zed and VS Code - #437

Draft
Menci wants to merge 151 commits into
mainfrom
zed-agent-setup
Draft

feat(agent-setup): configure Zed and VS Code#437
Menci wants to merge 151 commits into
mainfrom
zed-agent-setup

Conversation

@Menci

@Menci Menci commented Aug 7, 2026

Copy link
Copy Markdown
Owner

Adds Zed and VS Code as Agent Setup targets, beside Claude Code and Codex. One
PR because the two share everything but their merge programs: one projection of
the model catalog on the gateway, one set of managed-file primitives per shell,
and one dashboard pane shape.

Neither editor can discover models — Zed's anthropic_compatible provider has
no fetch path, and VS Code's customendpoint drops every model it cannot type —
so the gateway projects the catalog once, in models.ts, and embeds the result
in the script it serves. The dashboard preview reads the same function through a
package export. That projection previously existed three times over, as a jq
program, a PowerShell loop, and a preview builder which had to agree byte for
byte and repeatedly did not.

Zed

Why anthropic_compatible

Zed reaches Floway through its anthropic_compatible provider rather than
openai_compatible. Protocol choice is not the reason — PublicModel.endpoints
is the upstream wire surface, and any of chatCompletions/messages/responses
makes a model reachable from all four inbound chat routes, so every model works
on either provider. The reason is client-side fidelity:

  • Tool schemas. openai_compatible hardcodes JsonSchemaSubset with no
    escape hatch; anthropic_compatible has no override and gets full
    JsonSchema. The subset rejects if/then/$ref outright and silently
    drops format, additionalProperties, and exclusiveMinimum/Maximum.
  • Reasoning. mode: Thinking { budget_tokens } | Adaptive maps 1:1 onto
    chat.reasoning.budget_tokens and chat.reasoning.adaptive, which have no
    home at all in the OpenAI dialect.
  • Errors. The Anthropic path maps 429/529, retry_after, and the
    error.type taxonomy into typed retry semantics; the OpenAI path flattens
    everything at the transport boundary.
  • SSE. The Anthropic reader explicitly skips a stray [DONE] sentinel,
    with a comment naming gateways as the reason.
  • Usage. The OpenAI Chat Completions mapper hardcodes both cache counters to
    0. This matters because Zed does no token counting anywhere — no
    tiktoken, no count_tokens on the trait, no /v1/messages/count_tokens call
    — so streamed usage is its only truth source for the context meter and
    auto-compaction.

parallel_tool_calls is not a loss: on the Chat Completions path a false
omits the field entirely, and Zed never emits disable_parallel_tool_use, so
both surfaces land on the protocol default.

Why global_settings.json

Both installers write global_settings.json, a settings layer Zed reads below
the user's own file and never creates or writes
itself
— added upstream for
"enterprises with automation … without interfering with user's settings files".

Owning that file outright is what keeps this off the user's own settings.json,
which is theirs to shape and full of comments. global_settings.json is read by
the same lenient parser, so an operator may have written comments there too —
both installers strip them and merge the plain document that remains, which is
what a rewrite of the whole file would cost them anyway.

The merge touches only the one provider key, matching the Claude and Codex
installers.

Shape

Zed is configured but never installed: it ships outside any package manager
these scripts drive, so a missing configuration directory is a hard stop rather
than a directory to create. Model entries are snapshotted from /v1/models
because anthropic_compatible has no discovery path, and are selected by kind
rather than by endpoints.

The credential is written to the OS store ahead of the settings document.
Without a key is_authenticated() is false and every model disappears from the
picker with no error shown, so a registered provider with no credential is the
failure mode worth avoiding; an unreferenced credential is harmless.

VS Code

Why customendpoint

VS Code reaches Floway through the bundled Copilot extension's customendpoint
vendor — the only non-deprecated arbitrary-URL provider that ships in stable.
customoai is deprecated and carries when: productQualityType != 'stable',
so it is invisible in a release build; ollama is deprecated in favour of a
marketplace extension.

Its group takes an API path — chat-completions, responses, or messages
and Floway serves all three for every model, so that is one group-wide
preference rather than a per-model derivation.

Why models are enumerated, not discovered

customendpoint reads only id off a /models response and drops every model
it cannot type: it passes no known-models table and overrides no capability
resolver, so the base class's if (!modelCapabilities) { continue; } discards
all of them.

Worse, setting a group-level url is actively harmful — it short-circuits into
that discovery branch and suppresses the explicit models[] list, leaving the
provider empty. The installer never writes one.

Why the key rides in requestHeaders

The group's own apiKey property is declared secret, so VS Code runs its
${input:...} decoder over whatever it finds there. A literal has no :, so
decodeSecretKey slices it into a secret-storage miss and the provider sends an
empty Authorization header. requestHeaders passes through verbatim, and this
vendor deliberately un-reserves authorization "for endpoints behind APIM,
gateways, vanity domains".

The trade is that the document carries the credential rather than the keychain,
so it is written owner-only.

Shape

Every installed build (Code, Insiders, VSCodium) and every profile within it is
configured in one pass — the active profile is not discoverable from outside the
editor, and a named profile's directory is an opaque hash rather than its display
name.

Two shell notes. The Bash loop reads its profile list from a file rather than a
pipeline, since a while reading from a pipe runs in a subshell where a failure
could neither stop the loop nor reach the caller. The PowerShell writer restores
the array brackets by hand when ConvertTo-Json unwraps a lone group, because
-AsArray does not exist on the Windows PowerShell 5.1 baseline.

Comments here are even less durable than Zed's: VS Code rewrites this whole file
itself whenever Manage Models changes anything. They are stripped on the way in
just the same.

Persisted configuration

The strict schema parses stored rows as well as request bodies, so adding a
required vscode key needs migration 0082 to backfill it — without one, the
next acquire 500s for every existing owner, and permanently: the parse happens
before any replacement row can be written and the latest-record lookup ignores
expiry. This class has now occurred twice, so a guard seeds the configuration
shape 0060 stored when it created the table, runs every migration over it, and
parses the result. Deleting 0082 fails it naming vscode; deleting 0081
fails it naming zed.

Shared installer behavior

Both installers treat the operator's document as theirs. There is one execution
shape on each side — strip the comments and trailing commas the editor accepts,
merge, write — and the only things refused are the ones no merge can carry: an
unreadable file, a shape neither half can merge into, and a document that is not
strict JSON, which each half would otherwise repair into a different file than
the other. NaN and Infinity are refused for the same reason, since jq
rewrites them to values the operator never wrote. Every mutation runs through a
backup that is removed on any failure, staged owner-only when the document
carries the key, and written through a symlink rather than over it.

Verification

The installer harness runs the real scripts under both shells. Several VS Code
cases run through PowerShell and assert the same group as Bash — the jq
program and the PowerShell projection are two implementations of one mapping.

Writing them caught two defects: an override path that skipped the existence
check and crashed against a missing directory instead of reporting it, and a
catalog fixture that never exercised discrete effort levels.

Review then caught three more that would have broken real installs, all in the
PowerShell half and all confirmed by reverting the fix and watching a test fail:
File.Replace was passed $null, which PowerShell binds as String.Empty and
the API rejects, so every Windows re-run failed; the root shape was read from
the decoded value, which cannot tell [] from an object; and limits used
PowerShell truthiness, so a catalog value of 0 silently became a fallback.

The first of those shipped unexecuted because the atomic-replacement branch is
Windows-only and no test dropped its platform conjunct. That rewrite is now
keyed per agent and throws when a guard stops matching, and both editors have a
test through it.

Full repo: 5489 tests, typecheck, and lint all green.

Since then the harness has grown to 224 installer tests, and the
round that followed found the divergences a single-host suite cannot: awk
under a UTF-8 locale dies on a smart quote pasted from a web page rather than
classifying it, and ConvertFrom-Json on Windows PowerShell 5.1 does not
enumerate a top-level array — which made both editors configure nothing on
every Windows run while every test stayed green. Those were found by running the
served script on a real 5.1 host (5.1.26100.8875), which is also where the
credential-store and ACL paths were measured.

Full repo: 5683 tests, typecheck, lint, and the web build all green.

Notes

  • The tab icon comes from simple-icons under CC0; lobe-icons carries no Zed mark.
  • No uninstall path. Zed's own "Remove Provider" only deletes the key from the
    user file, so a Floway entry in global_settings.json survives it.
  • The tab icon is the VSCodium mark from simple-icons under CC0. Neither icon set
    carries a Microsoft-branded one, and the tab configures all three builds, so
    the unbranded shape is the more accurate label as well as the licensed one.

Menci added 4 commits August 8, 2026 05:26
Zed reaches Floway through its `anthropic_compatible` provider rather than
`openai_compatible`. That provider gets the full JSON Schema for tool
definitions where the OpenAI one is pinned to a lossy OpenAPI subset that
rejects `if`/`then`/`$ref` outright and silently drops `format` and
`additionalProperties`; it maps Anthropic errors into typed retry
semantics instead of flattening them at the transport boundary; its SSE
reader tolerates a stray `[DONE]` sentinel that gateways emit; and it
reports cache token counts, which the Chat Completions mapper hardcodes
to zero. That last one matters because Zed has no token counting at all,
so streamed usage is its only source of truth for the context meter and
auto-compaction.

Both installers write `global_settings.json`, a settings layer Zed reads
below the user's own file and never creates or writes itself. Owning that
file outright is what keeps this on plain JSON: the user's settings.json
is JSONC, and no portable tool edits it safely — jq and its kin cannot
parse comments, and PowerShell's ConvertFrom-Json errors on 5.1 while
7.0-7.5 silently turn a comment inside an array into a string element.
The merge touches only the one provider key, matching the Claude and
Codex installers.

Zed is configured but never installed: it ships outside any package
manager these scripts drive, so a missing configuration directory is a
hard stop rather than a directory to create. Model entries are
snapshotted from `/v1/models` because `anthropic_compatible` has no
discovery path, and are selected by `kind` rather than by `endpoints` —
the endpoint map is the upstream wire surface, and translation lets any
chat model serve a Messages request.

The credential is written to the OS store ahead of the settings document.
Without a key `is_authenticated()` is false and every model disappears
from the picker with no error shown, so the failure mode to avoid is a
registered provider with no credential; an unreferenced credential is
harmless by comparison.

Refs: zed-industries/zed#30444
      PowerShell/PowerShell#14553
Adds the Zed tab to Agent Setup: a provider-name field, the setup
command, and a config snippet pair for operators who configure by hand.

Zed takes only a name because everything else is derived. The catalog
projection lives beside the Claude and Codex model helpers and mirrors
the installer's jq program — both write the same `available_models`
array, and the suite pins the parts that would silently break Zed:
models are selected by `kind` rather than by `endpoints`, all three
capability flags are always written because Zed reads no per-field
default and a partial object fails the whole provider, and reasoning maps
onto adaptive, budgeted thinking, or no mode at all.

The snippet is a whole `global_settings.json` rather than a fragment to
merge, since Zed owns nothing in that file. Its credential half is a
separate block because Zed reads the key from the OS credential store,
which has no settings representation: macOS and Secret Service take a
one-liner, Windows needs a CredWriteW P/Invoke because the blob must be
UTF-8 and cmdkey writes UTF-16LE.

lobe-icons carries no Zed mark, so the tab icon comes from simple-icons
under CC0, normalized to the sizing and currentColor attributes the other
marks use.
Ten cases run both installers for real: the catalog projection, the
managed-key merge against a document holding a sibling provider, the
credential record, a provider name carrying a quote, and the three
refusal paths — absent configuration directory, unparseable settings, and
a catalog with no chat models.

One case runs the same scenario through PowerShell and asserts the same
provider document, which is the guard that matters most here: the jq
program and the PowerShell projection are two implementations of one
mapping, and nothing else would catch them drifting.

Both fragments gain a credential hook, since neither a keychain nor a
Secret Service is something a test host can be asked to mutate. The
harness serves `/v1/models` with one model per branch of the projection —
adaptive reasoning with images, a budget ceiling, no limits at all, and a
non-chat kind that must be dropped.

Collapse the three per-agent configuration builders onto a shared base so
the fourth does not repeat the whole schema a fourth time.
Menci added 6 commits August 8, 2026 07:55
Four defects, each of which would have shipped a broken or unsafe setup.

**A thinking mode with no budget.** Zed serializes
`Thinking::Enabled.budget_tokens` with no skip_serializing_if, unlike the
`Adaptive` variant beside it, so a mode carrying no budget puts
`"budget_tokens": null` on every Messages request and Anthropic rejects
it. The projection emitted exactly that for any model stating reasoning
without a ceiling — every Codex model (effort levels only) and every
Claude Code model (a floor, no ceiling). Such a model now stays in
Default mode, which the picker still offers, and where a budget exists
the floor is preferred: Zed sends it verbatim on every request and
Anthropic requires it below max_tokens.

**XDG honored on macOS.** Zed consults `XDG_CONFIG_HOME` on Linux and
FreeBSD only; macOS falls through to an unconditional `~/.config`. An
operator exporting XDG on macOS — routine for shared dotfiles — had the
file written where Zed never reads it, and the run reported success.
FLATPAK_XDG_CONFIG_HOME is honored on Linux, matching upstream's order.

**A hardcoded app bundle.** `security -T` fails the entire call when the
path does not exist, so anyone running Zed Preview, Nightly, or a
`~/Applications` install had setup abort with the reason discarded by a
stderr redirect. Only bundles present on the host are named now, and
naming none still writes the item.

**A test hook that exfiltrated the key.** `AGENT_SETUP_TEST_CREDENTIAL_
RECORD` wrote the live credential in cleartext to an env-var-chosen path,
replacing the hardened store. Every other hook in the family adjusts a
timeout, a colour, or where code is fetched from, and the two that hand
control to a chosen path strip `SETUP_API_KEY` first. Since these scripts
are piped into a shell that inherits the caller's environment, one
`.envrc` or profile line was enough. It is gone; the harness shims
`secret-tool` and `security` on PATH instead, which also brings their
real argument vectors under test for the first time.

Also from the same pass: the catalog request carries its credential in a
curl config file rather than argv; model projection moved ahead of both
the credential write and the backup, so no failure can strand an orphan
backup or an unreferenced keychain entry; PowerShell uses File.Replace on
Windows rather than a delete-then-create Move-Item, guards its Add-Type
against a second run in the same console, validates through the property
bag so a provider named `Count` resolves, compares against $null so a
catalog 0 survives, and surfaces the underlying fetch error. The
`secret-tool clear` call and its rationale were both wrong — libsecret
replaces a matching item — so both are gone, and the config-dir override
is renamed to the `AGENT_SETUP_TEST_` convention.
The dashboard fetches `/api/models?include_unlisted=true` to populate the
alias combobox, and nothing downstream dropped those rows. The installer
snapshots `/v1/models`, which by definition excludes them, so an operator
with `modelPrefix.addressable` wider than `listed` got a snippet naming
both ids — visually identical in Zed's picker, since the unlisted row
copies its display name — while the setup script wrote only one. The
projection now drops them, and the id dedupe it carried is gone: the
catalog collapses on a Map keyed by public id, so duplicates cannot
arrive, and the guard only masked the divergence from the jq program,
which never had one.

Reasoning maps the way the installers now map it — a budget or no mode at
all, floor preferred over ceiling — so the two implementations write the
same document again.

An empty projection no longer renders a copyable document. The installer
refuses that catalog rather than register a provider with no models, and
the panel offering one anyway would have handed the operator a config
that fails silently. It shows the same refusal instead.

The config hint said "Merge into", but the snippet is a whole document
whose paste would drop any other provider in the file; it now says to
save it and merge by hand when one exists. The credential snippet drops
the `secret-tool clear` that the installer also dropped.
**The curl config file corrupted the key.** Moving the credential off argv
looked like a hardening, but curl's quoted config value parses `\`
escapes and terminates at `"`, and the unquoted form is discarded for
containing whitespace. Verified against a listener: a key holding a quote
arrives truncated, and one holding a backslash arrives with the escape
expanded — the gateway 401s and the operator is told only that the
catalog fetch failed. An API key is an arbitrary string, so this is
reachable. Reverted to `-H`, which passes the key byte for byte, with the
tradeoff stated where it is made.

**PowerShell stored the key with a trailing newline.** `$key | & secret-
tool` terminates the piped object with a newline and secret-tool stores
every byte it reads, so Zed read back `sk-…\n` and sent a malformed
Authorization header on every request — a 401 loop behind a settings file
that looks correct, and a divergence from Bash on the same host. Now
written through a redirected stdin.

The shim could not have caught the second one: command substitution eats
a trailing newline, so `key` and `key\n` recorded identically. It records
the byte count instead, and the PowerShell test asserts it — the Bash
test could not, because it takes the `security` branch on macOS while CI
takes `secret-tool` on Linux. Confirmed by reintroducing the pipeline and
watching the test fail.

Also: the dashboard's macOS credential snippet still named a hardcoded
`/Applications/Zed.app`, which fails the whole call on a Preview,
Nightly, or `~/Applications` install — the installers were fixed for this
in the previous commit but the pasted form was not. `max_output_tokens`
moves after `capabilities` so the web projection matches the installers
byte for byte rather than only semantically, and `== null` there drops an
explicit JSON null the way both installers already do. `--agent` accepts
every registered agent instead of the original two. A ceiling-only
catalog row covers the budget fallback the installer harness never
exercised. PowerShell reports its own message when a backup cannot be
made, and an unverified claim about PSObject intrinsics is removed rather
than left asserting something I did not confirm.
…licitly

The redirected-stdin write leaked a Process handle on every run. Only
stdin is redirected, so there is no output pipe to deadlock on, but the
handle still needs releasing.

The stdin assertion compared against a JS string length, which equals the
UTF-8 byte count only because the sentinel is ASCII. It now says so and
measures bytes, so a non-ASCII sentinel would not quietly turn the check
into a tautology.
The strict schema parses stored rows as well as request bodies, so a
configuration written before this branch — one with no `zed` key — throws
on the next acquire. It throws permanently: `latestByUserId` has no
`expires_at` predicate, so the stale row stays latest, and the parse
happens in `restorableConfiguration` before the insert that would replace
it, so the sweep trigger never fires and retrying re-enters the same 500.

Verified by parsing a pre-branch blob against the current schema, then by
running the migration against SQLite and re-parsing: the legacy row
backfills and parses, an already-migrated row keeps its own name, and
unrelated fields survive. Same shape as 0061 and 0068, which backfilled
for the same reason.

The pasted macOS credential snippet now runs its Darwin arm in a
subshell. Unlike the installer's function, where `set --` is scoped, a
pasted script sets positional parameters in the operator's own shell —
leaving the API key in `$@` after the paste, where a later `echo "$@"`
would surface it.

The credential shim records the secret as hex rather than a byte count.
The count proved a trailing newline was absent but would have accepted
any same-length content; confirmed by substituting an equal-length wrong
secret and watching the hex assertion fail where the count had passed.
@Menci
Menci force-pushed the zed-agent-setup branch from 4db744f to 0313237 Compare August 8, 2026 08:24
Menci added 18 commits August 8, 2026 17:13
The shim piped stdin through `od` and `tr`, neither of which is in the
harness's hermetic tool list — the installer PATH is exactly binDir plus
SHIM_BIN, with no system directories. Both stages die with "command not
found", the substitution yields empty, and `case` swallows the status, so
the shim still exits 0 and the assertion compares '' against the key.
That is red on CI, which runs Linux and therefore takes the secret-tool
branch; my macOS runs take `security` and never reach it. Reproduced
under `env -i PATH=…` before fixing, and confirmed afterwards by forcing
the secret-tool branch on this host.

The secret now goes to its own file via `cat`, the one byte-preserving
tool already on that PATH. Still byte-exact, still fails on the trailing
newline a piping shell would add, and no longer licenses the installers
to reach for tools the harness does not provide.

PowerShell 6+ routes -Headers through HttpClient, which parses
Authorization as a typed header and rejects a parameter containing `,` or
`"` before the request leaves the host — verified: a key with a comma
throws "The format of value is invalid" with no request sent, and
succeeds with -SkipHeaderValidation. 5.1 has neither the validation nor
the switch. The Bash installer already passes the same key to curl
verbatim and says so in a comment; this makes the two agree.

The projection is also handed to jq through --slurpfile rather than
--argjson. It is already a file, and a single argument is capped at
128 KiB, which a few hundred chat models would reach.
It is the one free-text field in Agent Setup, so the only one whose draft
the gateway can reject — every other control is a dropdown over a closed
set. Typing a name and pausing after a space PUT a padded value, and a
400 is not retryable: the save was abandoned, the copy button stayed
disabled, and the untranslated Zod issue landed in the page-level message
bar with nothing pointing at the field.

The name is now held locally while invalid and only patched into the
draft once it is acceptable, with the reason shown on the field itself —
the same shape as `d2cd73ab3`, which moved model validation to its
fields. Confirmed load-bearing by removing the predicate and watching the
test fail.

The length bound is restated rather than imported, since `apps/web` may
not runtime-import the gateway package; it only stops typing early, and
the gateway still enforces the rule.
The local hold that keeps an invalid name out of the draft outlived the
configuration it was typed against: once set it shadowed the incoming
value permanently, so a lease arriving for another key rendered behind a
half-typed name from the previous one. That is the invariant the two
existing card tests protect — the form shows the lease and nothing else.

It is now keyed on the configuration's api key id, so the hold applies
only while that configuration is the one on screen. Confirmed by writing
the test first, watching it fail, and re-running it against the unkeyed
form afterwards.

My first attempt at that test asserted the field reverts the moment
another key is picked. It does not, and should not: the card deliberately
keeps showing the current configuration until a lease answers, which the
neighbouring test asserts. The test now stubs the arriving lease, which
is the point at which the hold must yield.

Also renames a lease-projection test that still said "both agents"; the
same phrasing was corrected in its sibling when Zed landed.
The dashboard's Windows credential snippet emitted a bare Add-Type under the
type name ZedCred, while the installer defines the same writer as
FlowayZedCredential behind an existence guard. Two surfaces writing the same
credential now agree on both.

Add-Type returns its cached type for a byte-identical re-add and rejects a
differing source under a name already in the AppDomain, so the guard makes a
second paste — after rotating the key or renaming the provider — a no-op
outright instead of resting on that cache. The emitted snippet was parsed with
the PowerShell parser and run twice in one session to confirm the here-string
survives the enclosing block.

The installers' /v1/models fixture served the catalog to any caller, so
removing the Authorization header from either installer left the suite green
while every real install would have failed. It now answers 401 without the
sentinel key; removing the header fails four Bash cases and the PowerShell
parity case.
The comment asserted how VS Code keys its own provider group, which nothing on
this branch implements and no reviewer here can check against code. The
constraint it documents — an opaque label neither editor derives behavior from
— is stated from the one consumer that exists.
The installer fetched /v1/models and projected it into Zed's available_models
itself, which meant the same mapping existed three times: a jq program, a
PowerShell loop, and the dashboard's preview builder. They had to agree byte for
byte and repeatedly did not — a stated limit of 0 became a fallback under
PowerShell truthiness, an empty effort list was dropped, and key order drifted.
Each divergence was found by a test written specifically to compare the three.

The gateway now projects the catalog once and embeds the result in the script it
serves. The installers keep only the merge: write the embedded list to a file
and hand it to jq, or decode it and hand it to the document writer. Nothing
compares three implementations any more because there is one, in
packages/agent-setup/src/models.ts, which the dashboard imports through a new
/models subpath export so its preview is what a run will write.

Two values stay client-side. The endpoint URL, because the gateway does not
render its own public origin — the dashboard injects it into the executing
shell. And the API key, because it already appears once in the script and
copying it into every model entry would multiply the credential for no gain.

Listing now happens while serving the script, so it can fail there. A listing
failure renders a script that says so and exits non-zero: an opaque 404 would
read as a dead setup link and a 500 as a gateway fault, and the upstream detail
stays in the operator's log. The upstream scope is resolved through the same
intersect rule the data plane uses, extracted so a setup script cannot advertise
a model the key cannot reach.

zed.sh drops from 307 to 248 lines and zed.ps1 from 344 to 278.
VS Code reaches Floway through the bundled Copilot extension's `customendpoint`
vendor, the only non-deprecated arbitrary-URL provider that ships in stable.
Its group takes one of three API paths, and Floway serves all three for every
model, so that is a group-wide preference rather than a per-model derivation.

Models are enumerated rather than discovered, for the same reason Zed's are:
`customendpoint` reads only `id` off a `/models` response and drops every model
it cannot type, and a group-level `url` is worse than useless — it is not even a
declared property, yet it short-circuits into that discovery branch and
suppresses the explicit list, leaving the provider empty. So the gateway
projects the catalog and embeds it, and the installer keeps only the merge.

The projection lives beside Zed's in one module, which is what keeps the two
halves honest: the same entries reach jq and PowerShell, and the dashboard
preview imports them too. Only the endpoint and the credential are attached
client-side — the gateway does not render its own origin, and the key already
appears once in the script. A pasted snippet has no merge to attach them, so
the preview builder does it instead.

The key rides in `requestHeaders` rather than the group's `apiKey`: that
property is declared `secret`, so VS Code runs its `${input:...}` decoder over a
literal and lands on a secret-storage miss. `requestHeaders` survives the header
sanitizer because this vendor un-reserves `authorization` for endpoints behind
gateways. The trade is that the document carries the credential, so it is
written owner-only.

Every installed build and every profile within it is configured in one pass,
because the active profile is not discoverable from outside the editor. Each
profile is its own transaction and a failure does not stop the others, so one
hand-edited file cannot leave the rest unconfigured; the run still exits
non-zero. Migration 0079 backfills the new configuration key, without which the
strict schema would lock every existing owner out of Agent Setup.
`budget_tokens.min` is a lower bound an operator may legitimately record as 0,
meaning "no lower bound stated", and the projection preferred the floor
unconditionally. Zed sends the budget verbatim on every Messages request and
Anthropic rejects anything under 1024, so such a model 400'd on every call with
nothing in Zed's UI to explain it — and a stated ceiling that would have worked
was discarded. A floor too small to use now falls through to the ceiling, and a
model with no usable budget stays in Default mode, which the picker still
offers. This is the same class as the zero-limits case: a stated 0 is a value,
not an absent bound.

Two failures around the new gateway seam:

The PowerShell failure script ended in `exit 1`. Its documented invocation is
`irm … | iex` in the operator's own console, which `exit` closes — taking the
message the script exists to show with it. Reproduced; it now sets
$global:LASTEXITCODE and returns, matching what the installers already do.

The listing failure told the operator to check the gateway log, while the
diagnostics helper deliberately drops the error message because a generic
failure may carry a secret. Both secrets are in hand at that call site, so the
reason is redacted and kept rather than discarded.

And a transaction boundary: the PowerShell document mutation sat outside the
try that owns rollback. A provider name PowerShell reserves on every object —
PSObject, PSBase, PSTypeNames, all accepted by the schema because Zed treats
the key as opaque text — makes Add-Member throw there and leaves the backup
beside the operator's settings forever. Moving it inside restores the
"no orphan" property the comment claimed; a test asserts the directory is clean
after such a run, and fails when the block moves back out.
`PowerShell writes the same provider document as Bash` never read the Bash
document — it re-asserted a hand-maintained copy of that test's expectations,
which is the duplicated-expectations mechanism the server-side projection was
built to remove: a restated expectation can drift from both implementations at
once. It now runs both halves over the same catalog and prior document and
compares the two results, with one anchored assertion so the comparison cannot
pass by both halves being wrong the same way. Making the PowerShell half drop a
model fails it.

A single-model catalog had no coverage in either half, though it is where
ConvertTo-Json unwraps a one-element array into an object — which fails Zed's
Vec deserialization and takes the whole provider down — and where jq's
--slurpfile/$models[0] is pinned.

The catalog fixture gained the two rows whose projection branches nothing
exercised: a model stating limits of 0, and an addressable-but-unlisted row
that must not reach the settings document.

The model server's /v1/models handler and its two catalog modes are gone. No
installer requests that endpoint any more, so the handler was unreachable and
its comment — that rejecting an unauthenticated request makes every success
case assert the header — had become false.
# Conflicts:
#	packages/agent-setup/scripts/test-installers.ts
The projection omits the per-model endpoint and the credential because a setup
run's merge attaches them — and a pasted snippet has no merge, so the dashboard
attached them separately. Two implementations of one rule is the shape of
divergence the server-side projection was built to remove, and nothing measured
them against each other: changing the snippet to emit a wrong URL and a
capital-A `Authorization` — which `customendpoint` does not un-reserve, so the
sanitizer drops it and every request 401s — left all 602 web tests and all 26
VS Code installer tests green.

`addressVSCodeModels` now states it once, beside the projection. The dashboard
builds its snippet from it, and both installers are asserted against it for all
three API paths: what a run writes must equal what the operator pastes. The
capital-A mutation fails that assertion.
`umask 077` made every Zed settings write come back 0600, including a run that
refused the document and reported leaving it untouched — the backup was created
under the umask and moved back over the original. This file holds no credential
(Zed reads the key from the keychain), so the operator's own mode is theirs to
keep: the backup is copied with `-p` and the staged replacement inherits the
mode of the document it replaces. A new file still takes the umask default.

The pre-backup gate also passed on a truncated file, because jq runs a filter
zero times on empty input and still exits 0. Such a file was backed up, staged,
and only caught at the staged check — reporting a fault in our own list rather
than naming the document the operator has to fix. `-e` answers that before any
backup exists.

The ESLint rule forbidding apps/web from runtime-importing @floway-dev/agent-setup
matched only ImportDeclaration, so the branch's own `export … from` re-export
of the /models subpath satisfied it by syntax rather than by intent — and a
future re-export of the root would have too. It now matches exports as well and
names /models as the one allowed surface, which is what makes the dashboard
preview and the embedded projection the same code.
…alves

Zed is the first agent to key a map by operator-chosen text, which reaches a
case-insensitivity the shared PowerShell helper had never been exposed to:
`-contains` matched `floway` for a chosen `Floway`, and the dotted assignment
then wrote the new value under the OLD key. Bash added a second key instead. So
one operator renaming only the case got a picker still showing the old name and
an exit 0 on Windows, and a stale broken provider beside the new one elsewhere.

The PowerShell property bag cannot hold both keys at once — adding `Floway`
beside `floway` replaces it — so keeping both is not a behavior the two halves
can share. Both now drop any key differing only by case before writing the
chosen one, which is also the better outcome: a case-only rename stops leaving
an entry whose stored credential no longer matches its name. ASCII case is what
both fold, stated where they fold it.

The Zed fragment does this itself rather than through Set-SetupProp, whose
case-insensitive lookup is correct for the fixed key names Claude and Codex
pass it.
Zed reads global_settings.json with serde_json_lenient, so a JSONC comment is
the operator's own content. jq refuses such a document and PowerShell 7 accepts
it and drops the comments on the way out — data loss reported as success, and a
third behavior on the 5.1 baseline, which errors. Both halves refuse it now. The
check walks strings rather than matching a pattern, so a `//` inside a URL or a
model id is not mistaken for a comment.

ConvertTo-Json emits a subtree deeper than its limit as the literal string
"@{k=}" with only a warning, and the staged check cannot see it because that
inspects the provider entry alone — an unrelated setting nested deeply enough
was replaced by a string under exit 0. The warning is promoted to an error.
# Conflicts:
#	packages/agent-setup/src/script-assets.generated.ts
Menci added 10 commits August 10, 2026 12:36
The unknown-catalog guard sat above the Codex fall-through, so a failed
model listing blanked the Codex snippet pane — a document built from the
configuration alone, needing no catalog at all. It belongs inside the Zed
branch, which is the only one that projects models.

The case-variant guard covered `language_models` and not the key one
level down. `anthropic_compatible` is reached by the same case-insensitive
member access, so `Anthropic_Compatible` took the provider into a key
Zed's deserializer never reads while the run reported success — and the
staged check could not see it, because it reads back through that same
access.

And the overflow arm compared the exponent against 308, which misses
every shape that overflows without one: `9e308` and `1.9e308` sit in the
same decade as the limit, and a 310-digit integer has no exponent to
compare. It measures the magnitude from the significant digits now, and
decides the boundary decade on the leading four, because the double range
ends at 1.797…e308 rather than at a power of ten. The scan enters at a
token's first character only — every digit re-entered it before, and a
suffix like `.7e308` measures larger than the number it came from.
The token-start guard treated `+` and `-` as interior characters, so the
scan never entered a signed number: for `-1e400` the leading digit is
preceded by the sign and every later digit by a digit or `e`, leaving the
whole token invisible. jq rewrites it and both PowerShell hosts refuse
it, so the Bash half configured a document the other stopped on — the
split this arm exists to close, reintroduced by the rebuild that closed
it for unsigned numbers.

A sign is now transparent to the entry test. `1e-400` still stays out,
because the digit after that sign is preceded by `e`, which is not.
… group

VS Code stores the Thinking Effort an operator picks in the picker inside
our own `customendpoint` group, keyed by model id, and reads it back on
every resolve. Both halves rebuild that group from scratch, so a re-run —
done to refresh the catalog or after rotating a key — reverted every
choice to the schema default with nothing on screen. The asymmetry gave
it away: a foreign gateway's `settings` survives, because its group is
copied whole, and only ours was rebuilt without it.

Carried across by exact name on both halves, in the position jq's object
addition puts it, so the two still write the same document.

A byte-order mark no longer reaches the scanner. It is not JSON content —
jq reads a document carrying one and the PowerShell reader strips it —
but some awks die on those bytes rather than classifying them, and the
verdict travelled in the exit status, so a failure of the tool doing the
examining was reported as a verdict on the operator's content. The verdict
travels on stdout now and the status is left to mean "could not examine",
which both installers name as such.

The BOM strip and the tool-failure arm are unobserved: the macOS awk on
this machine does not die on those bytes under any locale I could set, so
the die is host-specific and the harness uses whichever awk the host has.
The overflow arm guarded one end. A literal below the smallest subnormal
decodes to 0 on both PowerShell versions and is written back as `0.0`,
silently changing a number inside an entry the run was not asked to
touch, while jq preserves `1E-400` — the same harm the overflow arm
exists to prevent, reached from underneath.

Worse, the last round put `1e-400` into the document that asserts both
halves *accept* a file unchanged, where its only value assertion was on a
different key. It moves to the refusal table; `1e-320` is subnormal and
representable, so it stays as the accepting case. A stated zero is
unaffected, because its own token carries no significant digit.
The scanner blamed the operator's document for two failures that were
not the document's.

`awk` under a UTF-8 locale — the macOS default — dies with a multibyte
conversion error on any non-ASCII byte outside a string, so a smart
quote or a non-breaking space pasted out of a web page produced no
verdict at all. The scan now runs under `LC_ALL=C`, which makes it
byte-oriented and classifies those bytes the way the PowerShell half
does. The BOM-stripping `sed` stage became redundant once the scan reads
bytes and was removed.

The two halves also disagreed on subnormal magnitudes: `1e-324` and a
decimal-only tiny literal were accepted by Bash and refused by
PowerShell. Both causes are fixed — the boundary decade at 1e-323, and
the magnitude of a mantissa below one, which is the exponent less the
leading zeros of the fraction rather than the length of an empty integer
part.

With the scan no longer dying on readable content, a missing verdict can
only mean the scanner itself failed, so that arm names the scanner
rather than the document.

Tests cover the scan under both `C` and `en_US.UTF-8` — the harness
builds the child environment from scratch, so a locale-sensitive
behavior is unreachable unless a test asks for it — the underflow
boundary on both sides, and a non-breaking space in the VS Code half.
…th halves

Backing up a document was written out at nine sites across the two
halves, and four of them left a partial copy behind when the copy or the
mode restriction failed — a truncated file beside the operator's own,
which a rollback would later hand back as their document. Both halves
now reach one helper that removes the remains before the failure is
reported, so a site cannot be written without the cleanup.

`settings` inside our own VS Code group is the operator's whatever its
value, and both halves read it by truth rather than by presence: jq's
`//` dropped a stored `null` and `false`, and the PowerShell null test
dropped `null`. Both ask for presence now.

The PowerShell overflow refusal was inert on the only host that half
runs on: Windows PowerShell 5.1 reports overflow from
`[double]::TryParse` by returning False, not by parsing to Infinity —
measured on 5.1.26100.8875, where `1e400` gives False and `1e-400` gives
True with 0.

`Resolve-SetupManagedPath` returns a canonical path for a plain file as
well as for one behind a link, anchored on the session's working
directory rather than the process directory PowerShell does not move.

Also corrects three comments that described awk's exit status as the
scanner's verdict — the verdict travels on stdout — and removes a stale
paragraph in the Zed half that contradicted the one below it.
# Conflicts:
#	apps/web/src/components/api-keys/agent-setup-card.tsx
#	apps/web/src/i18n/locales/en.ts
#	apps/web/src/i18n/locales/zh-Hans.ts
#	packages/agent-setup/__tests__/routes_test.ts
#	packages/agent-setup/scripts/test-installers.ts
#	packages/agent-setup/src/script-assets.generated.ts
@Menci Menci changed the title feat(agent-setup): configure Zed feat(agent-setup): configure Zed and VS Code Aug 10, 2026
Menci added 18 commits August 10, 2026 19:08
…le takes

Four behaviors had no observer, so a change to any of them was silent.

The API path an operator picks reached the projection only as the
default, so a projection that hardcoded `messages` passed. It is now
asserted against a path they chose.

The user-directory derivation from `HOME` — the only path a real
operator takes, since every other test names its directories through the
override — never ran. Two builds are placed under `HOME` and both are
configured.

A file under `profiles/` is not a profile. Dropping the directory filter
wrote the key to a path VS Code never reads; nothing said so.

The profile loop's second arm, which names a failure this installer
never predicted, was reached only through Stop-Setup — which reports
itself. A profile directory that cannot be written now exercises it, and
costs only that profile.

`$keptSettings` is declared beside the list it belongs to rather than
inside the branch that fills it: a document that does not exist leaves
that branch unrun, and reading an unassigned variable as "absent" is a
coincidence of PowerShell rather than a decision.

Also corrects the token-plan summary, which claimed a bound the code
does not apply to a reservation that comes out of no window, and names
the dashboard's unknown-catalog suite for what it covers.
…pt for the C#

A hand-merged `chatLanguageModels.json` can hold two groups under our
name — which is what the snippet pane asks the operator to produce — and
the two halves searched them differently. jq takes the first `settings`
among them; PowerShell stopped at the first group under our name, so a
document whose settings sat on the second one lost every Thinking Effort
the operator had picked. Both halves now end the search where the
settings are, and a fixture asserts they write the same document.

The Zed credential writer was registered as a common section, so all
four PowerShell scripts compiled a P/Invoke declaration into the
operator's console and three of them opened with an unexplained block of
C# — in a response that also carries their API key. The manifest now has
a place for a fragment only one script needs, and the Zed script is
byte-identical to before.

Also corrects two comments: the VS Code verdict capture named `set -e`,
which this installer never enables — the real reason is the one the Zed
half gives, that the scanner answers with three statuses and `case`
would clobber `$?` — and the VS Code migration pointed at the number an
unrelated Ollama migration now holds.
…olves

Both halves preserved the first group under our name that carried
`settings`. VS Code resolves duplicates the other way: it walks the
groups in array order and applies each matching group's settings into
one map keyed by model, so the later group is the one whose Thinking
Efforts were in effect. Keeping the first preserved the settings the
editor was ignoring and dropped the ones the operator was using — and a
hand-merged file, which is what the snippet pane asks them to produce,
is exactly where two groups come from. The fixture now puts distinct
settings on both duplicates, so a half that took the first can no longer
pass by keeping the only one there was.

The manifest's whole-file groups are typed as such: `standalone` and
`agents` are embedded entire, and only `common` is tiled, so a cut point
declared on either would have been accepted and silently ignored.

Also corrects a comment served inside the Zed script, which still named
the credential fragment's path from before it moved out of `common/`.
…not add beside it

The snippet pane's comment claimed a second group under one name
collapses onto the last, citing a change-detection map that decides
nothing about it. What VS Code actually does is split: a model is
identified as `${vendor}/${group}/${id}`, so two groups sharing a name
produce the same identifiers and the second registration is skipped with
a log line — its models, endpoint and key never take effect — while
per-model `settings` are read from every matching group in order, so the
later group's win.

An operator who already ran the installer and then followed the hint
("Merge this group into chatLanguageModels.json") appended a second
Floway group that did nothing, with the stale entry still in effect and
only a log line to say so. The hint now says to replace a group already
using the name, and the comment states both rules against the code that
implements each.

The installer's own merge is unaffected: it replaces every group under
its name with one.
Three references pointed at code that does not decide what the comment
beside them claims.

Both installer headers said groups are keyed by `${vendor}:${name}` so
ours replaces only its own, citing a map built to diff old configuration
against new for a change event. Nothing upstream keeps the list unique —
which the same files say fifteen lines down, where they handle two
groups under our name — so the sentence now states the rule as the
installer's own: it selects every group of our vendor and name and
replaces them with one.

The last-wins rule for `settings` cited the branch taken by vendors that
contribute no configuration schema. `customendpoint` contributes one
(extensions/copilot/package.json), so that branch never runs for the
vendor these files write; the collection that does run is the one in the
resolve path, which the Bash half already cited and the other two sites
did not.

The rule's other half — last among the groups that carry settings, not
last under our name — had no fixture, so a PowerShell loop that cleared
the kept value on a later group without settings would have dropped the
operator's choices with every test green.
…from

Every Zed case named its directory through the test override, so the
derivation an operator actually takes shipped unexecuted: the Darwin
arm, the Flatpak and XDG arms, the `$HOME/.config/zed` fallback, and the
PowerShell `%APPDATA%\Zed` arm. Each of them silently configures a
directory Zed never reads if it is wrong, while the run still stores the
key in the credential store — and the sibling VS Code installer had this
closed for the same reason.

Two cases cover it. The first runs both halves with no override and
asserts the document lands under `~/.config/zed` and nowhere else, with
`XDG_CONFIG_HOME` exported and ignored on macOS, honored elsewhere. The
second answers the platform question as a Linux host would — a `uname`
shim for the Bash half, and for PowerShell the same targeted rewrite the
Windows-replacement guard uses, since its platform comes from an
automatic variable — so the XDG branches and the fallback below them run
on a macOS runner too.

The child gets no `XDG_CONFIG_HOME` unless a test asks for one, for the
same reason it gets no `LANG`.
…ay it does

Zed does not read its two variables alike. `FLATPAK_XDG_CONFIG_HOME` it
takes straight from the environment; `XDG_CONFIG_HOME` reaches it
through `dirs::config_dir()`, which discards a relative value and falls
back to `$HOME/.config`. Both halves honored a relative one, which
configures a directory the editor never reads while the key still lands
in the credential store. VS Code has no such filter and keeps its
variable raw, so only the Zed half gained the gate.

The VS Code half asked `$IsMacOS` directly rather than the one platform
decider the rest of the installer uses — a second answer to the same
question, and the one the harness cannot reach.

Three derivation arms had no observer: Zed's Flatpak arm on both halves
and VS Code's XDG base. All three now run, on a macOS runner too,
through the platform-forcing the previous commit established.

A failed backup is reported by the shared helper on the Bash side but
was rethrown bare by the Claude and Codex PowerShell callers, so the
operator read a raw .NET message naming Copy-Item rather than their own
file. All four agents now give the sentence their Bash half gives, and
both Codex documents are covered — the config is copied first, so a case
that only has a config never reaches the token's own arm.
…ther copy

The Codex config backup asked whether its failure was the already-
reported sentinel, but only the mode restriction raises that, and this
arm does not restrict a mode — the Zed backup, the same shape, correctly
asks nothing. The guard stays where it can fire, with a line saying why.

`renderScriptFailure` spelled the script-language union out by hand,
so adding a language would have left it behind rather than failing to
typecheck; it takes the exported alias its only caller already holds.

The upstream-cap intersection is not stated once, as its comment
claimed: the browser cannot import the gateway module, so the dashboard
states it again in `components/models/reachability.ts` — and this branch
made the two co-dependent by narrowing the Agent Setup preview through
the browser copy while the served script is projected from the gateway's.
Each copy now names the other, and the condition under which they should
become one is written down where the decision would be made.
…onfigure

The setup pane and the snippet pane each projected the catalog to decide
whether there was anything to configure, and each rendered its own copy
of the warning that says there is not — one rule with one sentence,
written twice, so a change to either reached only one tab. Both now go
through one projection and one component.

Nothing observed the panes with a catalog that is not empty: every case
rendered the empty one, which is the warning path, so a projection that
answered with nothing for an editor would have blanked that tab with a
green suite. Three cases render a chat model and assert each editor's
document appears instead of the warning.

Also corrects a test comment that credited the installer with an
`apiKey` property. Neither half writes one — that property is VS Code's,
and it is decoded as a secret-storage reference, which is why the key
rides in `requestHeaders` on both sides and in the paste.
… its first digits

The two halves disagreed over a real band of literals. The awk scanner
compared four significant digits, so `1.7978e308` — which shares
`1797` with the largest double and overflows — was called
representable, and the accepted literal then reached jq, which rewrites
it to `1.7976931348623157e+308`: a silent change to a number inside an
entry the run was not asked to touch, which is what this arm exists to
prevent. The same four digits refused `2.4705e-324` at the other end,
which rounds to the smallest subnormal rather than to zero and which
PowerShell keeps.

Both boundaries are now compared as 17-digit strings — the mantissas do
not fit in a double, which is the very thing being decided, and awk has
nothing wider — against the values `[double]::TryParse` uses. Fixtures
cover a literal on each side.

`zed.ps1` restricted a new document's stage twice; the second call has
been inert since the first was added for the mute-stat case, so nothing
could observe it going wrong. The carry-over now reads as what it is:
a step for a document that already existed.

Also: a dashboard case named for offering the setup command observed
only that the warning was absent, which a blank pane satisfies too, and
three harness comments had drifted onto neighbouring declarations.
The previous round moved the comparison to the largest double, but the
other half asks `[double]::TryParse`, which is correctly rounded: a
literal overflows only past the halfway point beyond that value. So
`1.7976931348623158e308` is finite and the Bash half refused a document
the PowerShell half configures — the same class of split as before, in
the other direction. The tie is decided by twelve digits past the
seventeenth, which puts the split beyond any precision a settings file
carries: `1.797693134862315808e308` overflows, everything below it does
not. The underflow end gets the same treatment, where
`2.4703282292062327e-324` rounds to zero and
`2.470328229206232721e-324` does not.

Thirteen literals either side of both boundaries were measured against
real IEEE doubles and against `[double]::TryParse` on pwsh before and
after; four of them are now fixtures.

The PowerShell underflow guard asked whether the whole token held a
non-zero digit, so `0e5` — a stated zero whose exponent carries the only
one — was refused where the other half accepts it. The significand
decides now, which is what its comment always claimed.
A 29-digit comparison still split the two halves at the digits past it,
in both directions: `[double]::TryParse` reads every digit, so a literal
sharing the prefix but running longer was ordered one way by awk and the
other by PowerShell. Both boundaries are dyadic rationals — 2^1024 −
2^970 and 2^-1075 — so both decimal expansions terminate, and the
scanner now carries them whole and orders a literal against them
digit by digit. Equality refuses at both ends, which is where rounding
takes the tie: to infinity above and to zero below.

Fifteen literals across both boundaries were run through the real
`_json_scan_verdict` and through `Get-SetupJsonVerdict` on pwsh 7; all
fifteen now agree. Four are fixtures, including each turn written out
by the same arithmetic that produced the installer's constants.
The PowerShell half already carved out `0e5` — a zero whose only
non-zero digit is its exponent — but the awk scanner derived the
magnitude from the exponent alone, so `0e400`, `0.0e-323` and
`0.000e400` landed in the out-of-range decades and were refused where
the other half configures the document. A significand of zero is zero
whatever the exponent says, and no writer can change its value, so none
of the range arms apply to it. Measured through both halves before and
after: thirteen literals now agree.

The macOS keychain comment claimed the key is unavoidably an argv
element. It is not — `security -i` reads the whole command line, key
included, from stdin — but that mode exits 0 whether its sub-command
succeeded or failed, so a store that never happened would be reported
as done and Zed would show no models with nothing to say why. Both
halves now record the measurement and the decision it forces.
…her number takes

`budget_tokens` was the one value this projection put on Zed's wire
without asking whether Zed can hold it. It is an `Option<u32>` under
Zed's fallible-options macro, so a fractional one is swallowed to `None`
rather than reported: the model reaches the picker in Default mode with
its thinking gone and nothing on screen to say why. The catalog can
carry one, since a raw upstream's thinking bounds are copied across
without a numeric check.

Two comments claimed more, and less, than the source they cite.
`max_output_tokens` is not a required `u64` — it is an `Option` under
the same macro, so a bad value there is swallowed and Zed applies its
own 4096, which is not the reservation the plan computed. And a failing
`max_tokens` does not take the settings document down: the entry fails
its provider, and `anthropic_compatible` is itself a fallible option, so
every anthropic_compatible provider is dropped while the rest of the
file loads. Both are now stated as measured, with the macro permalinked
beside the struct.
The three paths were described as a preference that changes nothing but
reachability. They do not carry the same request: VS Code emits the
reasoning effort an operator picked only inside a thinking config, and
that config is built from budget capabilities `customendpoint` never
forwards — so on `messages`, the shipped default, the Thinking Effort
picker records a choice the wire never sees, while `chat-completions`
sends it whenever the model declares its levels, with no thinking gate.

Measured at the pinned commit: messagesApi.ts builds `effort` inside
`if (thinkingConfig)`; chatEndpoint.ts reads the budgets from
`supports.min_thinking_budget`/`max_thinking_budget`; and byokProvider's
`resolveModelInfo` sets streaming, tool_calls, vision, thinking,
adaptive_thinking and reasoning_effort — never the two budgets. The
comment beside the default, the type's own comment, and the operator's
hint in both locales now say this.

The element gate's justification was also half right: jq aborts on every
scalar but `null`, which it indexes to `null` and would carry through
the rewrite. Both halves say so now; both already refuse it.
… message

The model-listing catch is the one place in this module that puts an
upstream error message in the operator's log, and an upstream is not
ours — whatever it says may quote the request it was given. Both secrets
in hand are redacted there by hand, and nothing observed either: the
served body carries neither, so asserting on the body alone left the log
free to carry both.

A case now rejects the listing with a message quoting the raw key and
the lease token, and asserts the log holds the markers and neither
secret. Verified by removing each redaction in turn.
…g they match

The PowerShell case said it was byte-identical to the shell rendering
but never looked at it: the expectation was rebuilt with
`JSON.stringify`, the production serializer, which is exactly the
technique the shell case beside it rejects for following the
implementation through any change of key order. Neither test compared
the halves, so the claim rested on nothing.

The catalog is now spelled out as its sibling spells it out, and the two
prefixes are rendered from one input and their embedded catalogs
compared — which is what the comment always said. Verified by giving the
PowerShell arm its own serialization and watching the case fail.
…ng three decoders

Each half now has one execution shape — strip, merge, write — where it
had a classifier in front of the merge that tried to predict what jq,
`ConvertFrom-Json` on 5.1, and `ConvertFrom-Json` on 7.x would each do
with the operator's document.

Comments and trailing commas are the operator's own content, since both
editors read their file with a lenient parser. Refusing them meant a
document the editor loads was one this installer would not touch; they
are stripped now, and the merge sees plain JSON on both halves. Nothing
survives that a whole-file rewrite would have kept anyway.

The number machinery is gone with it. It existed to keep jq and
PowerShell from disagreeing about values at the edge of the double
range — 300- and 750-digit boundary constants, a digit comparator, a
magnitude walk — for files whose numbers are font sizes and token counts.
`NaN` and `Infinity` stay refused on both halves, because jq rewrites
those to values the operator never wrote and a person might paste one.

What remains on the PowerShell side is a strict-JSON check, because
neither decoder there is strict: both take a single-quoted string, an
unquoted key, a raw control character and an unterminated container, and
would write the document back as a different file where jq simply
refuses it.

Net: two shared modules and four call sites lose 393 lines and gain 263.
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.

1 participant