Commit Graph
775 Commits
Author SHA1 Message Date
github-actions[bot] 267066dce0 chore: bump version to 1.6.12 [skip ci] 2026-06-16 03:31:05 +00:00
Juan Denis 49360806d2 Merge branch 'dev' of https://github.com/jhd3197/ServerKit into dev 2026-06-15 23:30:49 -04:00
Juan DenisandClaude Opus 4.8 ee7d492dd2 feat(remote-access): per-edge port allocation + blocked-UDP diagnostic (Phase 4)
Phase 4 of docs/REMOTE_ACCESS_ROADMAP.md (the buildable/verifiable parts):

- #20 multiple private peers per edge: allocate a per-edge WireGuard listen
  port (tunnel_netutil.pick_listen_port) instead of a hardcoded 51820, so one
  edge can front many private peers without UDP-port conflicts. (Multiple
  services per tunnel already worked since Phase 2.)
- #21 reachability diagnostic: tunnel_netutil.diagnose_reachability classifies
  an up-but-never-handshaking tunnel as likely-blocked outbound UDP; surfaced
  in the status-refresh response (diagnostic field) and as a hint on the Remote
  Access page. The relay data plane itself is deferred.

#22 (client-VPN) and #23 (identity-aware SSO) remain XL milestones, not built.

Verified: 14 backend tests (incl. port-allocator + diagnostic), eslint + build.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 23:28:12 -04:00
Juan DenisandClaude Opus 4.8 7cf8d4e18a feat(remote-access): NAT-traversing WireGuard tunnels to expose private services
Self-hosted tunneled reverse proxy: a service on a NAT'd host (e.g. a home
Jellyfin box) gets a public HTTPS hostname with no port-forwarding, by pairing
two agents over WireGuard. Implements Phases 0-3 of docs/REMOTE_ACCESS_ROADMAP.md.

Agent (Go):
- new internal/wireguard package: Manager interface + kernel backend (wg/ip)
  and a userspace netstack backend (wireguard-go + gVisor) so Windows/macOS
  peers need no driver/admin; in-process keygen returns only the public key
  (private keys never leave the host)
- wireguard:keygen/interface/peer/status/forward/unforward + firewall
  :allow_port/:deny_port commands; wireguard + firewall capability probes
- TCP forwarder (tnet.ListenTCP / net.Listen -> dial localhost) carries
  traffic to the real service behind a netstack peer

Backend (Python):
- Tunnel + ExposedService models; tunnel_netutil (subnet allocator, health);
  tunnel_broker_service (keygen->up->peer->verify, firewall open, reconcile-on-
  reconnect); tunnel_publish_service (forward -> nginx -> DNS -> auth -> cert)
- nginx remote-upstream vhost (WebSocket + proxy_buffering off)
- /api/v1/tunnels CRUD + /<id>/services publish/unpublish
- panel-authoritative reconcile hooked into the agent gateway on reconnect

Frontend (React):
- Remote Access page (tunnels + exposed services, status, teardown) and an
  "Expose a Local Service" wizard; new services/api/tunnels.js

Verified: go build (win/linux) + vet + go tests (forwarder + no-leak parsers);
py_compile + pytest (netutil, reconcile, nginx template); create_app boot;
eslint + npm run build. Live multi-host run pending.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 23:20:00 -04:00
github-actions[bot] 5885b425b5 chore: bump version to 1.6.11 [skip ci] 2026-06-16 02:36:46 +00:00
Juan DenisandClaude Opus 4.8 733f866f1c feat(wordpress): per-site PHP-fatals analytics from the WP_DEBUG log (#25)
Finish #25's deferred PHP-fatals signal, sourced from the WP_DEBUG log
(/tmp/wp-debug.log) that #30's Security toggle writes — not the access log
(which can't carry it):

- get_php_errors: one bounded docker exec (marker-guarded tail -c, 15s single-
  worker timeout) distinguishes logging-off / no-log from on-but-clean, then
  parses [time] PHP <Level>: <msg> into severity counts (fatal/warning/notice/
  deprecated; Parse + Recoverable-fatal fold into fatal) + the 25 most-recent
  entries (messages capped, stack-trace lines skipped).
- Merged into GET /sites/<id>/analytics as php_errors; a PHP errors panel added
  to the Analytics tab with an 'enable WP_DEBUG on the Security tab' empty state.
- Supplies the per-site error signal #27's error-spike rule wanted.

7 new tests (classification, parse/skip-traces, off/empty/unavailable, capping);
full backend suite 218 passed / 12 skipped; frontend lint-clean + production build.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 22:06:58 -04:00
Juan DenisandClaude Opus 4.8 a6a214c245 feat(wordpress): wp-content/full-site zip import with zip-slip-safe extraction (#17)
Finish #17's deferred files half on top of the .sql MVP. After the DB import +
search-replace, an optional wp-content/full-site .zip is extracted and copied
into the container:

- _safe_extract_zip rejects any member escaping the temp dir BEFORE writing
  (zip-slip: ../ traversal, deep traversal, absolute paths) + bad-zip guard.
- _resolve_wp_content_dir handles the 3 layouts: zip OF wp-content, zip WITH
  wp-content/ at root, and full-site zip one wrapper folder deep.
- _import_wp_content_zip docker cp's the resolved tree into
  /var/www/html/wp-content and chowns it to www-data. Non-fatal: DB import still
  succeeds; response carries a warning + wp_content_imported flag.
- Route accepts an optional wp_content multipart file (.zip, own temp, cleaned
  up); client importSite({wpContentFile}) + a file input in the import modal.

11 new tests (zip-slip rejection, layouts, docker cp/chown flow mocked); full
backend suite 211 passed / 12 skipped; frontend lint-clean + production build.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 21:59:59 -04:00
Juan DenisandClaude Opus 4.8 aaf99212cb feat(wordpress): durable per-site PHP limit writes + wire the PHP tab (#24)
Finish #24's deferred limit-WRITE side, Docker-correct for wordpress:*-apache
(Apache + mod_php):

- set_php_limits: whitelist + re.fullmatch-validate each directive (fullmatch so
  a newline-smuggled extra ini line can't pass the $ anchor), write a host-side
  php-conf/zz-serverkit.ini and bind-mount that single file into conf.d so it
  survives container recreate. File written before the mount is added; new mount
  applies via compose up, content-only change via restart so php.ini reloads.
  Partial updates merge; mount injected once.
- Route POST /sites/<id>/php/limits (admin), client setPhpLimits, and an Edit
  PHP Limits panel in the PHP tab (inputs seeded from live values, partial save).
- Fix latent bug: PhpTab (the #24 read panel) was built but never wired into
  WordPressDetail (no tab button / render branch) — the whole PHP panel was
  invisible. Added 'php' to VALID_TABS + a tab + render branch.

7 new tests; full backend suite 200 passed / 12 skipped; frontend lint-clean +
production build.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 21:43:12 -04:00
Juan DenisandClaude Opus 4.8 4a637c7181 docs(wordpress): split #34 into an executable per-site edge + scaling spec
#34 is infra-blocked and unverifiable on the Windows dev host, so spec it
rather than build it blind. A capability sweep found the load-balancing,
rate-limit, proxy-cache, per-vhost-log, wildcard-TLS, and S3-upload primitives
already exist (nginx_advanced_service / nginx_service / advanced_ssl_service /
storage_provider_service) — so #34 is wiring + a public base-domain + stateless
WP, not greenfield nginx.

Adds docs/HORIZONTAL_SCALING_SPEC.md: a shared keystone (K0-K4) that also
unblocks #21/#15-TLS/#8/#30-jail/#25, then #34 proper (H0-H4), each phase a
separately verifiable PR with a Windows-vs-real-host split. Links it from the
roadmap #34 entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 21:09:42 -04:00
Juan DenisandClaude Opus 4.8 fa73b217ef feat(workspace): scope domains/databases + reconcile workspace roles (#33)
Finish #33's two named workspace-scoping slices, backend-only (no schema
migration, no frontend changes):

- Domains: get_domains scopes through the parent Application via scope_query
  (own + granted, workspace-narrowed); also surfaces granted apps' domains,
  closing a gap vs the per-domain routes. Admin-no-context path unchanged.
- Databases: the app-linked /docker/databases enumeration scopes via
  scope_query; host-level MySQL/PG/SQLite stay global system resources
  (documented inline).
- Role reconciliation: WorkspaceService.effective_role + can_write_in_workspace
  intersect global User.role with per-workspace WorkspaceMember.role,
  narrow-only (a workspace role can only reduce capability; system admin never
  capped). Enforced at apps/servers create + reassign seams (viewer -> 403,
  only when a workspace context is active). Reconciled role surfaced on
  GET /workspaces (my_role/my_effective_role).

6 new tests; full backend suite 193 passed / 12 skipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 21:00:57 -04:00
Juan DenisandClaude Opus 4.8 60a676025a Fix wildcard HTTPS reading encrypted DNS creds raw + add encryption tests
Auditing the encryption-at-rest work surfaced one missed read site: sites_https_service
passed the DNS provider api_key/api_secret straight to the cert issuer, but those are
now Fernet-encrypted at rest — so DNS-01 wildcard issuance would have received ciphertext
and failed.

- Add DNSProviderService.decrypted_credentials() as the public, canonical way to read a
  DNS provider's usable creds (nothing reads .api_key raw anymore); use it in
  sites_https_service.
- Add tests/test_provider_secret_encryption.py — 8 proving tests: ciphertext-at-rest +
  plaintext-on-read for DNS / cloud / storage / registrar, the idempotent legacy-plaintext
  migration, and the wildcard path now receiving decrypted creds (the regression above).

pytest: 17/17 pass (8 new + 9 existing test_sites_https.py). Full suite: 187 passed; the
lone failure (test_agent_poll_e2e command roundtrip) is a pre-existing flake — the agent
registry in-memory singleton leaks across tests in that file (passes solo), unrelated here.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 19:55:57 -04:00
Juan DenisandClaude Opus 4.8 fd2313077f Polish connections: domain-expiry alerts, S3 image preview, encryption-health banner
- Domain-expiry notifications: a daily scheduler (RegistrarService.notify_expiring)
  fires through the notification channels when a registrar domain crosses 30/14/7/1
  days or expires, de-duped via a small state file so each crossing alerts once.
- S3 image preview: ImageThumb is now S3-aware (loads objects via short-lived
  presigned URLs); threaded through FileCard / PreviewDrawer / FileManager.
- Encryption-health banner: the Connections hub consumes GET /api/v1/connections
  and warns if any credential is not encrypted at rest (hidden in the happy path)
  — the registry's first real consumer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 19:43:00 -04:00
Juan DenisandClaude Opus 4.8 bbe6bee267 Unify credential encryption + add a read-only connection registry (#18)
- Encrypt the last plaintext credential store: CloudProvider.api_key_encrypted is
  now Fernet-encrypted on write and decrypted in _auth_headers, with an idempotent
  encrypt_legacy_secrets() migration run at startup. All five stores (source, DNS,
  cloud, registrar, storage) now encrypt identically via app.utils.crypto.
- Add ConnectionRegistry + GET /api/v1/connections: one normalized, secret-free
  list of every connected external account (the "what's connected" source of
  truth). Frontend api.getAllConnections().
- The full write-side "one model replaces five" migration is deliberately not done
  — it would risk five working subsystems for no user-facing benefit. The uniform
  encryption + read registry deliver #18's practical value.

Verified by booting create_app('testing') and running the registry live (returns
a list; /api/v1/connections present in the URL map).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 10:47:59 -04:00
Juan DenisandClaude Opus 4.8 d157fc514d Encrypt provider secrets at rest + add Namecheap registrar (#16, #17)
Encryption-at-rest (#16/#17) for the two provider stores still in plaintext:
- crypto.py: add decrypt_secret_safe (decrypt with plaintext fallback) + is_encrypted.
- DNS providers: encrypt api_key/api_secret on write; decrypt at point-of-use via
  _api_key/_api_secret in the Cloudflare/Route53/DigitalOcean/GoDaddy builders.
- Storage: encrypt secrets in save_config; decrypt in get_config.
- Idempotent encrypt_legacy_secrets() migrations run on startup, encrypting any
  pre-existing plaintext in place. Legacy values keep working via the decrypt
  fallback, so there is no flag day.

Namecheap registrar:
- RegistrarConnection.config_json column for non-secret extras (username, client_ip).
- RegistrarService: Namecheap XML domains.getList (portfolio + expiry), test, and
  MM/DD/YYYY -> ISO date parsing.
- Frontend: namecheap tile is live; the registrar form is provider-aware (GoDaddy
  key+secret vs Namecheap key + username + whitelisted IP).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 10:33:09 -04:00
Juan DenisandClaude Opus 4.8 845df9959e Add S3 bucket browsing to the Files app (#14, #15)
- backend: StorageProviderService.s3_* (browse/read/write/upload/delete/
  presigned-get) + /api/v1/files/s3/* routes, reusing the configured backup
  bucket — no new credentials.
- frontend: an "S3 bucket" target in the File Manager. UI paths stay slash-rooted
  and map to S3 keys server-side, so breadcrumbs/history work unchanged; virtual
  folders from prefixes; presigned-URL download. Folders/rename/permissions are
  disabled and the local-only sidebar is hidden on S3. TargetPicker gained a
  backward-compatible extraOptions prop.
- docs: mark #14/#15 done (Phase 4).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 10:21:39 -04:00
Juan DenisandClaude Opus 4.8 7b6086934d Finalize Connections hub: complete #8 SMTP relay docs + lint cleanup
Follow-up to the connections work already committed in 2e1bdbc + c1d4653
(swept in by a concurrent commit). Commits the two remaining edits:

- docs/CONNECTIONS_ROADMAP.md: mark #8 (SMTP relay) done — a real Postfix
  smarthost (relayhost + SASL) with an OS-agnostic smtplib connection test.
- ConnectProviderModal: drop the unused `provider` arg from EmailBody (lint).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 09:23:06 -04:00
Juan Denis c1d4653f6c Add registrars, GitLab, DO/Godaddy DNS and SMTP relay
Introduce a broad Connections expansion: add RegistrarConnection model, RegistrarService and /api/v1/registrars endpoints to surface domain portfolios and expiry. Add EmailRelayService and API routes plus Postfix relay wiring to configure/test/disable outbound SMTP. Extend DNSProviderService with DigitalOcean and GoDaddy implementations and helpers. Add GitLab support to source connection service and routes. Frontend: expand Connections hub, provider catalog, modal bodies, provider icons, RegistrarPortfolio component, API client methods and styles. Add docs/CONNECTIONS_ROADMAP.md describing roadmap and intent.
2026-06-15 09:19:56 -04:00
Juan Denis 2e1bdbcb3d Add Connections hub and provider UI
Introduce a new Connections hub for Settings: adds presentational components and catalog for external providers (ProviderBrands, ProviderCard, ConnectionsHub, ConnectProviderModal, providerCatalog) plus SCSS (_connections.scss) and brand icons. Replaces the old GitHub-only SourceConnectionsTab (removed) and wires ConnectionsHub into Settings. Also imports the new styles in main.scss and tweaks settings styles. Provides UI to list/manage GitHub/DNS providers, show scopes, and handle connect/manage flows; keeps several providers marked comingSoon for future work.
2026-06-15 02:22:29 -04:00
Juan Denis 5212704942 Add Git provider UI, refactor headers, and WP fixes
Introduce GitProviders (provider detection, badges, and RepoProviderStrip) and a shared _git-connect.scss; update Git connect modal and WordPress Git form to use the new UI and provider detection. Replace many page header blocks with PageTopbar and wrap loading states in page-container for consistent layout. Backend: add _is_wp_app helper and use it in wordpress API endpoints; ensure wp-cli login plugin is installed and activated. Minor style/token cleanups and .gitignore addition for docs/UI_CONSISTENCY_AUDIT.md.
2026-06-15 01:53:30 -04:00
Juan DenisandClaude Opus 4.8 0a74e5670d feat(sites): wildcard HTTPS for managed subdomains
One-time operator setup that makes every managed subdomain HTTPS. New
SitesHttpsService.setup creates the *.<base> + <base> A records via a connected
DNS provider and issues a wildcard Let's Encrypt certificate over DNS-01,
reusing that provider's credentials — then flips sites_https_enabled on.

- AdvancedSSLService.issue_wildcard_cert now supports route53 (AWS env creds) as
  well as cloudflare, takes an email, installs the matching certbot DNS plugin
  best-effort, and returns the cert/key paths.
- SiteDomainService gains https_enabled / wildcard_cert_paths / covers. create_site
  installs new subdomain sites at https when enabled, and _write_app_vhost serves
  the wildcard cert on vhosts whose every domain it covers (custom domains keep
  their own cert).
- NginxService.create_site takes optional ssl_cert/ssl_key and bakes a 443 block +
  HTTP->HTTPS redirect (idempotent via full regen).
- sites_base_domain / server_public_ip / sites_https_enabled are now discoverable
  settings. Admin endpoints GET/POST /admin/sites-https/{status,setup}, surfaced
  in Settings → Site as a "Managed Sites Domain & HTTPS" panel.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 00:52:30 -04:00
Juan DenisandClaude Opus 4.8 ad5c9b6bf0 feat(wordpress): attach a custom domain with auto-DNS
Turns "point myblog.com at this site" into one action that consumes the URL-swap
tool. New DNSProviderService.find_zone_for_domain (longest zone-suffix match
across connected providers) + ensure_a_record upsert the A record via Cloudflare/
Route53, or return the exact record to add when nothing manages the zone.

WordPressService.attach_custom_domain orchestrates it: auto-create DNS (server IP
from SiteDomainService), optionally obtain a Let's Encrypt cert (the host vhost is
written first so certbot --nginx can validate), then migrate the site URL to the
domain via change_site_url. External steps degrade to warnings; only a requested
migration that errors aborts.

POST /wordpress/sites/<id>/domain (admin). Frontend: "Add Domain" button +
AttachDomainModal — enter a domain, optional HTTPS, and the result shows the
auto-created record or the exact A record to add manually.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 00:30:50 -04:00
Juan DenisandClaude Opus 4.8 57a7b7b3e6 feat(wordpress): URL-swap tool — safe, previewable site URL changes
Changing a WordPress site's URL is not a single UPDATE: the URL is embedded
across wp_options, post content, and PHP-serialized blobs whose byte-length
prefixes break under a naive REPLACE. This adds a first-class swap tool built on
WP-CLI search-replace (serialization-safe), with the rails to make it routine:

- preview_url_change: dry-run that reports per-pair replacement counts, no writes
- change_site_url: backs the database up first, rewrites with --skip-columns=guid
  (full-URL + host-only pairs so scheme/protocol-relative refs are covered),
  updates home/siteurl, flushes the object cache + permalinks, then re-points the
  primary Domain row and nginx vhost. Rolls the DB back from the backup on
  failure. keep_old_redirect keeps the old host resolving so WordPress 301s it.
- POST /sites/<id>/url/preview and /sites/<id>/url (admin-gated)
- "Change URL" modal on the site detail page: live dry-run preview, backup
  notice, keep-old-address toggle, then apply.

_write_app_vhost is factored out of _provision_routing and shared by the
re-point path. The pure-SQL search-replace in db_sync_service remains the
offline-dump fallback only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 00:19:16 -04:00
Juan DenisandClaude Opus 4.8 1a0214f11d feat(wordpress): publish new sites at a real subdomain, not localhost:port
create_site installed WordPress at http://localhost:<port>, which WordPress
bakes in as its canonical home/siteurl — reachable on the box but useless as a
public site URL. New SiteDomainService resolves <slug>.<base_domain> (config
SITES_BASE_DOMAIN: lvh.me in dev, opt-in in prod, empty falls back to the old
localhost URL). create_site now installs at that host and provisions a primary
Domain row + nginx reverse-proxy vhost (_provision_routing, best-effort so a box
without nginx warns instead of failing). Teardown removes the vhost; clone_site
URL rewriting is generalized via _canonical_site_url so cloning stays correct
with real hostnames.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 00:01:51 -04:00
Juan DenisandClaude Opus 4.8 a6f66b2976 fix(databases): distinguish a connection failure from an empty database
docker_mysql_get_tables returned [] on both a genuinely empty database and a
container/auth failure, so the explorer rendered "Empty" either way and silently
hid broken connections. It now returns {connected, tables, error}; the API
passes that through, and the source tree shows "No tables yet" for a connected
but empty database vs the real reason when the container exec/auth fails.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 00:01:41 -04:00
Juan DenisandClaude Opus 4.8 0abed0efa1 redesign(dashboard): admin-gated recent-activity feed
Wire the previously-unused Feed/FeedItem ds primitives to the existing /admin/activity/feed endpoint as a Recent Activity panel beside Applications (span 8 + span 4). Admin-gated via AuthContext isAdmin so non-admins neither see it nor trigger the 403; actions map to tone+icon, with relative timestamps and a link to the audit log.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 03:18:30 -04:00
Juan DenisandClaude Opus 4.8 e1a2a68359 redesign(security): fail2ban per-jail KPI cards
Fetch each active jail's status (getFail2banJailStatus) and render a per-jail card grid (Banned / Failed / Total banned) above the banned-IPs table, matching the prototype. Per-jail counts already existed behind the /fail2ban/jails/<jail> endpoint — the tab just never called it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 03:12:10 -04:00
Juan DenisandClaude Opus 4.8 a3f4676ad9 redesign(wordpress): functional plugin/theme card grids
Replace the dense plugin/theme tables with the prototype's card grids. Plugins: ServiceTile avatar + name/slug + version + update-flag, and a working activate/deactivate Switch. Themes: preview placeholder + Active pill + Activate. Wires the already-existing backend WP-CLI endpoints via new activatePlugin/deactivatePlugin/activateTheme api methods (these were never backend-blocked — only the frontend api methods were missing).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 03:12:10 -04:00
Juan DenisandClaude Opus 4.8 80bdd21375 redesign(files): owner column + flush inline preview pane
Add an Owner column to the list view (header + FileRow cell + grid). Convert the file preview from a fixed overlay-drawer into a flush inline 3rd pane within the file-manager body via an opt-in 'inline' prop on PreviewDrawer (the shared .preview-drawer overlay used by Terminal/Docker is untouched). All preview features, tree, uploads, bulk and context-menu preserved.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 02:57:47 -04:00
Juan DenisandClaude Opus 4.8 b08c7970e3 redesign(security): overview posture gauge + dense integrity table
OverviewTab gains a ScoreGauge posture ring + hardening checklist computed client-side from real signals (ClamAV state, file-integrity baseline, recent integrity changes, notifications) — no invented backend number. IntegrityTab's plain path lists become a dense sk-dtable with change-kind state chips (modified/deleted/new/permission). Fail2ban per-jail cards left out (payload has no per-jail counts).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 02:57:47 -04:00
Juan DenisandClaude Opus 4.8 fb0fd1cf44 redesign(domains): inline DNS records table in the drawer
The domain drawer punted DNS to a /dns link; now it loads the domain's zone records and renders them inline as a dense sk-dtable (colored .dns-rtype chip + mono name/value + TTL), reusing the DNSZones records API. Handles loading/empty/no-zone/error; the Manage DNS records link is kept for full editing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 02:57:47 -04:00
Juan DenisandClaude Opus 4.8 1feb6ef336 redesign(dashboard): server-switcher dropdown on title
Replace the static hostname h1 + separate select with a .srv-switch button that opens a popover server list (status dot + name + provider/IP meta + active check), matching the prototype env-menu. Reuses the existing server-change handler verbatim; shown even for a single server. (Recent-activity feed stays backend-blocked.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 02:57:47 -04:00
Juan DenisandClaude Opus 4.8 3eeb0611b9 redesign(wordpress): list subtitle, filter counts, search, gradient tiles
Add a count subtitle, per-filter counts on the status SegControl, a client-side site search, and per-site ServiceTile gradient avatars (replacing the flat static Globe). Bulk-select, tag filter, status pills and links are preserved; enriched columns (plugins/visits/uptime) stay backend-blocked.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 02:49:36 -04:00
Juan DenisandClaude Opus 4.8 fd341b4d63 redesign(services): restyle new-service method picker as a grid
Convert the vertical 3-card mode strip into the prototype's .method-grid (icon tile + title + sub, accent border/wash on the selected card, hover lift). All three create flows and form logic are unchanged; Docker-image/from-scratch stay backend-blocked.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 02:49:36 -04:00
Juan DenisandClaude Opus 4.8 83cab5af28 redesign(git): add repository-connect hint banner
Add the prototype's .git-hint accent-gradient explainer banner atop the Git page (icon tile + headline + sub). The providers OAuth grid remains backend-blocked and is intentionally not added.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 02:49:36 -04:00
Juan DenisandClaude Opus 4.8 e3a9208172 redesign(marketplace): add horizontal category chip row
Add a quick-scan .cat-chips row above the browse grid (All + per-category), wired to the existing category filter. The right sidebar list and select are preserved (additive).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 02:49:36 -04:00
Juan DenisandClaude Opus 4.8 69cb44dbd5 redesign(email): show status dots on DNS presence pills
DKIM/SPF/DMARC presence pills passed dot={false}, rendering as flat chips; the prototype dtable uses dot+label status pills. Drop dot={false} so the colored status dot shows. A 3rd pending/verifying state remains backend-blocked (API reports boolean presence only).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 02:43:43 -04:00
Juan DenisandClaude Opus 4.8 67ae2ae8ab redesign: settings nav accent-wash + rail width parity
Settings nav active item used a heavy solid accent fill; switch to the subtle accent-bg wash + accent-bright text/icon used by .sk-topbar__tab (theme-safe in light and dark). Tighten the nav column 240->206px and widen the workflow palette rail 212->248px to match the prototype.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 02:43:43 -04:00
Juan DenisandClaude Opus 4.8 a8f77bc97b redesign(cron): migrate page header to PageTopbar
Cron Jobs still used the legacy .page-header (plain h1 + subtitle). Swap to the shared PageTopbar (icon + title + actions) used by the other migrated pages, matching the prototype's topbar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 02:43:43 -04:00
Juan DenisandClaude Opus 4.8 dfc628e13b redesign(fleet): render fleet tables with sk-dtable
Agent Fleet and Fleet Monitor used the generic .table style while every other migrated list page uses the dense .sk-dtable look (mono uppercase headers, token row borders). Swap the className so they match their siblings under the Servers tab group.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 02:43:43 -04:00
Juan DenisandClaude Fable 5 b2fad9528a docs: clarify agent.key + session-token notes; record audit remediation (L2/L4-L7)
- SECURITY.md: spell out that agent.key encryption is off-host/tamper
  protection, not confidentiality against local root; 0600 perms are the real
  control (L2).
- agent_registry.py: reword the per-message session-token "TODO" to an accepted
  limitation (defence-in-depth, not a bug) (L7).
- AGENT_AUDIT.md: record H1/H2/H3 and L1-L7 status, with L4 (cap OR-merge),
  L5 (single-worker), L6 (windows .bat) noted as accepted by-design/documented.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 18:35:52 -04:00
Juan DenisandClaude Fable 5 df67ce782b fix(ui): remove dead agent-version management buttons (audit L3)
The Agent Versions tab rendered an "Add Version" button and per-row Edit/Delete
buttons with no handlers — there are no edit/delete endpoints. Remove the dead
controls (and now-unused Plus/Trash2 imports); the version list is unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 18:35:43 -04:00
Juan DenisandClaude Fable 5 75db466096 fix(pairing): require developer role for claim/lookup (audit L1)
Pairing /claim creates a fleet server and /lookup reveals a pending agent's
details; both were @jwt_required() only, so any authenticated user (incl. a
read-only viewer) could use them. Gate both with @developer_required to match
POST /servers. Test: viewer 403, developer clears the gate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 18:35:43 -04:00
Juan DenisandClaude Fable 5 a34064d15a fix(plugins): make plugin/template features fail honestly, not silently (audit H2)
agent_plugin_service and server_template_service dispatched agent commands
through get_agent_gateway() — a function that does not exist — inside a bare
try/except. Every call raised ImportError, was swallowed, and left DB rows stuck
in 'installing'/'checking'/'remediating' forever while the UI implied the
feature worked. The agent also has no handlers for these commands.

- Plugin install now reports STATUS_ERROR with "Agent-side plugin support is not
  implemented yet" instead of hanging in 'installing'.
- Template drift-check/remediate report 'unknown' + an explanatory
  drift_report.error.
- Local-only ops (disable, uninstall, configure) just drop the dead dispatch.
- test_agent_features_honest.py: 3 tests asserting honest states.

Implementing the feature for real (agent handlers + sandbox) remains future
work; this stops it from lying in the meantime.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 18:35:35 -04:00
Juan DenisandClaude Fable 5 db4b403a59 fix(agent): fail-closed checksum verification in the self-updater (audit H1)
The updater could install an unverified binary three ways: it skipped
verification when no checksums URL was given, returned success when no checksum
line matched the platform, and ignored the HTTP status of the checksums fetch
(a 404 body parsed to an empty map → skipped). Since the updater runs as the
agent process (root on packaged installs), that was a code-execution primitive
that bypassed every feature gate.

- Refuse to install without a checksums URL.
- Extract a pure, fail-closed matchChecksum: empty/unparseable body, no entry
  for this GOOS/GOARCH, or a mismatch all error (was a silent skip).
- Check the checksums fetch HTTP status.
- requireSecureURL rejects non-HTTPS update/checksum URLs (dev override via
  SERVERKIT_INSECURE_TLS).
- updater_test.go: 6 tests covering match/mismatch/no-entry/empty/case/https.

Cryptographic release signing remains the recommended next layer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 18:35:27 -04:00
Juan DenisandClaude Fable 5 089b75526e fix(agent): accept registration token from env to keep it out of argv (M5)
The installers ran `serverkit-agent register --token <token>`, putting the
registration token in the process argument list — world-readable via `ps` /
/proc/<pid>/cmdline while register runs. The documented curl|bash one-liner also
landed the token in shell history.

- register now falls back to SERVERKIT_REGISTRATION_TOKEN /
  SERVERKIT_CONNECTION_STRING / SERVERKIT_SERVER_URL when the flags are absent
  (a process's environment is readable only by its owner and root).
- install.sh passes the token via env (drops the old `eval`) and accepts it from
  the environment so it need not appear on the curl line.
- install.ps1 passes the token to the child via env, and makes -Token optional
  with the same env fallback.

Verified: agent reads the env token at runtime (register proceeds past flag
validation to the network call); install.sh `bash -n` and install.ps1 parser
both clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 17:52:59 -04:00
Juan DenisandClaude Fable 5 6eb4471829 fix(servers): require developer role for agent self-update (audit H3)
POST /<id>/agent/update was @jwt_required() only, so a read-only viewer could
trigger agent binary replacement + service restart across the whole fleet —
inconsistent with every other state-changing server route, which is
@developer_required. Add @developer_required and a test proving a viewer gets
403 while a developer clears the gate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 17:33:29 -04:00
Juan DenisandClaude Fable 5 0bc30b62ef ci: run agent Go checks and backend pytest on push/PR (addresses audit M4)
Nothing ran the agent's tests or the backend suite automatically, so breakage
was only found by hand. Add two workflows:

- agent-ci.yml: go vet + go test on linux, plus a cross-compile build matrix
  (linux amd64/arm64, windows amd64) to catch platform-specific breakage at
  PR time. darwin is excluded — fyne.io/systray needs the macOS Cocoa toolchain
  and can't cross-compile from Linux (it's built on a mac at release time).
- backend-ci.yml: install requirements and run the full pytest suite, including
  the new end-to-end agent command-loop test.

Verified locally: go vet clean, all three cross-builds succeed, agent unit
tests and the full backend suite (136 passed) are green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 17:31:26 -04:00
Juan DenisandClaude Fable 5 43fc900944 test(agent): end-to-end test for the panel<->agent command loop
The agent subsystem had unit tests but nothing proving the whole protocol
works. This drives the REST poll transport (/connect, /poll, /result) — plain
HTTP that shares the WebSocket gateway's auth, registry, and command-routing
code — through the full loop: a correctly-signed agent authenticates, registers,
receives a command the panel issued via send_command (run on a background
thread since it blocks on the result), and round-trips the result back to the
waiter. Uses a temp-file SQLite DB so the thread and request handlers share
committed rows. 4 tests, all green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 17:31:26 -04:00
Juan DenisandClaude Fable 5 b57e436432 docs: add agent subsystem security & completeness audit
Severity-ranked, file:line-referenced audit of the agent fleet (Go agent,
gateway/registry/long-poll, pairing, panel→agent APIs, install/packaging, UI,
docs/CI) plus a remediation-status section recording the M1/M2/M3 fixes and the
M2 correction (compose allowlisting was already enforced at the handler layer).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 01:34:02 -04:00
Juan DenisandClaude Fable 5 315d443305 fix(agent): run packaged DEB/RPM agent as an unprivileged service account
The packaged systemd units ran the agent as User=root with a self-contradictory
ProtectSystem=strict, while the script installer (install.sh) runs a dedicated,
hardened serverkit-agent account. Align the packages with install.sh:

- User/Group=serverkit-agent; NoNewPrivileges=yes, ProtectSystem=strict,
  ProtectHome=yes, PrivateTmp=yes, ReadWritePaths for config/log/data.
- Create the account (deb postinst / rpm %pre), add it to the docker group,
  create+chown the data dirs, and re-chown the config dir on (re)install so an
  existing root-owned agent.key becomes readable after upgrade.
- ExecStartPre=+chown fixes credential ownership at start regardless of how the
  operator ran `register`; install docs now use `sudo -u serverkit-agent`.

Tradeoff: packaged agents match install.sh's capability set (Docker + metrics +
file access) and no longer do systemd/package management or self-update by
default (both need root). Disabling self-update also routes updates through the
OS package manager's signed path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 01:34:02 -04:00