39 Commits
Author SHA1 Message Date
Juan Denis a9668000da Keep the new route logic behind services
The SSL-enable and server-overview changes put session and query
calls in the routes, tripping the controller-boundary ratchet. Move
them into SiteDomainService and WorkspaceService, raise a typed error
for bad allowed_ips, point the ACME-contact test at the new vhost
door, and regenerate the baseline and inventory (476 -> 453).
2026-10-26 01:43:12 -04:00
Juan Denis 83c871c32b docs: refresh controller migration count after preview extraction 2026-10-24 14:57:27 -04:00
Juan Denis c0dbb921e1 feat: queue preview automation and preserve cleanup ownership through app deletion 2026-10-24 06:21:26 -04:00
Juan Denis ac0871c016 fix: start agent maintenance only in the guarded runtime phase 2026-10-24 05:14:36 -04:00
Juan Denis dc81185b7d Lower the JWT-only ceiling to 598
Two JWT-only routes were removed with the dead /system endpoints.
2026-10-10 22:05:38 -04:00
Juan Denis c07eb056ad Regenerate the migration inventory
The File Manager dialog extraction removed one raw API call, one toast
extraction and two hand-rolled form groups.
2026-10-10 20:53:33 -04:00
Juan Denis d8f1d5b2d5 Let the global handler render queue-bus errors
QueueBusError is an expected, caller-actionable failure, so it now
subclasses ApplicationError and the global handler answers it with its
own status. The 21 queue-bus routes each wrapped their whole body in
try/except only to do that by hand. Also records the two workspace
queries the previous commit moved out of the API layer.
2026-10-10 16:57:29 -04:00
Juan Denis 6068b099dc Replace nine per-app access helpers with one
Nine route files each loaded the live app and checked access, with
four return shapes, docstrings promising error tuples the code never
returned, and 41 dead 'if err: return err' branches. require_app()
backed by ResourceGrantService.get_app_for does it once; conceal=True
keeps the 404-for-denied answer the previews, WAF and slot routes
already gave, so no route's status changes. Nine query sites leave the
API layer.
2026-10-10 16:36:01 -04:00
Juan Denis 4e45ecb5f5 Move the new auth lookups behind the model/service
The server access helper and the app-name clash checks queried the DB
from the API layer, which the controller-boundary scan rejects. They
now go through WorkspaceService.get_accessible_server and
Application.name_clash_error; the baseline drops by three.
2026-10-10 09:56:44 -04:00
Juan Denis bd850adfb3 Raise typed errors from the new auth checks
The checks added to apps, servers and 2FA verify returned hand-shaped
{'error'} bodies, pushing that census past its ceiling. They now raise
the typed errors (a RateLimitedError joins them for 429). create_app
loads the user once through get_current_user(), which drops the
identity census to 68. Regenerates the migration inventory, stale since
the raw-subprocess ceiling fell to 23.
2026-10-10 09:47:11 -04:00
Juan Denis 862b1022c5 Regenerate the migration inventory
The hex colour literal ratchet dropped to 139 when the table work
removed one from the design system.
2026-10-09 00:34:41 -04:00
Juan Denis d2465d9817 Pass error details through and translate last labels
The API client handed translateServerError only the code and message,
so validation.required and conflict.exists could not name the field or
resource and fell back to server text; it now passes data.details.
Also translates the service-type subline, RepoConnectForm's default
labels and a ConnectionsHub fallback that the literal census skipped,
and refreshes the migration inventory counts.
2026-10-04 19:03:49 -04:00
Juan Denis 97a96d963b Give common API errors stable codes and service wording
Toasts showed backend prose verbatim, with no code to translate
against and "application" where the UI says "service". Not-found,
missing-field, permission, conflict and agent-offline errors now come
from factories in app/exceptions.py that pair one fixed message with
a dotted code (not_found.<resource>, validation.required,
permission.denied, conflict.exists, agent.offline), so the client can
translate them.

The shared app-loading helpers and the apps, domains, servers,
databases and backups routes raise those errors instead of hand
shaping bodies, which drops the hand-shaped body count from 1132 to
850. Five routes picked an HTTP status by comparing error text; they
now key off a result code or a sentinel so rewording can't change a
status. Five broad excepts that echoed str(exc) now log the crash and
return the request id instead.
2026-10-04 18:59:25 -04:00
Juan Denis 93a3cd783e Let GET /api/v1/servers accept a scoped API key
Vela holds one ServerKit API key as a host-side secret and polls the
fleet list, so this read route moves from bare @jwt_required() to
auth_required() + require_scope('servers:read') — a deliberate,
per-route jwt_only_census exception, not a sweep. JWT callers are
unchanged; a restricted key without servers:read gets 403 from the
scope middleware, no credentials get 401.

Scope choice: the existing servers:read catalog entry already covers
'list servers, agents, and metrics', and the serverkit-gui read routes
(capabilities/frame/surface, sibling repo) take the same scope, so one
key scope serves the whole Vela polling surface.

The census ceiling ratchets down to 600 and the migration inventory is
regenerated, as both ratchets require. tests/test_servers_api_key_scope.py
pins the four cases: scoped key 200, wrong-scope key 403, JWT 200,
anonymous 401.
2026-10-03 00:31:36 -04:00
Juan Denis acdf6b878d Remove auto-scaling and the load-balancer builder
Replica counts could be changed, but nginx only proxied to one port
and the auto-scale sweep was never scheduled, so neither the
Auto-scale section nor its five routes could work; the nginx
upstream builder had no UI at all. With one live copy per app they
are gone, and the scale-policy table is dropped. Running containers
are not touched. The scaling spec now says replicas are not
pursued.
2026-09-28 09:03:34 -04:00
Juan Denis d3baa27b72 Regenerate inventories for the Storage work
Adds GET /system/storage to the API surface, records the Storage
tab's form block in the migration inventory, and seeds the history
retention test through make_application.
2026-09-27 17:04:36 -04:00
Juan Denis 172e410ce3 chore: prepare 1.11.0 and refresh release inventories [skip version] 2026-09-08 09:31:21 -04:00
Juan Denis fde0acee6d fix(ai): satisfy release gates and prepare 1.10.0 [skip version]
Keep connection persistence behind the service boundary, use typed errors and shared JSON parsing, and refresh reviewed inventories. The new GET connection catalog accepts API keys so API clients managing connections can discover IDs without exposing credentials; existing JWT-only routes stay unchanged. Add regression coverage and release notes.
2026-09-08 01:30:03 -04:00
Juan Denis d67e5d07d0 fix: complete audit follow-ups and add regression coverage
Centralize backup schedule calculations and shared request/form flows, encode query values, and move repository finalization into its service. Add browser CI coverage, repair stale test fixtures, and provide opt-in request and build measurement tools.
2026-09-05 14:41:15 -04:00
Juan Denis aae180aaef docs: refresh migration inventory after reuse cleanup 2026-09-05 13:55:48 -04:00
Juan Denis c8c93a8e0c fix(auth): enforce MFA, scoped keys, and revocable sessions
Add session-family revocation and migration 097, require current credentials for password changes, constrain API keys, and require verified WebAuthn authentication. Update browser token handling and add boundary regressions. Existing JWTs require fresh login; restricted keys deny undeclared endpoints.
2026-09-05 04:29:16 -04:00
Juan DenisandClaude Opus 5 4dfb468f77 chore(connect): regenerate the surface, inventory and locale files
Generated files the connect work moved and nobody re-ran:

- docs/API_SURFACE.md gains GET /connect/status
- docs/MIGRATION_INVENTORY.md: the hand-shaped error bodies count is 1145
  (ceiling 1150) for the 409 the backups PUT now answers when Cloud owns
  the destination
- en.json gains app.backups.storageManagedByCloud, so the sentence the
  read-only backup form shows is translatable like every other string

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018q56AfqpgKjio4n9KYubvZ
2026-09-03 00:19:35 -04:00
Juan DenisandClaude Opus 5 503dedd95c fix(apps): start/stop/restart a docker app that has no compose project
`app_type == 'docker'` was being read as "compose-managed", but the build-pack
path deploys by building an image and running a single container
(DeploymentService._deploy_docker, named serverkit-app-<id>) and never writes
a compose file. start/stop/restart called `docker compose` anyway, which -- in
a directory with nothing to read -- fails with:

    no configuration file provided: not found

So every build-pack app returned 400 from the panel's start, stop and restart
buttons and could not be run at all. There was already a materialize-compose
guard for BYO-image apps, but it only fires when root_path is NULL; a
build-pack app has source on disk, so it fell straight through to compose.

Decide on the artifact rather than the type: `_has_local_compose_project()`
looks for a compose file on disk (the app's own `compose_file`, else the four
default names). When there is one, nothing changes. When there isn't, drive
the container the deploy actually created.

Stop treats an already-gone container as stopped -- reporting that as a
failure would strand the app in `running` forever. Start and restart explain
that the app has not been deployed yet rather than surfacing a docker error.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 01:34:30 -04:00
Juan DenisandClaude Opus 5 c8ae7f35f2 chore(docs): regenerate the API surface and migration inventory
Both committed inventories went stale behind the walkthrough work, so the
two ratchets that pin them failed on dev:

  test_api_surface_inventory::test_committed_surface_matches_served_surface
  test_migration_inventory::test_the_committed_inventory_matches_the_measured_state

API_SURFACE picks up GET and PUT /walkthroughs/definitions, the endpoints the
Walkthrough Studio contribution contract added. MIGRATION_INVENTORY moves the
hand-shaped {'error': ...} census 1141 -> 1142 against an unchanged ceiling of
1150; the routes above are the new sites and the ceiling still holds.

Regenerated, not hand-edited:
  SERVERKIT_UPDATE_API_SURFACE=1 pytest tests/test_api_surface_inventory.py
  python scripts/generate-migration-inventory.py

Version Bump failed only as a dependent of Backend CI ("Sibling CI failed;
VERSION will not advance") and needs no change of its own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 00:21:38 -04:00
Juan DenisandClaude Fable 5 5417c39100 perf(models): index every foreign-key column
An audit found 102 FK columns with no index — every join, per-parent
listing, and the delete cascades from _delete_cascade_policy scanned
whole tables on them (deployments.app_id, audit_logs.user_id,
dns_records.zone_id, webhook_logs.webhook_id, ...). All FK columns now
declare index=True; migration 096 creates the same indexes on existing
installs, deriving the list from the live schema at upgrade time (so
it is idempotent and cannot drift from what an install actually has).

Ratchet: test_every_fk_column_is_indexed pins the rule with no
allowlist — a new FK column is born indexed or the build fails.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 20:42:53 -04:00
Juan DenisandClaude Opus 5 e181ae7f44 fix(authz census): count extension routes from a source git tracks
a31cc5d3 widened the jwt-only census to app/plugins and shipped a ceiling
of 611 measured on a dev box. CI measured 591 and went red: most
app/plugins/ live copies are GITIGNORED, so the count depended on which
extensions a machine happened to have installed. That is the same
machine-dependence the builtin-copy drift census exists to prevent, and
this walked straight into it.

Count builtin-extensions/ instead -- the source, pinned equal to the live
copy by test_builtin_extension_drift.py, and tracked. Filter to
git-tracked files as well, because extractions leave stale ignored
directories behind (cloudflare-ops, git, localkit after plan 52) that
would otherwise be counted on some working copies but not others.

Stable population: 608 (35 of them extension routes across
cloud-provision, remote-access and status). Also regenerates
docs/MIGRATION_INVENTORY.md, whose row is derived from this ceiling, and
points the CLAUDE.md / AGENTS.md notes at the scope actually scanned.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 00:53:15 -04:00
Juan DenisandClaude Fable 5 4be264ccd5 chore: migration inventory picks up the 511 controller-boundary count
Second regeneration - the first ran before the boundary baseline rewrite
landed, so it still measured 514.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 14:37:51 -04:00
Juan DenisandClaude Fable 5 2bdddd4d77 chore: regenerate ratchet inventories after the git split
Controller-boundary baseline drops the three api/security.py crossings that
moved to serverkit-image-scan; API surface sheds the 18 Gitea operations;
migration inventory regenerated by preflight; test-count floor raised
4735 -> 4799 (SERVERKIT_CLEAN_COLLECT).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 14:35:38 -04:00
Juan DenisandClaude Fable 5 e29872f0cd fix(restore-points): satisfy jwt-only and seed-factory ratchets
The three restore-point read routes authenticated with bare
@jwt_required(), pushing the census to 579 against a 577 ceiling.
They now use the auth_required() policy door like the file's mutating
routes (which already accept API keys via @developer_required), keeping
read access consistent with write access for API-key callers; scope
authorization stays in-route. test_restore_point_env.py hand-rolled
Application/User seeds — switched to the make_application/make_user
factories. Migration inventory regenerated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 02:52:59 -04:00
Juan Denis 515d960614 chore(ratchets): regenerate the boundary baseline, error ceiling and migration inventory
The recipes Phase 0 commit moved all three measured counts without
regenerating their baselines. The new api/recipes.py persistence
crossings are owned and documented in the baseline; the hand-shaped
error census is unchanged by this branch.
2026-08-21 23:05:56 -04:00
Juan DenisandClaude Opus 5 550dd47585 fix(ratchets): regenerate the boundary baseline and the migration inventory
Two more shards were red on generated artifacts nobody regenerated:

- api_controller_boundary_baseline.json: the attachment round rewrote
  _persist_user_message to build the row before adding it, so the fingerprint
  moved from `db.session.add(AiMessage(...))` to `db.session.add(message)`.
  Same crossing, same category, count unchanged at 509 -- one line.

- docs/MIGRATION_INVENTORY.md: three measured numbers had moved without a
  regeneration -- bare @jwt_required() routes 577 -> 576, and the two frontend
  rows whose totals come from maps inside check-frontend-boundaries.mjs
  (setInterval pollers 10 -> 9, hex colour literals 151 -> 150). All three are
  deterministic AST/constant counts, so these are CI's numbers, not this box's.

The reason nobody caught the inventory locally is the third fix here: the
generator hardcoded `node`, and under WSL the Windows nvm directory is on PATH
but exposes only `node.exe`, so test_migration_inventory.py skipped on the dev
box. Both the generator and the test now resolve $NODE / node / node.exe, and
the test runs instead of skipping.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 20:14:34 -04:00
Juan Denis 2825c536a8 fix(i18n): address review — reset locale readiness, typed error for bad language, correct parity test reference
- LocaleContext: reset ready on each language change so non-default
  locale bundles trigger a rerender once active
- auth PUT /me: raise typed ValidationError (auth.unsupported_language)
  instead of a hand-shaped error body; error-shape ceiling 1145 -> 1144
- i18n.py docstring: point at the real parity test
  (test_user_language_preference.py)
2026-08-20 19:04:09 -04:00
Juan Denis ae95ac4478 docs: regenerate MIGRATION_INVENTORY to match measured state 2026-08-20 18:41:14 -04:00
Juan DenisandClaude Opus 5 7311abe61f docs(inventory): regenerate the migration inventory after the authz sweep
The workspace-authz round converted 28 routes off bare @jwt_required()
onto role decorators and ratcheted JWT_ONLY_CEILING 605 -> 577, but the
generated inventory was never regenerated, so CI's freshness test failed.

Regenerated from the same censuses that enforce the ratchets:

  bare @jwt_required()        605 -> 577 (ceiling 605 -> 577)
  hand-shaped error bodies   1159 -> 1158 (ceiling stays 1159, 1 of 25 slack)
  controller-boundary         510 -> 509 (baseline json already at 509)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 22:29:52 -04:00
Juan DenisandClaude Fable 5 4fcd0db4d5 chore(frontend): delete the dead useContainerStatus hook and its legacy-poller entry
The hook has zero importers yet was counted in the LEGACY_POLLERS baseline,
holding a phantom slot in the ratchet (a place a new raw setInterval could
hide). Poller debt shrinks 11 sites/10 files -> 10 sites/9 files; the
migration inventory is regenerated to match.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 10:14:36 -04:00
Juan DenisandClaude Fable 5 c803ee5012 fix(census): measure only tracked files — the instruments count what ships
The raw-subprocess census and the thread-ownership guard walked the
filesystem under backend/app, which on a dev box also holds *installed*
extension copies under app/plugins/ that git does not track. The census
therefore measured 28 on dev machines but 24 on CI's clean checkout — four
sites of invisible ceiling slack, a stale thread classification for the
locally-installed analytics extension, and a red migration-inventory
freshness test on every CI run since the range landed.

Both instruments now intersect their walk with git ls-files (falling back
to walking everything when git cannot answer), the analytics thread entry
is removed (extension repos own their own threads), the ceiling ratchets
down 28 -> 24, and the inventory is regenerated. Proving test:
test_untracked_files_are_not_counted plants an untracked file with a raw
call inside app/ and asserts the census does not see it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 10:06:38 -04:00
Juan DenisandClaude Fable 5 c5ade5a2bc docs: regenerate MIGRATION_INVENTORY (runs endpoint now on auth_required)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 06:06:08 -04:00
Juan DenisandClaude Fable 5 b0589e95db feat(realtime): generalized run envelope — one stream door for every run kind (plan 77 E1)
RunLogStream accepts any RunLike (__run_kind__ + id + to_dict, optional
result/step members): one room grammar run_<kind>_<id>, one batched
persisted run_log/run_status event pair, and a GET
/api/v1/runs/<kind>/<id>/logs?after_id polling twin over the new
run_log_entries table (migration 087). Deploys keep DeploymentJobLog and
dual-emit the legacy deploy_log/deploy_status during the migration
window. The unified Job runtime — which had NO live channel — adopts it
in the consumer (start/succeeded/failed frames; handlers can log via
stream_for(job)). The dead pipeline_* scaffolding was retired in E2.
Gate: emit -> persisted row -> after_id resync contract test; consumer
adoption test. MIGRATION_INVENTORY.md regenerated (census moved).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 05:13:02 -04:00
Juan DenisandClaude Fable 5 3953090526 feat(conformance): generated remaining-migration inventory (H closure)
docs/MIGRATION_INVENTORY.md is generated from the live censuses and the
frontend boundary checker's new --inventory JSON mode — no hand-typed
numbers, so the plan cannot claim completion while a door is bypassed. A
freshness test fails whenever a ratchet number moves without regenerating
the document, making the regeneration part of every migration commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 00:16:39 -04:00