You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This commit was created on GitHub.com and signed with GitHub’s verified signature.
Added
services() wraps GET /localapi/v0/services — Tailscale Services (VIP services) visible to this node, keyed by svc: name, with ServiceDetails/ServiceAction models (addresses, ProtoPortRange text-form ports, open-set action slugs with JSON attribute maps; upstream GetServices, unannotated). Daemons without a netmap answer 503; daemons predating the endpoint surface .endpointUnavailable. CLI: tailscale-swift services (with --json).
shutdownTailscaled() wraps POST /localapi/v0/shutdown (upstream ShutdownTailscaled, annotated unstable) — graceful daemon exit. Destructive and documented as such; requires write access and the AllowTailscaledRestart policy (403 → .permissionDenied); wire-shape unit tests only, never exercised against live daemons.
Linux interface discovery: NetworkInterfaceDiscovery (and therefore StatusResponse.interfaceName/interfaceInfo) now works on Glibc platforms — the getifaddrs path was Darwin-gated, silently returning nil on Linux. The whole discovery test suite now runs on Linux CI as the regression assertion.
Model-conformance gate: Scripts/check-model-conformance.py enforces the API conventions mechanically in CI — every Codable struct must be Sendable + Equatable with a public init, and every raw-value wire enum must decode tolerantly. Its first run caught a real bug (see Fixed). "Tolerant" is proven, not assumed: a custom init(from:) only passes if it demonstrably falls back (?? .other / self = .other), so a future strict custom decoder fails the gate instead of sneaking past it (negative-tested), and the two decoded wire enums carry executable unknown-value decode tests.
Full upstream handler inventory: endpoints.json now records the 20 registered-but-unwrapped LocalAPI handlers (debug surface, dial, pprof, …) with their gates and reasons, and verify-upstream-maturity.py fails when any handler derivable from the pinned sources is missing from both lists, when an inventory entry goes stale, or when an unwrapped handler's gate drifts. This closes the drift-automation blind spot where a new upstream endpoint derived cleanly, was compared against nothing, and the weekly run reported clean (negative-tested for all three failure modes).
Upstream drift automation (issue draft 07): a weekly upstream-drift workflow re-derives maturity/gates/capability against current tailscale/tailscale main (verify-upstream-maturity.py --against-revision) and files/updates a re-pin issue describing exactly what moved. The pinned verification in regular CI is unchanged.
Strict DocC lane: PRs now build documentation with --warnings-as-errors (broken symbol links and malformed directives fail before deploy time), and the documentation-coverage table gates as regression floors (Types 90% / Members 68% / Globals 1% abstract coverage; measured 93/70/1.6) — the pre-1.0 audit raises them to 100%.
DocC tutorial: Build a Tailscale menu bar app — a step-by-step tutorial (observable model seeded from status(), live updates via watchIPNBus with reconnect, MenuBarExtra assembly) whose code mirrors the compiled Examples/Recipes menu-bar recipe.
Coverage floor ratcheted 70 → 85 (85.9% measured): the CI gate now surfaces the measured percentage plus the ten worst-covered files as check-run annotations, and that report drove the closing test batches — an edge-coverage suite (every error case's description/recovery/preview text, reconnect backoff growth, redaction edge shapes) and full-field encode/decode round-trips for the least-covered model files.
startFreshProfile(controlURL:authKey:) — the LocalAPI equivalent of tailscale up --login-server, and the missing piece of the auth lifecycle: logout() deletes the profile (control URL included), so a subsequent loginInteractive() dialed the default control plane — observed live in the hermetic lane, where headscale could never approve a login.tailscale.com URL. Seeds upstream's ipn.NewPrefs() defaults (verified at the pinned revision, field for field — Prefs gained noStatefulFiltering so the NewPrefs value actually reaches the wire instead of silently degrading subnet-router behavior) plus the control URL over start's UpdatePrefs, and rejects empty or non-HTTP(S) control URLs with .transport(.invalidURL) — upstream reads an empty control URL as "the default Tailscale control plane", which an explicit call to this method never means. The raw UpdatePrefs carrier is deliberately internal: this package's Prefs models a subset of ipn.Prefs, so re-encoding a fetched snapshot would silently zero unmodeled fields (services advertisement, stateful filtering, remote config, drive shares, …) — it stays scoped to the fresh-profile helper until Prefs is lossless (tracked for the pre-1.0 audit).
Scripted login-lifecycle test (the one open item carried from v0.9.0): the hermetic headscale lane now drives the interactive auth flow end to end — logout() → startFreshProfile(controlURL:) → loginInteractive() → BrowseToURL from the IPN bus (asserted to point at the seeded control server) → approval via the headscale CLI → Running. Triple-gated (TAILSCALE_INTEGRATION_LOGIN=1 on top of the integration/write gates) and run as a separate final step so the main suite always sees a logged-in daemon.
Fixed
IPNState (IPN bus backend state) decodes unknown values as a new .other case instead of failing the notification line — upstream adding a state no longer costs you the notify. Caught by the new model-conformance gate.