* backup: pin the #880 subscription sign-in policy with a test
A backup must not carry a subscription's sign-in: it is the sign-in of an
agent on one machine, so a bundle that carried one would sign the agent in
on the machine it was put on. #880 asked for that and was closed as
intentional. Nothing pinned it, so this adds the test that does.
Both stores, both credential policies, and the asymmetry the report tripped
over together: a subscription moved onto its plugin still has a row in
providers.json, and the row travels so the receiving machine offers the
subscription at all — the credential behind it does not. A user-typed key
is the control, so the policy cannot be read as "nothing credential-shaped
syncs", and Restore is checked for leaving this machine's sign-ins alone.
scopedKeys has no case for "providers" and falls through to the whole-
bundle bit. That fall-through is deliberately not pinned: scopedKeys is
called once, with "settings", so there is nothing observable to assert,
and a test that did would go red for a good reason the day the case is
added. The tests around it pass either way; the reason is written down.
Co-authored-by: multica-agent <github@multica.ai>
* backup: say what the #880 sign-in tests actually guarantee
Review of 974ca819 found no blocking problem but two comments that
pinned more than they could back up.
carried() claimed that a credential absent from a bundle's marshalled
bytes cannot be inside a sealed backup either. That is false, and it was
measured rather than argued: Seal marshals the whole Bundle in one
json.Marshal, but a []byte field is base64 in those bytes, and a Collect
that copies plugin-auth.json into b.Icons["auth-state.bin"] seals the
whole plugin store while every one of these tests stays green. The probe
is still the right one for what the policy is about, which is a
credential arriving as plain text, so the comment now says that and
records the two limits that keep it honest: a literal check misses JSON
escaping, and a Seal that grew a wire type of its own would decouple
carried() from the bytes it claims to describe.
TestRestoreLeavesLocalSignInsAlone compared the two sign-in stores byte
for byte. Byte equality is file identity, not policy, and re-indenting
logins.json — same accounts, same credentials, same counts — turned it
red. It now compares the sign-ins: who is signed in, and the credential
behind each, canonicalised, in a fixed order. An incidental reformat
cannot fail it, and a restore that really did take the sign-ins over
still does, which a probe that overwrote both stores confirms.
Also notes that TestCollectCarriesTheRowOfAMovedSubscription does not
compare Headers, and where that coverage does live.
Co-authored-by: multica-agent <github@multica.ai>
* backup: apply the #901 maintainer's four review changes
Drops the two line-number references in favour of the function names
Seal and scopedKeys("settings"), which do not go stale when the file
moves: Seal does the single json.Marshal of the Bundle (backup.go:252)
and the scopedKeys("settings") call is inside Restore (backup.go:401),
so both attributions check out.
Renames the four plugin fixtures that named a vendor they never used.
fixtureGrok* are written under movedProviderID ("opencode-zen") and
fixtureGeminiKey under "opencode-copilot"; both are OpenCode plugin
providers, so the names now say plugin. The built-in codex and claude
fixtures are left alone — those agents really do own those logins.
Removes TestScopedKeysFallsBackToTheWholeBundleBit and
TestScopedKeysPrefersAPartsOwnMarker with setScopedMarker. They are
about part-scoped key markers, not about the sign-in policy this file
states, and the file header no longer leans on them.
Closes the Icons blind spot instead of documenting it away. carried()
only ever saw plain text, and Bundle.Icons is map[string][]byte, so a
Collect copying plugin-auth.json into b.Icons["auth-state.bin"] put
the whole plugin store in a sealed backup with every assertion green.
whereSignInIs searches the decoded value of every Icons entry as well
as the marshalled bytes, and names the layer in the failure. Both the
keyless and keyed subtests now go red under that mutation, where they
previously passed.
Test-only: one file, no production code.
---------
Co-authored-by: multica-agent <github@multica.ai>
* davsync: scope provider and library credentials during partial sync
* davsync: a sync writes which part carries its credentials, and no blank clears one
yetone's review of the first cut found two losses the per-part markers
caused, and both are fixed by making the markers say exactly which part
they belong to:
1. A keyed computer's library-only upload cleared a keyless computer's
private MCP token. The blanks in the uploader's library are redactions
it downloaded and never held a value for, not clears it made; take now
keeps the server's value for an empty secret whatever the policy is, so
only a server taken out here is gone. The test that had the loss as
its expected now expects the token kept.
2. Mixed versions dropped the remote's provider keys. A keyed upload
didn't write the whole-bundle Keys bit, and an older keyless magpie,
which reads no per-part marker, uploaded its own providers over the
server's with nothing to keep. take sets Keys for the providers' again.
The per-part markers are three-state now: a whole collect and a magpie
before them write none, and the whole-bundle bit says it then; a sync
writes one for every part it merges — true when the upload carried that
part's credentials, false when it didn't — and that bit, not Keys, is
what a reader goes by. That is what keeps 1 and 2 apart: the providers'
whole-bundle bit is set for older readers, while the settings' and the
library's own false is what tells a new reader they came without keys and
their local values stay. take marks its merged bundle as a sync one
(BundleVersion), so a sync's readers never fall back to Keys for a part
it wrote.
The two regression tests of the first cut that encoded the losses as
expected are updated: library/clear keeps the server's secret, and the
keyed providers upload writes the whole-bundle bit while leaving the
other parts' contents alone.
A remote that main already flipped to Keys=true over redacted settings or
library isn't repaired by this: the fix keeps new syncs from flipping it,
and a keyed computer syncing those parts repairs them by writing them
whole.
Validation on Windows: go test ./internal/... under a temp HOME passes
whole; the davsync, backup, access and settings packages pass with -race;
stashing the change fails TestTakeScopedPartCredentials (8 cases), the
scoped-sync tests that cover both losses, and the balance-token and
restore-scope tests that read the markers. go vet ./internal/... and the
darwin, GOOS=linux and GOOS=windows builds pass.
* davsync: preserve local secrets when applying partial uploads
* Add German translation and harden paths against unset env vars
Add a German locale to the GUI, tray menu, usage alerts and website home page, and refuse to start when the home folder is unset or relative so nothing is written under the working folder; the pi plugin no longer adopts the user's PI_CODING_AGENT_DIR, and an inherited APPIMAGE no longer selects portable mode. Scripts refuse to run from the wrong folder or empty a shared out folder, and a new internal/testenv sandboxes every test package from the user's real files, which also fixes the Claude picker and Termux installer tests on Windows.
* Makefile: know magpie's folder by its go.mod, so paths with spaces work
The check compared $(CURDIR) with $(dir $(abspath …)), which make splits on spaces, so a checkout under 'My Projects' or 'John Doe' refused every target. It now reads go.mod relative to the working folder and wants magpie's module there: make -C and a symlinked Makefile work, make -f from another folder still stops before any recipe runs.
* i18n: German for the strings main added since, with a parity test
75 strings main added (group order, patterns, slow-start wait, usage caps, RTK savings …) get their German, and two that main dropped go. gui-de.test.cjs checks, as gui-ja does, that de has every zh string with its placeholders and tags, and that the window reads in German when picked or on a German system.
* appdir: ignore relative folder variables when reading, keep them for children
CheckEnv no longer unsets anything. magpie reads the XDG folders, APPDATA/LOCALAPPDATA and every agent's folder variable through appdir.Getenv/LookupEnv, which pass over a relative value; the environment stays as it is, so the agents magpie starts get the variable as the user set it. That covers what proc.UserPath lends from the login shell too, which is set after main's check and reached the desktop app unchecked. On Windows a path rooted on the current drive (\Users\x, Git Bash's /c/x) counts as usable, and magpie version and help answer without a home, before run makes any folder.
* Sandbox the tests main added since
internal/wslrun gets testenv's TestMain, and four tests main added that set HOME alone set their home with testenv.SetHome, so on Windows they don't read the runner's profile through USERPROFILE.
* agentenv: PI_CONFIG_DIR and OPENCODE_DB take relative names on purpose
omp puts $PI_CONFIG_DIR under the home and OpenCode a relative $OPENCODE_DB in its data folder, so neither depends on the working folder: appdir no longer passes them over.
* i18n: German for the Library's multi-select and System One's decision API
The 19 strings main added since (#791's skill multi-select, the decision API snippets) get their German; the one main replaced (four APIs → five APIs) goes.
* appdir: tidy the import into magpie's group; say what Go makes of /c/x
The appdir import joins the group of magpie's own imports where a file has one. rooted's comment now says that Go takes a Git Bash path like /c/x as \c\x on the current drive, not C:\x.
The Routing page's group list followed the order the groups were made in, found ones after, so a group used every day couldn't be put first, though /v1/models lists the groups in that order and agents' pickers (Claude Desktop's 3P list among them) show it and some clients probe its first model. A group row's logos are now its handle, as an agent row's logo is (#57): dragged (dragRows, the Agents page's sorter) it moves the row, clicked, or the row right-clicked, it opens Move up and Move down (openRowMenu, with the Agents menu's arrows, now MOVE_UP/MOVE_DOWN), and Alt+↑/↓ moves it from the keyboard, which stays on it. The grip in the row's margin is drawn only while the row is under the pointer, dragged, or its handle is focused or its menu open, as on the Agents page. The list is drawn in its new order at once and put back if the save fails; a later move wins over an earlier one still saving.
The order is kept in providers.json as groupOrder (provider.SetGroupOrder, POST /api/groups/arrange), beside the providers' own order (#499), and applied in groupsIn, which every list of the groups is made from: the Routing page, Catalog and so /v1/models, magpie group ls and the TUI's routing page. A found group, which isn't stored until changed, can be put among the user's this way, which reordering the groups array couldn't do. A group not named (one made or found after) goes last; a renamed one keeps its place. Backup and sync carry the order as they do the providers' (MirrorGroupOrder).
TestGroupOrder (Groups and the catalog follow SetGroupOrder, providers.json has it, a new group last, a rename in place, bad orders refused, a synced order first) fails on the old code. group-arrange.test.cjs (WebKit, en and zh): the grip hidden with the pointer away and shown on the hovered row only, in its margin; right-click Move down with Move up off on the first; Alt+↓ and Alt+↑ with focus kept; the handle's click opens the menu, not the editor; a drag by the grip to the top; each posting every group's id with the removed one last; no click moving the page. It fails on the old code. agent-grip, click-scroll, account-arrange, usage-arrange, provider-order and the group-*/routing-* GUI tests pass in WebKit, but for group-from-model-add's 600x420 cases, which fail on main as well. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass.
* davsync: a damaged file on the server is rebuilt, not failed on forever
When the server's backup can't be read as a backup at all — a write a
relay or tunnel cut short, or one damaged since — syncOnce returned the
error and stopped. This computer's own setup is fine, so it could never
be pushed over the unreadable file to fix it, and every sync after
failed the same way: the only way out was deleting the file by hand.
(#531 detects a short write as it happens, but a file already damaged —
by an older magpie without that check, or by anything since — stayed
stuck on "not a magpie backup".)
The rebuild merges; it never pushes this computer's bundle as it is. A
sync that finds the file unreadable falls back to the last good copy of
the server's file it read whole — its cache, when that copy is
trustworthy — and runs the normal three-way merge over it, so another
computer's newer setup the damaged write swallowed survives. Three
guards say when a computer may do that, and any one missing leaves the
error standing: it has synced before (a fresh computer has no last-good
copy and no right to rebuild what it never saw); the cached copy is the
version the damage replaced (the damaged body is a prefix of it, which
is what a cut-short write leaves — and what an out-of-date computer's
older copy is not); and the body really begins like a magpie backup, so
a server answering with a page (a captive portal) is reported, never
rebuilt over. backup.ErrCorrupt — the message those two damaged checks
already gave — marks that case, told from a wrong passphrase and from a
newer magpie's file, both still refused. The push carries the damaged
file's ETag, so it stays checked against the server, and it happens
even when the merge matches the cached copy: the server's file is the
damaged one and must be replaced. The unreadable body is kept aside
under the sync folder, the newest few, so a server that always answers
the same unreadable way doesn't pile one up per tick.
Tests: TestSyncRebuildsFromCachedCopy pins the self-heal (the server
ends whole, the damaged body kept aside); TestSyncFreshComputerDoesNot
Rebuild and TestSyncOutOfDateComputerDoesNotRebuild pin the two
data-loss cases — the out-of-date one fails without the prefix guard
(verified by mutation); TestSyncPageIsNotDamage pins that a 200 HTML
page is reported, not pushed over; the newer-magpie refusal is guarded
against regression. go vet, and go test -tags nogui ./internal/davsync
./internal/backup under a temp HOME pass, as do the linux and windows
builds.
* davsync: a body cut inside the sealed file's constant head rebuilds nothing (yetone on #607)
The prefix check alone let an out-of-date computer rebuild: Seal writes
the envelope with MarshalIndent, so every version begins with the same
constant head up to the salt, and a body cut inside it (40 or 90 bytes)
is a prefix of every version — including an older cached copy — so that
copy's computer passed the guard and pushed its merge over a newer one's
data. The guard now also requires the damaged body to reach past the
random fields, to the "data" key that follows the salt and nonce, which
ties it to the one version it was cut from. A body cut before them is
attributable to no version, so it stays an error for every computer,
even the one that saw the good version — fail loud rather than rebuild
blind; a relay cutting a real write cuts deep into the data, so legit
rebuilds still go. TestSyncOutOfDateComputerDoesNotRebuild now cuts at
half, 90 and 40 bytes; the two short cuts fail against the old guard
(the out-of-date computer rebuilds and kimi is lost).
Co-Authored-By: Claude Code <noreply@anthropic.com>
---------
Co-authored-by: Claude Code <noreply@anthropic.com>
* usage: add opt-in OTLP HTTP metadata export
Export gateway traces and optional duration/token histograms asynchronously with a bounded queue, retries, proxy support and a bounded shutdown. Add bilingual Settings controls and MAGPIE_OTEL_* overrides. Keep prompts, replies, sessions and provider credentials local; use the same OTLP path for Langfuse.
Validated: full Go tests (nogui), usage race tests, focused go vet, and Chromium/WebKit settings tests in English and Chinese.
Refs #213
* otel: protect collector credentials in backups and settings
Remove all OTLP headers from keyless backups and restrict both new and existing settings files to 0600 before saving. Clarify SIGTERM export behavior. Addresses the two blocking review findings in #412.
* otel: preserve local credentials and read-only settings on restore
---------
Co-authored-by: raper <raper@raperdeMacBook-Air.local>
* feat(gui): show and filter usage for each API key
* feat(gateway): issue per-user caller keys and attribute usage
* feat(gui): manage gateway users and filter caller-key usage
* test: verify caller-key failover and document user management
* refactor(gateway): use named caller keys without user accounts
* feat(gui): manage named caller keys with existing account styles
* fix: remove user breakdown from CLI and leftover gateway styling
* fix(usage): drop stale user field from request response
* feat(gateway): authenticate LAN calls with named caller keys
* fix(gateway): preserve LAN key rotation in the named caller store
* fix(gui): retain native LAN key copy and rotation controls
* fix(gui): inherit the global theme for API key inputs
* fix(usage): show client API keys using the existing terminology
* fix(gateway): rotate named keys without changing usage identity
* fix(gui): unify gateway keys and selectable connection examples
Keep sharing controls in Settings and manage credentials only in Gateway. Refresh the named-key list on navigation, ignore stale reads after key actions, and generate all API examples from the selected address and enabled key.
* fix(gui): keep caller key names literal in connection menus
* fix(access): use Magpie as default key and drop user migration
* refactor(gui): name caller key source and tests consistently
* refactor(usage): split provider-key accounting out of caller-key PR
* fix(gateway): preserve LAN compatibility and add headless key management
* fix(gui): clarify gateway keys and confirm destructive actions
* test(gateway): guard caller attribution under 120 concurrent requests
* fix(gateway): keep open access, tolerate migration errors and revoke legacy keys
* fix(backup): preserve named gateway keys and honor no-keys restores
* fix(gui): keep local API-key semantics and disambiguate shared credentials
* docs(gateway): keep review scope focused and append browser test documentation
* fix(backup): retain existing default keys when old backups lack their store
* fix(gateway): attribute video creation usage to the caller key
* fix(access): explain default key revocation blocked by read-only settings
* test(usage): preserve provider and caller identities after rebase
* test(gui): reconcile caller key fixtures with merged main
* docs(usage): clarify caller coverage and independent provider accounting
* fix(gui): initialize fast members for model-shortcut groups
Latest main introduced fast-member arrays but omitted the model shortcut's draft. The existing unlisted-models browser regression fails unchanged on main. Initialize fast to an empty list and assert a standard-speed group is saved, without relaxing existing assertions.
* fix(access): retain legacy LAN access when the key store cannot be created
Allow only the configured legacy credential before either migration marker exists. Refuse revoked mirrors, disabled matches and unreadable stores; keep the fallback's caller identity stable when migration later succeeds. Cover read-only configuration directories, remote credential transports and revocation.
* fix(backup): save settings before replacing gateway credentials
Leave the key store untouched when settings validation or writes fail. Restore the previous settings association when the key store refuses its write, and report a rollback failure separately. Cover invalid settings and read-only settings/store failures.
* fix(gui): reset missing caller filters and focus key controls without scrolling
* test(provider): synchronize Zhipu quota fixture observations