Commit Graph
2228 Commits
Author SHA1 Message Date
Juan DenisandClaude Fable 5 ab13972cfc chore(tests): raise BASELINE_COUNT floor 4687 -> 4732
Locks in plan 82's suites: route contract, migration schema drift,
restored survey observed-mode tests, stub-adoption ratchet, php pool
guards, fail2ban orphan sweeper, WAF drift/repair, real-binaries
crontab + nginx legs, API surface inventory. Measured with
SERVERKIT_CLEAN_COLLECT=1.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-27 05:08:17 -04:00
Juan DenisandClaude Fable 5 0fe6d090fd test(api): pin the served API surface to a committed inventory
OpenAPIService.generate_spec() existed but nothing snapshotted it, so a
route rename/removal/method change shipped invisibly to API-key
consumers, the CLI, and agents. docs/API_SURFACE.md now pins the sorted
METHOD /path inventory (1418 operations) with the same
regenerate-and-review contract as docs/MIGRATION_INVENTORY.md: an
intentional surface change regenerates the doc in the same commit
(SERVERKIT_UPDATE_API_SURFACE=1 pytest
tests/test_api_surface_inventory.py) so the diff is the review.
Method+path only on purpose — summaries and schemas churn without
breaking anyone. Named *_inventory so preflight's globs run it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-27 05:03:40 -04:00
Juan DenisandClaude Fable 5 a0a111c071 test(real-binaries): run cron and nginx service code against real parsers
The panel's crontab/nginx code only ever met the fake_subprocess stub —
the 8-distro matrix runs stubs and the container legs run 12 detection
tests, so a syntax the real parser rejects failed on a user's box, not
in CI (the #117 class).

- new `real_binaries` marker (registered in conftest); the tests also
  gate on SERVERKIT_REAL_BINARIES=1 so ordinary suite runs never mutate
  a machine's crontab by surprise. Local loop (WSL works):
  SERVERKIT_REAL_BINARIES=1 pytest tests -m real_binaries
- test_real_crontab_roundtrip: add -> list -> toggle -> remove through
  the real `crontab -`/`crontab -l`, operator lines preserved, name
  injection blocked; user crontab saved/restored around each test
  (verified green on WSL against the real binary)
- test_real_nginx_vhost_syntax: every render_site_config flavor through
  a real `nginx -t` in a tmp wrapper conf, plus a garbage probe so the
  harness can't go vacuous
- backend-ci gains a real-binaries job (ubuntu runner, apt nginx+cron)
- scripts-ci gains the probe-matrix job: scripts/test/probe-matrix.sh
  (real ufw/nginx/firewalld in 4 container shapes) existed since plan
  75 but was wired into NO workflow; the ubuntu-root-sbinless leg was
  re-verified green locally before wiring

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-27 05:03:31 -04:00
Juan DenisandClaude Fable 5 452a2d43ae fix(php): guard pool paths and stop fabricating unreadable pool configs
version and pool_name flow from the URL straight into a filesystem path
handed to privileged write/rm — a traversal pool_name could delete an
arbitrary *.conf via run_privileged(['rm', ...]), and a hostile version
escaped the pool.d directory the same way. Both are now validated at
the service door (create/delete/get share the helpers).

_parse_pool_config returned {} on any read error, so an unreadable
pool listed as "user: www-data, pm: dynamic" — fabricated defaults.
It now returns None and the listing reports the honest unknown
(user/listen/pm null + unreadable: true), per the probe-honesty rule.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-27 04:56:12 -04:00
Juan DenisandClaude Fable 5 1a27aa784c fix(fail2ban): make renamed-away jails findable and sweepable
The jail filename derives from the site name at enable time, so a site
renamed afterwards orphans its jail: disable_jail derives a path that
no longer exists (honestly reports removed: False) while the stale
jail keeps banning against a log nginx no longer writes — invisible
to every name-derived lookup.

- list_jails() scans JAIL_DIR for serverkit-*.conf and parses the site
  label + logpath out of the rendered header, the only view that still
  sees orphans
- cleanup_orphans(active_keys) removes ServerKit jails no live key
  derives any more (operator files never touched), reloading once
- the _sanitize collision (my.site / "my site" -> one shared jail) is
  pinned as documented behavior: changing the derivation would orphan
  every existing install's jail files

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-27 04:54:13 -04:00
Juan DenisandClaude Fable 5 67aa529197 fix(waf): keep the include through drift detection and repair
Drift byte-compared the live vhost against the bare template render,
but an app with an enforcing WAF policy carries the post-render
include — so every WAF'd app read as permanently drifted, and one
"repair" click regenerated the vhost without the include: silently
unprotected while the DB still said enforcing.

- the injector is now a text-level function (inject_include_text) so
  the live wiring and drift's expected render share one implementation
- _nginx_render_expected injects the include for enforcing policies,
  making a correctly-wired vhost compare clean
- _nginx_repair re-runs WafService.apply after regenerating, so a
  genuine repair re-wires protection instead of stripping it

Basic auth (WordPress-site-scoped) and the analytics tracker
(plugin-scoped) have the same erase-on-regen exposure and are tracked
in plan 82 §H — their state lives outside the app row this check walks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-27 04:51:38 -04:00
Juan DenisandClaude Fable 5 c7fd2f2033 test(ratchet): census hand-rolled subprocess stubs in the suite
tests/subprocess_stub.py is the shared stubbing kit — hard-failing
UnscriptedCommand, sudo/basename normalization, sbin-guard still in
force — but adoption stalled half-way: 26 sites across 9 test files
still monkeypatch subprocess by hand, execs the runtime guard never
sees. Every other convergence here has a census + ceiling; this one
did not. Same contract as the rest: AST-counted over tracked files,
ceiling 26, may only go down; patching a service door
(run_privileged etc.) is the blessed pattern and not counted.
Preflight picks both files up by its existing globs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-27 04:47:38 -04:00
Juan DenisandClaude Fable 5 39501e4262 test(migrations): gate model <-> migration schema drift
The test suite's schema comes from db.create_all() (see conftest), so
none of the 93 alembic revisions had ever been executed by a test — a
model column with no migration keeps every test green while every real
upgrade dies with "no such column". New test runs `flask db upgrade`
against a throwaway sqlite file in a subprocess and diffs the migrated
schema against db.metadata in both directions.

First run caught real drift: migrations 059/065/068 create columns
(events_json, management_mode, allow_agent_update_observed) that the
models had lost in the recovery rebuild — restored in the previous
commit, which also brought two 500ing survey routes back to life.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-27 04:45:45 -04:00
Juan DenisandClaude Fable 5 69136ef48f fix(servers): restore the Observed-mode surface lost in the recovery rebuild
Migrations 065/068 have always created servers.management_mode and
servers.allow_agent_update_observed, but the model fields,
MANAGEMENT_MODES, is_managed, and agent_registry.observed_blocked_count
were lost in the 2026-07 data loss — POST /management-mode and
GET /observed-status raised AttributeError, and a mode flip that got
past validation would land on a transient Python attribute and
evaporate. test_survey_api carried skip-stubs documenting the hole.

Restored: the two columns (specs mirror the migrations), the mode
constant and is_managed property, to_dict emission, and a minimal
observed-block counter on the registry. The four skip-stubs are now
real tests, including one proving the flip persists across sessions.
The dispatch-level Observe guard remains hollow and is tracked in
plan 82 §H. events_json on notification_preferences is likewise
re-mapped (column carried by migration 059; its resolution surface is
still lost) so pre-loss override data survives.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-27 04:45:37 -04:00
Juan DenisandClaude Fable 5 6dfbc0df38 test(contract): pin frontend API paths to real backend routes
Nothing verified that the ~1200 URL literals in
frontend/src/services/api/*.js resolve to rules in the backend url map,
so a backend route rename surfaced as a dead button found by a user.
New test statically extracts every this.request() literal (mirror of
test_socket_contract.py's cross-language pattern) and asserts each
matches an /api/v1 rule from the session app's route_rules; paths under
a builtin extension's plugin.json url_prefix are excused only while
that manifest still declares the prefix, and a second test keeps the
allowlist from rotting.

First run caught 78 unresolved paths. Extension-owned families
(email/ftp/cloud/status/tunnels) were excused; the rest were dead
frontend code calling routes that no longer exist, removed here:
- 11 workflow wrappers in apps.js (the workflow engine left the
  backend in v1.7.33; zero callers remained)
- notification catalog/org-defaults wrappers (routes never existed;
  zero callers)
- 6 WordPress standalone lifecycle wrappers (no routes, no callers;
  the live status probe stays, allowlisted as extension-provided)

Frontend suite still green after the deletions (141/141, lint clean).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-27 04:33:57 -04:00
Juan DenisandClaude Fable 5 f5e3816add ci: run frontend tests and widen the release gate
The frontend's 141 node --test assertions were invocable but invoked by
no workflow — written insurance never cashed in. frontend-ci now runs
npm test after lint (verified green locally: 141/141 in ~2s).

release.yml's ci-gate covered backend-ci only, so a release could ship
with a red frontend or drifted builtin extensions. The gate now also
requires frontend-ci and extensions-ci (both fast, both already
workflow_call-able). scripts-ci stays ungated on purpose — its
update-e2e leg takes ~45 min — with a comment showing how to add it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-27 04:24:06 -04:00
Juan DenisandClaude Fable 5 d3a7d4bd73 chore(tests): raise BASELINE_COUNT floor 4519 -> 4687
Locks in the round-trip suites added for the #117-class fixes (ssh
keys, env export/import, waf vhost injection, ufw parser parity,
postfix maps) plus tests accumulated since the floor was last set.
Measured with SERVERKIT_CLEAN_COLLECT=1.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-27 04:20:27 -04:00
Juan DenisandClaude Fable 5 7211560768 fix(postfix): match virtual-map entries by address token, not substring
add_domain deduped with a raw substring test, so with sub.example.com
already mapped, adding example.com was a no-op that still reported
success and mail for the new domain never routed. remove_domain
filtered alias lines with a bare `@domain in line` test, which also
deleted example.com.br entries. Appends went through tee -a with no
trailing-newline guard, so a truncated map glued the new entry onto
the last line, corrupting both and making them unremovable.

- add_domain dedupes on the line's domain field
- remove_domain drops only lines whose address tokens are at exactly
  that domain (com.br superstrings and sub.example.com both survive)
- map appends heal a missing trailing newline first

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-27 04:10:14 -04:00
Juan DenisandClaude Fable 5 fee322734b fix(firewall): share one ufw parser between the listing and the guard
_get_ufw_rules and _ufw_numbered_rules each parsed `ufw status numbered`
with their own regex, and the listing's omitted LIMIT — a `ufw limit
22/tcp` rule was invisible in the panel's rule table while the
SSH-removal guard still counted it, so delete-by-number targeted rows
the user could not see. Both paths now match with the shared module
_UFW_NUMBERED_RE, so the table and the guard always agree row for row.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-27 04:08:12 -04:00
Juan DenisandClaude Fable 5 cc5323d8a9 fix(waf): wire the include into the serving block, not the SSL redirect
_inject_include inserted after the first "server {" in the vhost. On an
SSL site the first block is the HTTP->HTTPS redirect from
SSL_REDIRECT_TEMPLATE, so ModSecurity was wired into a server that only
issues 301s while the real HTTPS server stayed unprotected — and
apply() still reported wired: True. The micro-cache injector already
guards this exact trap; the WAF include now does too: redirect-only
blocks (a 301 return with nothing served) are skipped and the include
lands in the first block that actually serves the app.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-27 04:06:45 -04:00
Juan DenisandClaude Fable 5 de609526df fix(env-vars): make .env export/import lossless and injection-safe
export_to_env_format escaped backslashes and quotes that parse_env_file
never unescaped, so every export -> import cycle grew literal
backslashes in the value; newline values were written as raw multiline
quotes the parser mangled; and the user-supplied description landed as
a raw comment line, so a newline in a description injected brand-new
KEY=value pairs on re-import.

- exported values now escape \, " and newline onto a single line, and
  the double-quoted import path decodes exactly those three sequences
  (unknown sequences like C:\path stay as written)
- descriptions are whitespace-collapsed before being written as a
  comment, the same hardening the cron #117 marker got
- a None value exports as empty instead of raising TypeError

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-27 04:05:05 -04:00
Juan DenisandClaude Fable 5 3de88b5f82 fix(security): join SSH key ids to the lines delete actually pops
get_ssh_keys numbered keys over raw authorized_keys lines (comments and
blanks included) while remove_ssh_key popped that id from a list of key
lines only, so any comment banner shifted every id and delete-by-id
removed the wrong key — or refused the last one as "Invalid key ID".
The rewrite also hoisted all comments to the top, shifting ids again.

- both sides now index the same sequence: non-blank, non-comment lines
  in file order; delete drops the line in place so comments keep their
  position
- add_ssh_key dedupes on the key-data token instead of a raw substring,
  and normalizes a missing trailing newline before appending so a new
  key can't be glued onto the previous line

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-27 04:02:35 -04:00
Juan Denis 1c7501d1d3 Merge branch 'dev' of https://github.com/jhd3197/ServerKit into dev 2026-08-27 03:45:51 -04:00
Juan DenisandClaude Fable 5 719d29c8a5 fix(cron): join crontab lines back to their metadata (#117)
Creating a named cron job on Linux wrote the display name into the
"# ServerKit Job:" marker comment and list_jobs re-parsed the crontab
under synthetic cron_<index> ids, so the metadata store (name,
description, enabled, application_id) never joined back: the job listed
as unnamed and every mutation route answered "Job not found" — the only
way out was crontab -e.

- add_job now always writes the job_id into the marker comment (the
  name rides along, whitespace-collapsed so it cannot inject lines)
- list_jobs recovers the id from the marker, falls back to a
  schedule+command match for pre-fix lines whose marker holds the name,
  and backfills metadata-only jobs so disabled (commented-out) jobs
  stay visible and manageable
- remove_job/toggle_job also match shim-wrapped (tracked) lines, and
  remove drops a legacy marker comment left above the deleted line

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-27 03:42:01 -04:00
github-actions[bot] f9a0d95136 chore: bump version to 1.9.7 [skip ci] 2026-08-25 13:21:17 +00:00
Juan Denis 49edb33ee8 Merge main into dev to sync PR #118 2026-08-25 09:14:29 -04:00
Juan DenisandClaude Fable 5 41266dd8ee fix(env-vars): let an explicit null clear a variable's description
The PUT route guarded description with 'is not None', so JSON null never
reached EnvService.update_env_var — whose _UNSET sentinel explicitly
supports clearing. Key-presence check now forwards the null; absent keys
still leave the field untouched. Raised in PR #118 review.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 09:14:29 -04:00
github-actions[bot] 6df222a4fd chore: bump version to 1.9.6 [skip ci] 2026-08-25 12:37:30 +00:00
Juan DenisandClaude Fable 5 ffec0ca3ad fix(restore-points): json one-door model reads; modernize last-domain restore test
RestorePoint hand-rolled json.loads for payload/coverage — now reads
through JsonColumnMixin._json_read like every other model, keeping the
models-json ratchet green.

test_restoring_the_last_domain_re_enables_the_site mocked create_site
and counted enable_site calls, but since write_vhost became the one
enabling door (enable=True re-links sites-enabled and create_site goes
through it), write_app_vhost no longer calls enable_site separately —
test_site_domain_writer_does_not_enable_twice pins that. The restore
test now asserts through write_vhost's enable flag instead, still
failing if a restore path ever opts out of enabling.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 03:13:06 -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 DenisandClaude Fable 5 6f51b3b3c0 fix(i18n): drop orphaned noChange/envVar locale keys
ConfigDiffModal renamed noChange to noChangeValue and DeploymentTimeline
renamed envVar to envVarCount, but the old keys were left behind in the
16 translated locales, failing the i18n lint orphan check.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 02:44:09 -04:00
Juan Denis 69182c3d95 feat(restore-points): add server restore points UI 2026-08-23 23:40:22 -04:00
Juan Denis d060e77e99 feat(servers): add merged restore timeline 2026-08-23 17:16:29 -04:00
Juan Denis aa82fc4082 feat(restore-points): add authorized lifecycle API 2026-08-23 17:00:50 -04:00
Juan Denis cf2cce6041 feat(restore-points): add surface adapters and checkpoints 2026-08-23 16:41:02 -04:00
Juan Denis cd956d1cb0 feat(restore-points): add generic checkpoint core 2026-08-23 16:07:09 -04:00
Juan Denis ded7d54039 refactor(nginx): consolidate vhost writes 2026-08-23 15:50:53 -04:00
Juan Denis 44b3ff3a9d Preserve frontend assets in release builds
Adjust release cleanup/packaging to stop stripping image files from frontend/dist, which broke Vite hashed asset references at runtime. The script now excludes dist from image deletion, removes image-wide tar excludes, and adds a bundle integrity check that fails the build if referenced /assets files are missing.
2026-08-23 15:16:08 -04:00
Juan Denis 15cce6047b Merge pull request #116 from jhd3197/dev
Recipes, walkthroughs, disk reclaim, and a redesigned app shell
v1.9.5
2026-08-22 16:31:21 -04:00
github-actions[bot] e6d3477c3c chore: bump version to 1.9.5 [skip ci] 2026-08-22 19:07:39 +00:00
Juan Denis 7cb04576bc fix(styles): remove duplicate disk reclaim owner 2026-08-22 15:00:53 -04:00
Juan Denis a2901bf064 perf(deployments): skip unused plan parsing 2026-08-22 15:00:52 -04:00
Juan Denis 1e4922746c fix(operations): normalize recipe handoff descriptors 2026-08-22 15:00:46 -04:00
github-actions[bot] 853ca5ce3b chore: bump version to 1.9.4 [skip ci] 2026-08-22 18:55:56 +00:00
Juan Denis c756b61abe Merge remote-tracking branch 'origin/main' into dev 2026-08-22 14:49:20 -04:00
Juan Denis dd59a84619 fix(ci): keep version bumps behind green checks 2026-08-22 14:49:12 -04:00
Juan Denis 1fad04e880 fix(recipes): restore CI boundary contracts 2026-08-22 14:49:11 -04:00
github-actions[bot] ab0c3174b0 chore: bump version to 1.9.3 [skip ci] 2026-08-22 18:25:23 +00:00
Juan DenisandClaude Fable 5 2a5d18867f refactor(walkthroughs): the shell dock says Walkthroughs, not Recipes
Reserve the word Recipes for installers. The status-bar segment, dock
tab, hub aria-label and command-palette category now read Walkthroughs
(new app.walkthroughs.title/all keys; stale es allRecipes dropped).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-22 14:21:04 -04:00
Juan DenisandClaude Fable 5 93c8abbbbe feat(recipes): serverkit-recipes registry catalog with one-call installs
The curated serverkit-recipes index is consumed like the themes registry
(cached remote index, bundled fallback, SERVERKIT_RECIPES_REGISTRY_URL
override). GET /recipes/registry[/<slug>] browses it; POST /recipes/runs
now accepts a registry_slug plus server_id — targeting a server finds or
creates the per-workspace Recipes project — and validated non-secret
params that pre-supply handoff inputs so only true secrets pause the run.

Frontend: Recipes catalog page (cards, category chips, install drawer
with running-server picker, preflight table, up-front inputs and an
asked-during-the-run secrets preview) under new Services and Marketplace
tabs; runs land in the deployment console.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-22 14:20:55 -04:00
github-actions[bot] 82dc866c41 chore: bump version to 1.9.2 [skip ci] 2026-08-22 06:55:21 +00:00
Juan DenisandClaude Fable 5 d867b92c98 chore(i18n): sync locales for the shell console round
en.json regenerated from inline defaults (new statusbar/console/palette/
recipes strings; Prompture and old walkthrough-hub strings gone). es picks
up translations for all 26 new keys; every locale drops the now-orphaned
keys so the parity gate stays green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-22 02:43:38 -04:00
Juan DenisandClaude Fable 5 adf6b48bff style(buttons): prototype accent treatment for the primary button
Gradient fill (accent-bright to accent), accent glow, inner top highlight,
and a hover that lifts the gradient toward white — all from theme tokens so
runtime skins keep recoloring it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-22 02:43:27 -04:00
Juan DenisandClaude Fable 5 8b7cecb62e feat(palette): create tile grid + recipes; the sidebar + opens the palette
The palette's empty state now leads with a Create tile grid (shared list in
data/createItems.js) plus a '+ n more' shelf that folds in the less-common
targets and any extension-contributed 'New ...' commands, then a Recipes
group (picking one starts it and opens the recipes console). Create and
recipe entries also score into regular search results.

The sidebar '+' stops carrying its own little dropdown and opens this
palette instead — one door for every new-thing flow, like the prototype.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-22 02:43:24 -04:00
Juan DenisandClaude Fable 5 3f3ff156f1 feat(ai): dock the assistant into the shell console; drop Prompture branding
The right-side chat drawer now carries the shared console tab strip, so the
assistant reads as the right-docked tab of the same console the bottom
panels belong to (its close moved into the strip). The 'powered by
Prompture' lines are gone from the header and empty state — the empty state
now tells the user what to ask instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-22 02:43:15 -04:00