Comments, docstrings and a few strings cited plan numbers ("plan 29
#9", "plan 86 §B2") from a git-ignored folder, so they pointed at
nothing; one reached users in a bottleneck hint. Code and frontend
strings are unchanged apart from those citations (checked by AST and
token comparison). A vhost still carrying the old asset comment reads
as undrifted, and a guard test keeps new citations out.
Panel side of the managed profile: the panel receives a presentation
profile from Cloud over the existing command channel, stores it,
acknowledges it, and hides or simplifies fleet-owner-only surfaces
until the profile is lifted or the link is revoked.
- ui.managed_profile command handler with managed-profile.json
storage, audit entries, and ack-by-output; profile is cleared on
disconnect and on consent revoke
- GET /api/v1/connect/managed-profile for the local UI
- ManagedProfileContext with capability-driven filtering of sidebar
items, command palette actions, and sidebar settings; ManagedCard
explains the managed state
- In-page collapses for Templates deploy, About self-update, and
Backup schedules/storage (restore stays available)
- Settings gains a ServerKit Cloud tab showing link and profile state
Local verification: 153 backend tests pass, 198 frontend node tests
pass, lint clean, build ok. No live policy has been pushed to a real
panel yet.
Four things the guards and the review caught, all of them real:
- app/api/connect.py: GET /connect/status carried a bare @jwt_required(),
which the jwt-only census forbids for a new route (609 vs a ceiling of
608). It is an admin read of pairing state, so it takes @admin_required
— the same door the rest of the admin surface uses, API-key capable by
way of auth_required() like its neighbours.
- connect_policy._systemctl ran systemctl through raw subprocess, one over
the raw-subprocess ceiling. run_privileged() adds sudo when the panel is
not root, so the fail2ban repair now works off a packaged install
instead of reporting a permission error nobody can act on.
- connect_client.hello_headers assumed the end-to-end key fields were
always there. build_hello() drops them rather than fail the connection,
so a panel that could not read its X25519 key would KeyError on the
long-poll fallback — exactly the path taken when things are already bad.
- status() called resolve_cloud_url(), which raises on a URL with no
scheme. A typo in SERVERKIT_CLOUD_URL should surface when you pair, not
turn `serverkit connect status` and the Settings panel into an error.
Both connect threads are now classified in the thread-ownership registry:
the relay loop is a process loop, and a signed command is bounded delivery
whose record lives in the control plane.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018q56AfqpgKjio4n9KYubvZ
- app/services/connect_keys.py: Ed25519 device keypair (0600, config dir),
fingerprint = sha256(raw pubkey)[:16] shown in groups of four
- app/services/connect_client.py: enroll/poll (signed PoP)/rotate against
the control plane, writes connect.json (0600) on claim;
status()/disconnect(); control-plane URL from --cloud / SERVERKIT_CLOUD_URL / https://app.serverkit.ai
- cli.py: 'connect' group (pair, status, disconnect)
- app/api/connect.py + core_blueprints.py: GET /api/v1/connect/status (JWT)
- tests/test_core_blueprint_registry.py: registry guard constants for the
new blueprint