main had fallen a full feature generation behind: 13739 lines and 29 files
that main did not contain at all (the whole Lua plugin mechanism, the billing
package, stats_period, plugins_api, docs/plugins.md). v1.7.x contains
everything main has and nothing main lacks, so this is a one-way merge rather
than two divergent lines.
Eight conflicts, each read before resolving — the script that does it lives at
.probe/resolve_merge_conflicts.py and aborts rather than guess:
* cmd/gui/package.json, cmd/gui/package-lock.json — main already carries 1.8.0
(4b37e1a); the release line carries 1.7.6. Kept 1.8.0. Taking 1.7.6 would
ship tag v1.8.0 next to a manifest claiming 1.7.6.
* packaging/config.example.yaml (2 hunks) — release side is a superset both
times: it documents the `auto:` field, and it replaces the older one-line
runtime_file note with a paragraph that also states where templates and
deleted-markers live and warns that the AUTO chain, gateway keys and sources
are in config.yaml. HEAD's wording is strictly less.
* internal/config/config.go, internal/core/core.go, internal/gateway/api.go,
internal/gateway/apiv1.go, internal/gateway/stats.go (6 hunks) — HEAD is
zero-length in every one and the release side is the new code: BillingDSL
types, applyBillingDSL, the pointer-semantics source upsert, the
optional_fields doc string, the AUTO chain-walk field. The resolver asserts
the empty-HEAD property instead of assuming it.
Verified before committing: no residual conflict markers, both manifests read
1.8.0, `go build -tags luajit ./...` clean, `go test -tags luajit ./...` all nine
packages pass.
The auth rule calls readAdminKey() on every outbound request so injection
never depends on ordering. Once the sealed-config path shells out to the
core, that turns each request into a process spawn: 200 simulated requests
took 1012ms and launched 200 cores.
Cache the unsealed key against config.yaml's mtime. Editing the config still
invalidates it, which is what the auth rule actually needs -- the port
rewrite, the first write, and a user edit all change mtime. Measured: 200
requests now cost 10ms and one spawn.
Only a successful unseal is cached. Caching a failure would pin an empty key
until the config next changes, turning a momentary spawn error into a locked
out user.
The desktop build authenticates the embedded core by reading the admin key
out of config.yaml with a regex and injecting it as a gw_key cookie. The core
seals credentials at rest (enc:v1:...), so from the second start onward that
regex yields ciphertext, the cookie is worthless, and the app asks the user for
a key they never set. The key is generated and hidden by the app itself.
Reproduced end to end: first start writes a plaintext profile, the core seals
it, every later start reads back "enc:v1:..." and falls through to the login
prompt.
- when the stored value is sealed, ask the core to unseal it via
-show-secrets, which only reads, prints and exits. Reimplementing the core's
AEAD in JS would be a second source of truth for its key format.
- cwd must be the profile dir. The core locates master.key relative to the
config's runtime_file, so a call made from anywhere else has it generate a
second master key in the CWD and then fail to decrypt ("master key changed?").
Electron's CWD is not the profile dir, so without this the desktop build
cannot read its own key even after unsealing is wired up.
- the loose regex is kept as a fallback so a future change to the -show-secrets
output degrades to a login prompt rather than to a wrong credential.
Verified: plaintext start -> core seals -> restart recovers the same key, with
the core running the whole time. Dropping cwd:PROFILE_DIR makes the unseal fail
and leaves a stray master.key in the CWD, so the cwd argument is load-bearing
rather than tidiness.
Two defects on the admin source write path, both found while adding a model
to a live source by hand.
PUT /api/sources/{name} was advertised in the API index but never
implemented — handleSourcesAPI only switched on GET/POST/DELETE, so the
documented update verb answered 405 while the POST upsert behind it worked.
POST is an upsert that replaces the whole source, so a partial edit that did
not carry api_key persisted an empty or placeholder credential. The source
kept its name, base_url and models, the write returned 200, and the source
then answered 401 on the next request — long after the writing script exited
0. The WebUI had been routing around this by loading the real key through
?reveal=credentials; any script or partial update went straight into it.
- implement PUT, taking the name from the path and rejecting a body name
that disagrees rather than silently resolving to one of them
- inherit the stored credential when api_key is omitted or sent as the
literal "__KEEP__"; an explicit new key still rotates
- an empty api_key on a source that does not exist yet stays empty, since
credential-less local upstreams are legitimate
- add model_ids, an additive shorthand, so "add these models" never has to
read and echo the existing list back
- align the API index with the implementation
The model_ids merge had a first cut that dropped the existing list when the
request carried no models field; TestSourceModelIDsIsAdditive caught it.
Verified by mutation: removing PUT turns three tests red, flattening
resolveAPIKey into a pass-through turns TestSourceUpsertKeepsAPIKey red
on both subtests, and making model_ids replace instead of merge turns
TestSourceModelIDsIsAdditive red.