2068 Commits
Author SHA1 Message Date
Juan Denis fc343de231 Merge pull request #105 from jhd3197/dev
Require an explicit authorization decision on every mutating route
v1.7.116
2026-08-19 22:42:32 -04:00
github-actions[bot] 9e0f19c378 chore: bump version to 1.7.116 [skip ci] 2026-08-20 02:33:07 +00:00
Juan Denis 61bed36023 Merge branch 'dev' of https://github.com/jhd3197/ServerKit into dev 2026-08-19 22:31:29 -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
github-actions[bot] 1c28afca44 chore: bump version to 1.7.115 [skip ci] 2026-08-20 02:16:48 +00:00
Juan DenisandClaude Opus 5 0d42baf678 fix(tests): set the test-count floor to the clean-checkout number
4431 was measured on a dev box that has four git-ignored extension copies
installed under backend/app/plugins/ (cloud-provision, ftp, remote-access,
status). test_builtin_extension_drift.py parametrizes over the on-disk
intersection of builtin-extensions/<ext>/backend and app/plugins/<ext>, so
those four contribute 3+3+5+3 = 14 cases that no clean checkout can collect
— CI collects 4417, and the ratchet went red for a suite that had lost
nothing.

This is the failure mode check_test_count.py's own docstring warns about (a
floor --update'd above what a clean checkout can reach). 4417 is still a
raise of 108 over main's 4309, matching the 107 tests this branch adds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 22:09:59 -04:00
github-actions[bot] 343b4e3b14 chore: bump version to 1.7.114 [skip ci] 2026-08-20 01:54:09 +00:00
Juan DenisandClaude Opus 5 c8afc7ccd2 fix(workspaces): hide the creation entry points from non-admins
Creating a workspace became admin-only server-side, so the topbar button, the
empty-state action and the `?focus=create` deep link were all offers of a
guaranteed 403. Non-admins get an empty state that says who to ask instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 21:44:56 -04:00
Juan DenisandClaude Opus 5 9e18e35494 docs(authz): write down the default-deny and one-boundary rules
Both are now enforced by tests that fail the build, so they belong where the
"adding a feature" checklist is rather than in a commit message nobody
re-reads. The one-boundary rule is the trap this round fell into: stacking
`@jwt_required()` outside a policy decorator looks like belt-and-braces and
is actually a regression, because it rejects API-key callers before the
policy can authorize them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 21:44:56 -04:00
Juan DenisandClaude Opus 5 7aa3286932 test(authz): default-deny backstops plus proving tests for the gates
Two nets, deliberately complementary, because neither alone would have caught
the advisory:

- `test_route_authz_sweep.py` fires every mutating route as a viewer and
  fails on any 2xx outside a reviewed `VIEWER_WRITABLE` list. It proves
  *enforcement*, but a route that 400s or 404s on a dummy request before its
  gate runs is invisible to it — `POST /workspaces/` 400s on an empty body,
  so the net would never have seen the bug it exists for. Hence the targeted
  Part A: the fixed routes must answer a viewer and a developer with 403, and
  must not block an admin.
- `test_route_authz_static.py` proves *coverage* from the source instead: every
  mutating route in `url_map` either carries a `_sk_authz` decorator, reaches
  an authorization primitive through its module's call graph, or sits in
  SELF_SCOPED / PUBLIC_ALT_AUTH / STATELESS with a written justification.
  Staleness checks keep an allowlist slot from outliving the route that
  earned it, and a SELF_SCOPED entry must still resolve the caller's identity
  — calling `get_current_user()` is necessary but not sufficient, which is
  exactly how `create_workspace` looked self-scoped while creating a global
  row.

Both nets are blind below the API layer and to public routes, so
`test_authz_round3_guards.py` covers the three fixes whose shape they cannot
express (API-key scope widening, demo-deploy job creation, nginx diff
containment), the webhook cases land beside the rest of the gateway surface,
and the shared-resource tag gate lands with its own blueprint's tests. Each
comes with its non-vacuity case: the same call succeeds for a caller the gate
admits.

`migrations/history` gets tests because it had none — which is why the
NameError in it shipped.

Ratchets: JWT-only census 605 -> 577 (the routes that moved to policy
decorators), the controller-boundary baseline loses the one persistence
crossing `complete_onboarding` no longer makes, and the collected-test floor
rises to 4431.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 21:44:45 -04:00
Juan DenisandClaude Opus 5 47ec555915 fix(security): a webhook with no signature is as unauthenticated as a wrong one
The inbound gateway receiver is public by design — the HMAC signature IS the
credential — but the reject branch read `if not signature_valid and
signature_header`, so it only fired for a request that bothered to sign. A
delivery with no `X-Hub-Signature-256` at all skipped the branch entirely and
was logged, filtered and forwarded to the endpoint's `forward_url`. Every
endpoint row carries a non-null secret, so there is no legitimate unsigned
caller to preserve; the check is now unconditional and reports which of the
two cases it refused.

`preview_diff` joins a request-body `domain` straight onto SITES_AVAILABLE
and returns the current file's contents inside the diff, which makes a
traversing value an arbitrary file read. The guard lives in the service, not
only in the route, so every future caller inherits it, and the route now
surfaces the refusal as a 400 instead of handing the error dict back as a
200.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 21:44:28 -04:00
Juan DenisandClaude Opus 5 031142111d fix(authz): give every mutating route an explicit authorization decision
GHSA-r4q7-x795-vr4j was one forgotten check, but the audit behind it found
the same shape across the surface: `@jwt_required()` authenticates, it does
not authorize, and ServerKit has no default-deny to catch the difference.
This closes the routes that audit left open — fleet-monitor alert
acknowledge/resolve, recycle-bin restore, nginx test/diff, registrar
sync/test, server-template drift checks, uptime tracking on/off, server ping
and remote compose ps/logs, refresh-capabilities, template
test-db-connection, deploy branches, buildpack detect — plus three inline
gaps: a viewer could widen their own API key's scopes, the demo-deploy flag
was doing double duty as an authorization one, and tagging a shared resource
had no write check at all while attaching one did.

`_ensure_group_mutable` only ever blocked *system* queue groups, so every
queue_bus mutation now also calls `_ensure_group_accessible` (owner-or-admin)
— renaming, draining or deleting another user's group was open to any
authenticated caller.

The rbac policy decorators authenticate through `auth_required()`, so they
replace `@jwt_required()` rather than stack under it; stacking rejects
API-key callers before the policy can authorize them
(tests/test_auth_decorator_boundaries.py enforces this). Where a route then
needed the caller it reads `get_current_user()` — `get_jwt_identity()` raises
for an API-key request. The decorators also carry a `_sk_authz` marker so a
static test can see the decision from the source.

`migrations.get_migration_history` moves to `require_admin_user()` for the
same reason its siblings did: the inline `User.ROLE_ADMIN` check it kept was
already reading two names this module no longer imports, so the route raised
NameError for every caller, admin included.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 21:44:17 -04:00
Juan DenisandAlex a92d40a042 fix(workspaces): require admin role to create workspaces
POST /api/v1/workspaces/ was gated only by @jwt_required(), so any
authenticated user - including the read-only viewer role - could create
a workspace, automatically become its owner, then add arbitrary members
and mint workspace-scoped API keys (GHSA-r4q7-x795-vr4j, CWE-862).

Add @admin_required, matching the owner/admin checks every other
mutating workspace route already enforces, plus a regression test
asserting viewer and developer JWTs get 403 while an admin still
creates.

Co-authored-by: Alex <134992700+campguyuz@users.noreply.github.com>
2026-08-19 19:16:42 -04:00
Juan Denis ec78947157 Merge branch 'dev' of https://github.com/jhd3197/ServerKit into dev 2026-08-19 19:01:54 -04:00
Juan DenisandClaude Fable 5 3d3395d3ad ci(release): cap the tarball build at 30 minutes
The amd64 tarball job on run 32270325886 hung inside apt-get update
(unreachable azure apt mirror on the runner) and sat for GitHub's
6-hour default job timeout. Normal runs finish in ~13 minutes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 17:34:32 -04:00
Juan Denis 46a94c938d Merge pull request #104 from jhd3197/dev
Stop the panel guessing, and give every answer one door
v1.7.113
2026-08-19 11:29:00 -04:00
github-actions[bot] 3e0a5618e9 chore: bump version to 1.7.113 [skip ci] 2026-08-19 14:45:28 +00:00
Juan DenisandClaude Fable 5 de454527f5 fix(tests): docker-probe test patches the door, not the removed subprocess import
test_docker_probe_exception_is_unknown_not_absent monkeypatched
ips.subprocess.run — reaching through the service's namespace to the global
subprocess module, which only worked while install_profile_service still
carried its orphaned 'import subprocess'. The probe execs through
run_checked() since the door migration; the test now fails that door
directly, exercising the same except-Exception -> None (unknown) path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 10:35:23 -04:00
Juan DenisandClaude Fable 5 a8ba79348e docs(readme): correct the stats table to measured, gate-backed numbers
The table drifted across the convergence run: 3,874 tests (floor is now
4,309 and CI-enforced, written as 4,300+ so it stays true as tests are
added), 107 blueprints (registry test pins 109), 106 templates (bundled
index carries 118), 1,519 endpoints (a clean checkout's /api/v1 url_map
measures ~1,357 once locally-installed extension routes are excluded,
written as 1,350+), 65 screens (62 page modules today, written as 60+).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 10:16:19 -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 5448694c8d refactor(app): SERVERKIT_SKIP_BACKGROUND reads through the env door
The last hand-rolled truthiness read in the app factory: it accepted only
the literal '1', invisible to the env-door ratchet's pattern. env_bool()
keeps '1' working (the updater's contract) and adds the door's documented
true/yes/on spellings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 10:13:44 -04:00
Juan DenisandClaude Fable 5 dcdd60b609 docs(env): document the ALLOW_* security opt-ins and demo/staging flags
Five env_bool-parsed flags had no line in either .env.example. The two
ALLOW_* switches are security-relevant opt-ins (pip runs with backend
privileges; private download URLs) and must be discoverable; the demo and
staging flags are documented alongside them so the gap stops being unnamed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 10:10:59 -04:00
Juan DenisandClaude Fable 5 0e60a0c2b2 chore: drop the imports the door migrations orphaned
Twenty-one imports whose last caller left during the plan-76/77 sweeps:
mostly 'import subprocess' in services now routed through the shared
runners, plus time/requests in the CachedRemoteIndex adopters, threading
in agent_gateway, os in api/system, User in api/bandwidth, run_privileged
in nginx_advanced_service, and the dead SENSITIVE_SETTING_PARTS alias in
audit_service (zero importers). Each removal AST-verified unused before
deletion; all modules compile and the app factory boots.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 10:10:40 -04:00
Juan DenisandClaude Fable 5 1a503240ba fix(ci): scope the test-count guard to tests/ and correct the inflated floor
The guard ran a BARE pytest --collect-only from backend/, so --update on a
dev box swept git-ignored dev-data/ into the count and wrote a floor (4591)
that no clean checkout can collect — CI collects 4309, and the ratchet job
has been permanently red since 2026-08-18, which is a guard nobody can act
on. The floor's history shows the inflation entered at e6e605d1 (+314 with
only +46 test defs in the range it spanned).

Collection is now scoped to tests/ (matching what the CI shards run), a
non-zero collection exit refuses to report a partial count instead of
silently deflating it, and the floor is LOWERED 4591 -> 4309 — the value a
clean checkout actually collects; a dev checkout with sibling-repo
extension tests collects 4324, still above the floor. This is a deliberate,
documented decrease: no tests were removed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 10:09:21 -04:00
Juan DenisandClaude Fable 5 0e8ab745fb fix(tests): stub-kit and popen-guard samples no longer depend on the host's ufw
Three stub-kit dispatch tests used ['ufw', ...] as an arbitrary example
argv and one popen-guard test expected the real Popen to fail on it. Both
assumed ufw is absent on the running machine — true on every dev box, false
on CI's runners, where ufw exists sbin-only: the sbin guard fired inside
the stub path (SbinPathError instead of the scripted result) and the real
exec succeeded instead of raising FileNotFoundError. Red on every CI run
since the stub kit landed; invisible in local runs.

The dispatch tests now pin the guard's view of ufw to 'absent' (they test
dispatch mechanics, not the guard), and the guard test execs a name that
exists on no machine.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 10:08:00 -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 8753ae6768 docs(plans): fix two pre-rebase hashes and the drifted Server query-site count
The plan-76 completion record cited a75f435f (clipboard) and 3ac566cb (first
poller wave) — pre-rebase hashes unreachable from HEAD; the landed commits
are e1a5a5e1 and 1503c493. The plan-77 record's '98 query sites' for the
Server SoftDelete tail measures 99 today, so it is recorded as approximate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 09:52:08 -04:00
Juan DenisandClaude Fable 5 0034b25a55 docs(architecture): describe the auth policy layer, typed errors, and realtime registry
Four passages still described the pre-convergence world: bare @jwt_required()
as the route guard (now the frozen legacy population behind auth_required()
and the rbac identity door), plain JSON errors (now typed envelopes with
request ids), sockets.py as a pile of handlers (now the channel registry,
room grammar, and run envelope), and deploy_log/deploy_status as the deploy
channel (now a compatibility dual-emit under run_log/run_status). Also names
the model mixin suite and canonical status vocabulary in the Models row.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 09:51:32 -04:00
Juan DenisandClaude Fable 5 06dacf8310 docs(security): current release line, drop dead audit link, describe API auth
The supported-versions table still said 1.6.x while 1.7.112 ships; the
SECURITY_AUDIT.md link pointed at a file that has never existed in git
history. Adds a short API Authentication section describing the JWT-only
default with per-route API-key opt-in through the rbac policy decorator.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 09:46:50 -04:00
Juan DenisandClaude Fable 5 ed77f169c5 docs(roadmap): bring Recently Completed current through the convergence run
Adds the user-facing work the last 107 commits shipped: managed-database
Recycle Bin, the typed error envelope with request ids and no-swallowed-
crashes guarantee, visibility-aware polling plus the unified run stream,
tunnel reconciliation on the long-poll transport, and surfaced credential
encryption failures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 09:45:30 -04:00
github-actions[bot] 62d02682c2 chore: bump version to 1.7.112 [skip ci] 2026-08-19 12:44:53 +00: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 3dbd234eb6 fix(ratchets): reconcile plan-77 additions with the standing gates
The full-suite run tripped four ratchets on plan-77's own additions —
exactly their job: /api/v1/runs moves onto auth_required (the JWT-only
ceiling holds at 605) and behind run_log_service.list_run_logs (no
model access in the controller); the core-blueprint snapshot records
runs_bp deliberately (108 -> 109 + sha); sso_service joins the crypto
allowlist (C2 dual-read lives there by design); the controller-boundary
baseline re-records the two pre-existing ManagedDatabase crossings under
their B5 query_active spelling (count unchanged).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 06:04:59 -04:00
Juan DenisandClaude Fable 5 a3e907e97f docs(plan-77): completion record — decisions, scope calls, retained tail
All workstreams A-G executed 2026-08-19; open questions resolved
(D1 read-edge normalize, C2 permanent dual-read on the one key, B2
full-dict + _query shaping, E1 dual-emit); deliberate tail named with
owners (Server soft-delete pass, template/security_feed remote-index
non-adopters, the two raw log channels). BASELINE_COUNT 4460 -> 4591.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 05:50:35 -04:00
Juan DenisandClaude Fable 5 b07b988261 test(one-doors): route_rules fixture, create_app ratchet, authz one-liners (plan 77 G3/G4)
G3: session-scoped route_rules serves url_map checks without booting a
second app (each boot re-exposes the plan-64 state-leak classes);
test_fixture_scope_guard freezes the 13 remaining direct create_app
callers as a shrinking baseline (config_checkpoint_alias migrated as
proof). G4: assert_requires_auth/assert_admin_only promoted into
tests/factories.py; test_raw_infra_authz delegates to the shared shape.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 05:46:25 -04:00
Juan DenisandClaude Fable 5 f237d4f3b4 feat(databases): ManagedDatabase adopts SoftDelete into the Recycle Bin (plan 77 B5)
First of the regret-ranked adoptions: a plain delete now tombstones the
row (migration 088; no unique constraints, so no partial-index rewrite).
The database content and its managed BackupPolicy survive the bin window
— restore brings everything back intact; PURGE removes the policy but
NEVER drops database content; the explicit ?drop=true path stays
immediately destructive. Active read paths (list/get/adopt/manifest/env
references/app detail) switch to query_active(); the backup executor
deliberately stays tombstone-blind so bin-parked data keeps backing up.
Server (98 query sites, fleet enrollment + HMAC secrets) is the next
ranked adoption — deliberately its own pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 05:43:53 -04:00
Juan DenisandClaude Fable 5 9860c6f344 refactor(crypto): fold the parallel Fernet stacks into utils/crypto (plan 77 C2)
env_variable's class-level SECRET_KEY-derived Fernet (reached
cross-model by shared_resource) and sso_service's private stack now
WRITE through the one key path (SERVERKIT_ENCRYPTION_KEY) and dual-READ:
one path first, legacy key second — permanent window, so rows never
rewritten keep decrypting and no bulk decrypt-all/re-encrypt-all pass
ever runs (PR #94 postmortem). The '[DECRYPTION_ERROR]' sentinel and
None-on-failure token semantics are preserved.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 05:37:32 -04:00
Juan DenisandClaude Fable 5 5d875d8a09 refactor(scss): status-variant mixin — 174 re-rolled color pairs converge (plan 77 D4)
@mixin status-variant($color, $bg, $border) in _mixins.scss is the one
shape for coloring status-bearing elements; every mechanical
background/color token pair across 40+ page/component partials now
includes it (pass-through args -> identical compiled CSS values, build
green). check-status-scss.mjs holds the raw-pair count at zero and runs
inside npm run lint.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 05:35:28 -04:00
Juan DenisandClaude Fable 5 2e3a735d5a refactor(models): SerializableMixin — mechanical to_dicts become introspection (plan 77 B2)
Opt-in to_dict over __table__.columns: auto-isoformat datetimes,
*_encrypted always dropped, __serialize_exclude__ for redacted columns,
serialize_extra() for computed fields (named to dodge ServerMetrics'
literal 'extra' column). 20 purely-mechanical to_dicts across 15 files
convert; genuinely custom shapes keep their hand-written methods (plan
75 §F's ruling stands). Field selection stays api/_query.py's job.
Gate: key-for-key parity against the pre-conversion key sets frozen
from git HEAD (only the Timestamp pair may be additive).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 05:33:17 -04:00
Juan DenisandClaude Fable 5 57c0dc3956 feat(utils): CachedRemoteIndex — one remote-catalog engine (plan 77 F1)
The fetch -> TTL -> last-good -> bundled pipeline that four services
re-implemented (and that produced the bundled-index and proxy-rewrite
incidents) now lives once in utils/remote_index. The extension registry
and theme registry become thin normalizers over it; the registry gains
the failure-retry throttling the theme copy had already hardened to
(last-good under full TTL, bundled held only error_ttl). Deliberate
non-adopters, documented: template_service (keyed per-repo cache with
targeted invalidation) and security_feed (DB-durable last-good for a
daily job). Engine unit tests pin the semantics; registry/theme/signing
suites stay green (test seeds updated to the shared cache seam).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 05:27:10 -04:00
Juan DenisandClaude Fable 5 01b82955f9 feat(cache): ttl_cached decorator on CacheService; two TTL dicts migrate (plan 77 F2)
One TTL-memoization shape (Redis-backed where configured, in-memory
otherwise, None results never cached, .invalidate() per key).
install_profile_service capability probe and resource_tier_service tier
probe drop their hand-rolled module-level timestamp dicts. Ratchet with
a documented allowlist for caches whose semantics genuinely differ
(lock-guarded non-JSON snapshot, cache-forever catalog, the F1
remote-index family).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 05:20:59 -04:00
Juan DenisandClaude Fable 5 a9e76cf317 feat(frontend): useServerStream hook — one join/listen/cleanup dance (plan 77 E4)
joinServerStream()/useServerStream() own connect + join_room + listener
+ teardown, including the re-join-on-reconnect step the hand-rolled
copies missed (a reconnect silently loses server-side rooms).
JobProgressModal adopts the hook form; CloudflaredTab's imperative login
flow adopts joinServerStream. RemoteTerminal keeps its role-gated
subscribe_terminal dance (a different channel contract).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 05:14:30 -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 bae6e18578 test(deploy-console): drive the registry-generated subscribe handler (plan 77 E2)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 05:05:11 -04:00
Juan DenisandClaude Fable 5 c8a5d3a39c refactor(realtime): declarative channel registry; dead pipeline_* retired (plan 77 E2)
register_channel() generates each subscribe/unsubscribe pair with ONE
auth model (connected-identity base check + optional per-channel gate)
and rooms built on sockets_rooms; metrics, container_status, terminal
and deploy convert. The pipeline_* channel (zero callers anywhere) is
deleted. Gate: per-channel auth tests driving the generated handlers;
ratchet freezes the two remaining raw handlers (file/container logs,
which own per-sid OS resources).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 05:04:28 -04:00
Juan DenisandClaude Fable 5 ff501b51ad feat(realtime): BackgroundLoop helper; sockets broadcasters adopt it (plan 77 E5)
One shape for process polling loops: Event-based stop (immediate, not
sleep-bounded), lock-guarded idempotent start/stop, optional app-context
per tick, real logger error handling (the sockets loops reported errors
via print() — unreadable in prod), and a run_while predicate for
subscriber-scoped loops. The metrics and container-status broadcasters
convert; thread_ownership now carries ONE entry for the helper's spawn
site instead of a row per hand-rolled thread.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 05:01:10 -04:00
Juan DenisandClaude Fable 5 00eac38d5d feat(realtime): one room grammar + mirrored event constants (plan 77 E3)
app/sockets_rooms.py owns every Socket.IO room-name builder (the grammar
lived in ~25 scattered f-strings); app/constants.py SOCKET_EVENTS is
mirrored verbatim by frontend/src/constants/events.js and a contract
test parses the JS so a renamed event fails in CI instead of silently
killing a stream. sockets.py / agent_gateway / notification bus and the
two frontend components that hand-derived rooms now use the builders;
ratchet forbids new room f-strings in the realtime layer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 04:58:49 -04:00
Juan DenisandClaude Fable 5 f550ddc04f refactor(deploy): deployment_runner rides RunLifecycleMixin transitions (plan 77 D2)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 04:55:13 -04:00
Juan DenisandClaude Fable 5 0e1ebee601 refactor(deploy): DeploymentJob adopts RunLifecycleMixin (plan 77 D2)
The 11 hand-written status+completed_at(+error_message) transition
blocks in deployment_job_service collapse into mark_running/
mark_succeeded/mark_failed. The model keeps its stored 'succeeded'
spelling via the mixin override until this domain's data migration;
error text routes through __error_attr__ = 'error_message'.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 04:54:38 -04:00
Juan DenisandClaude Fable 5 ba09ec75b6 chore(frontend): wire status one-door ratchet into npm run lint (plan 77 D3)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 04:50:15 -04:00