AGENTS.md forbids attribution trailers, but a rule an agent has to go and read loses to the instruction it is handed every turn. CLAUDE.md now imports AGENTS.md so it is always in context; a commit-msg hook (via make setup-hooks) refuses the trailers at commit time; a CodeRabbit pre-merge check flags a PR whose description or commits carry them. The check reports rather than blocks, since the repository keeps CodeRabbit's request-changes workflow off; turning that on is a separate, repository-wide decision.
Every curl invocation in the install script that follows redirects now passes
`--proto` and `--proto-redir` set to https only, so neither the initial request
nor any hop in the redirect chain can drop to plaintext. This matters most for
the macOS .pkg download, whose URL is itself the result of a redirect
resolution, and for the release tarballs that get moved into the install dir as
root.
The protocol set lives in a single `PROTO_HTTPS` variable rather than being
repeated at each call site, and every expansion is quoted — the variable holds
one option value, not a list of flags.
The two call sites without `-L` (the release metadata lookups) are left alone:
they do not follow redirects and their URLs are https literals.
Verified against every URL the script fetches on curl 7.29.0 (CentOS 7),
7.68.0, 7.76.1, 7.88.1 and 8.14.1; both options have existed since curl 7.20.0.
Plaintext http:// is refused on all of them.
GetSessionExpiresAt took the exclusive lock for a plain field read, so
every caller queued behind writers and behind each other. The Android
SessionMonitor polls it from the main thread, and in the captured ANR
that is exactly where the main thread was blocked while hundreds of
peer-list callbacks held or waited on the same mutex.
d.mux is already an RWMutex and the other getters use RLock; this brings
the deadline read in line with them.
The install script downloaded both the macOS .pkg and the release tarballs
into /tmp under fixed, predictable names, then passed those same paths to the
privileged install steps (`installer -pkg`, `mv` into the install dir).
/tmp is shared, so those fixed names can collide with entries created there
beforehand, and the privileged steps consume whatever the path resolves to.
Stage every download in a directory from `mktemp -d` instead: unpredictable
name, mode 0700, owned by the caller, created atomically. Extraction now
targets that directory (`tar -C`, `unzip -d`) rather than relying on `cd /tmp`,
and an EXIT trap removes it, so a failed run no longer leaves the archive and
the unpacked LICENSE/README behind in /tmp either.
TestAgentConfigAllowlistOfDeclaredModels still pointed its providers at api.openai.com and bedrock-runtime with a dummy key, so every save has been refused with "the provider rejected the credential" and the Agent Network E2E has been red on main since — both subtests, every scheduled run.
Every other suite already uses the mock vLLM upstream (harness.StartVLLM), which answers both the OpenAI (/v1/models) and the Bedrock (/inference-profiles) listing; this test does the same. What it checks — the allowlist advertising the provider's declared ids on GET /api/agent-network/agent-config — never depended on the vendor.
The agent network gateway service is private: agents reach it over the WireGuard tunnel, authorised by peer identity, with the cluster as its only target. Only a reverse proxy cluster with private capabilities can serve that, reported per cluster as the `private` capability — the same `supports_private` flag the dashboard gates NetBird-only services on.
A bootstrap could pin an account to a cluster without private capabilities, leaving an immutable dead gateway. Both bootstrap paths now validate the picked cluster: one the account can see must have a connected proxy reporting the capability. Shared and account-owned clusters qualify alike. Known-ness comes from proxy rows, not heartbeat freshness, so a cluster without the capability stays refused while merely offline. A hostname no proxy has declared stays pinnable (address-first). Identity is compared case-insensitively over the account's cluster list.
An agent network bootstrap stores its cluster as proxy_address, which selects the proxy that serves the endpoint. An account-scoped proxy only receives its own account's mappings, so a pin onto a host another account's proxy declares can never be served, and the endpoint is immutable — a dead gateway until the account deletes its settings. Nothing refused that pin; the domain unique index only arbitrates between endpoints.
Both bootstrap paths now refuse, before the insert, a host that another account's proxy declares, a host another account has labeled pins beneath (self-addressed path), or a hostname that is another account's endpoint (labeled path).
Shared clusters are unaffected: shared proxies are never foreign, and labeled pins under one cluster are never asked about, so any number of accounts still pin beneath eu.proxy.netbird.io. Registration is deliberately unchanged — refusing a proxy for another account's pin would let a pin lock a tenant out after the reaper drops its rows.
Management / Unit (amd64, mysql) hit the 20 minute go test budget on #7516. The package was not hung: each of the 133 test store creations in management/server paid about 1.6s on MySQL for CREATE DATABASE, the pre-migrations, a 40-table AutoMigrate and the post-migrations, which puts the package at 10 minutes on a healthy runner and over the budget on a slow one.
The migration now runs once per test binary into a template database and each test database is cloned from it, with CREATE DATABASE ... TEMPLATE on Postgres and a replay of SHOW CREATE TABLE on MySQL. The MySQL test container also drops the binary log, doublewrite buffer and per-commit redo fsync. Two goroutine leaks in the test helpers are fixed.
tools/gotestsummary turns the go test -json stream into a readable log, and the Management unit and integration jobs now pipe through it, so a timeout names the tests still running. On MySQL, management/server went from 10m16s to 6m36s.
* [client] Fix the Windows tray deadlock on re-entrant window creation
The Wails systray runs the left-click handler synchronously inside the
tray window procedure, and creating a window on a running app pumps a
nested Win32 message loop while WebView2 initialises. ensureWindow held
the non-reentrant createMu across that creation, so the second button-up
of a double click re-entered ShowWindow from the pump and blocked the
main thread on its own lock. A goroutine holding createMu while the main
thread pumped, and the Open* dialogs holding mu across NewWithOptions,
Show, Hide and InvokeSync, exposed the same inversion.
WindowManager now serialises creation with a per-slot creating flag and
queues the callers' operations until the window exists, and no Wails call
runs while mu is held. The tray click and second-instance handlers call
ShowWindow off the message loop.
* [client] Serialize window operations while a slot is being created
Callers arriving after the window is published but before the creator
has drained the queue took the existing-window fast path and could run
ahead of older queued operations, so a newer SetURL could be overwritten
by an older one. withWindow now queues every caller while the creating
flag is set and clears the flag only once the queue is seen empty under
the lock.
A factory panic or a nil window left the creating flag set and the slot
dead; creation and drain now reset that state on early exit.
hideOtherWindows records the windows it hid only when no restore ran
in between, tracked by a generation counter, and re-shows them otherwise,
so a restore racing the hide cannot strand hidden windows.
* [misc] Run the client/ui subpackage tests in CI
The three test workflows filtered the package list with a `/client/ui`
prefix match, which dropped the subpackages along with the package that
cannot compile without a frontend build. `services`, `preferences`,
`i18n` and `authsession` all carry Go-side unit tests that never ran,
including the window manager re-entrancy regression test.
Anchor the pattern so only `client/ui` itself is excluded. The linux leg
keeps the prefix match on 386, where only the 64-bit gtk4/webkitgtk dev
packages are installed and the Wails application package would fail to
link, and the alpine container job keeps it for the same reason.
* [misc] Run the client/ui subpackage tests on a gtk4 4.10 runner
The previous commit let the subpackages into the linux client job, where
client/ui/services failed to build: the wails runtime's linux cgo layer
uses GtkFileDialog, which arrived in gtk4 4.10, and the job's ubuntu-22.04
runner ships 4.6.
Move them to their own job pinned to ubuntu-24.04 and restore the linux
client job's original exclusion, leaving the 386 and privileged legs on
the runner they have used since 2024. The new job needs no build cache,
sudo or privileged tag, so it stays a few seconds long.
Darwin and Windows keep the anchored pattern from the previous commit and
already run these tests green, including the window manager re-entrancy
regression test on the platform the deadlock was reported on.
* [client] Defer a window close that lands while the window is still being created
WindowManager publishes a dialog's slot only after the factory returns,
and on Windows the factory blocks in the WebView2 embed pump. A Close*
arriving in that gap found a nil slot and returned without doing
anything, so the dialog appeared afterwards for a flow that had already
been cancelled. The pre-fix Open* dialog functions held mu across the
whole creation, which blocked a concurrent Close* until the slot was
set; removing that lock hold reopened this gap.
Close* now goes through closeWindow: while the slot is being created it
records a closer in pendingClose, and finishCreation runs that closer
before any queued operation, so a window that is going away is never
shown and Wails never sees a Show on a destroyed window, which would
recreate it. Ops queued behind a close are dropped; windowOp carries no
factory, so they cannot be replayed into a new creation, and the
frontend callers reissue on the next state change.
The browser-login slot uses the same restoring closer from both
CloseBrowserLogin and CloseRenewFlow, since the popup's WindowClosing
hook only restores on a user close. Where two closers race one
creation the first registered wins, so a later caller cannot replace a
restoring closer with one that does not restore.
`validateDeleteGroup` already refuses to delete a group that is still used by routes, policies, nameservers, setup keys, users, network routers, reverse proxy services, and agent network policies. Account-level agent network budget rules also store group IDs in `TargetGroups`, but that check was missing.
Deleting such a group left a dangling ID on the budget rule. `budgetRuleApplies` then never matched callers by group, so the spend cap silently stopped applying.
This adds `isGroupLinkedToAgentNetworkBudgetRule` and uses it in `validateDeleteGroup`, matching the existing helpers.
The minio/minio repository is no longer on Docker Hub: the repo and the
pinned tag both return 404 and anonymous pulls are refused, so
Test_S3HandlerGetUploadURL fails on every Linux CI run with "pull access
denied for minio/minio".
The same release is published at quay.io/minio/minio, digest
sha256:a1ea29fa28355559ef137d71fc570e508a214ec84ff8083e39bc5428980b015e,
so the testcontainers request points there and keeps the pinned tag.
Prevent unvalidated registrations from reserving domain names indefinitely.
Give pending registrations a 48-hour validation window and clean up expired entries at startup and every 60 minutes. Emit CustomDomainValidationExpired for each deletion and preserve registrations referenced by services.
Reject validation after expiry and prevent concurrent validation from recreating deleted registrations. Normalize domain names with the shared parser before registration.
Migrate existing pending registrations to receive a fresh 48-hour validation window.
Read the relay instance URL and IP atomically to prevent reconnects from mixing values from different connections. Extend existing connection and offer/answer logs with relay URLs and IPs to help trace mismatched advertisements.
* [client] Track the WireGuard device on the engine as a lock-free handle
Add an atomic handle on the wg device next to wgInterface, stored once the
interface is up and cleared when it is closed. Nothing reads it yet, so this
is a pure addition with no behavior change; it exists so the next commit can
reach the device without taking syncMsgMux.
* [client] Retune the WireGuard buffer pool without the engine lock
SetPerformance took syncMsgMux before reaching the device. That lock is held
by handleSync while it adds and removes peers, and peer removal is exactly
what blocks when a device's buffer pool is exhausted: Peer.Stop waits on a
keepalive timer callback that is itself parked in WaitPool.Get. Raising the
cap is the way out of that state, so the call must not queue behind the lock
the stall is holding.
Read the device through the atomic handle instead. Device.SetPreallocatedBuffersPerPool
takes the pool's own lock and broadcasts, so the waiters wake up.
* [proxy] Extract the buffer-cap apply loop out of the perf handler
Pure move: the loop over the registered clients becomes applyBufferCap, with
the same sequential behavior and the same return values. Split out so the next
commit can change how it iterates without the diff also carrying the move.
* [proxy] Bound the perf endpoint so one wedged client cannot hold it
The apply loop was sequential and unbounded. embed.Client.SetPerformance goes
through the client lock, which Start holds for the whole of a startup, so a
single account that is busy or wedged delayed the new buffer cap for every
other account on the node -- on the endpoint whose whole purpose is to
un-wedge a node.
Apply to all clients concurrently and give the whole call a 5s budget.
Accounts that do not answer in time are reported in "failed" instead of
blocking the response.
* [client] Drop the device handle before closing the interface
close() cleared the atomic handle only after wgInterface.Close() returned, so a
concurrent SetPerformance could still load it, retune a device that is being
torn down, and report the change as applied for an engine that has stopped.
Clear it first, so the window closes before the teardown begins.
Reported by cubic on PR #7452.
* [proxy] Put the per-client retune behind a field
Pure refactor: applyBufferCap calls h.setPerformance instead of the client
method directly, and NewHandler wires it to setClientPerformance. Same call,
same behavior; the seam is what lets the next two commits be tested without a
live embedded client.
* [proxy] Do not report a finished retune as timed out
When the deadline fires, select chooses at random among the ready cases, so a
result already sitting in the buffered channel could be skipped and its account
reported as timed out even though the cap had been applied. Drain what is
buffered before declaring the rest pending.
Reported by cubic on PR #7452.
* [proxy] Keep one retune per account in flight
The 5s budget bounds how long the endpoint waits, not the work: SetPerformance
goes through the embedded client's lock, and on a wedged account Stop holds that
lock forever, so every retry left one more goroutine parked there.
Route each account through a single worker. A request that finds one already
running takes its result if it has landed, and otherwise reports the account
under "in_flight" instead of starting a second attempt. One stuck account now
costs one goroutine, no matter how often the endpoint is called.
Reported by CodeRabbit and cubic on PR #7452.
* [proxy] Make the retune budget a var
Pure refactor: perfApplyTimeout becomes a var so a test can shorten it instead
of waiting five seconds. Same value, same behavior in production.
* [proxy] Extract the buffered-result drain
Pure refactor: the loop that empties the results channel when the deadline
fires becomes collectBuffered. Same behavior; split out so it can be tested
on its own, which the inline version could not be without racing the deadline.
* [proxy] Cover the retune single-flight and the deadline drain
TestApplyBufferCapSingleFlightPerAccount fails without the worker registry:
five calls against a client stuck in its own lock start five blocked workers
instead of one.
TestCollectBufferedCountsResultsReadyAtTheDeadline pins the drain helper's
contract - buffered results counted, errors recorded, only unanswered accounts
left pending. It drives collectBuffered directly: through applyBufferCap the
two select cases race by construction, so an end-to-end version of it would
pass on the unfixed code about half the time.
* [proxy] Keep the worker alongside each pending account
Pure refactor: the pending set becomes a map to the account's worker instead of
an empty struct. Same membership and same behavior; the next commit needs the
worker to resolve an account whose result has not reached the channel yet.
* [proxy] Publish a retune result before releasing its slot
The worker sent its result last, after taking perfMu to remove itself from the
registry. That lock is taken once per account by every caller walking the fleet,
so a worker that finished on time could queue behind an apply over thousands of
accounts and land after the deadline. Send first, deregister after.
Reported by cubic on PR #7452.
* [proxy] Read the worker, not the clock, for a finished retune
Publishing earlier only narrows the window: a client that answers just before
the deadline can still be reported as timed out. At the deadline the workers
themselves are authoritative - a closed done channel means the retune finished
and w.err carries its outcome, ordered by the close. Consult them instead of
declaring every pending account timed out, and keep the timeout label for the
ones actually still running.
Reported by cubic on PR #7452.
* [proxy] Cover the finished-worker resolution at the deadline
Fails on the previous behavior with "applied = 0, want 1": every pending
account was labelled a timeout, including the one whose retune had already
completed.
* [client] Add a release-wired rootless UBI image variant
* [client] Add ARM64 to the rootless UBI image
* [client] Express license output validation as a guard