2331 Commits
Author SHA1 Message Date
Juan Denis 4e6b00431d Merge pull request #125 from jhd3197/dev
Fix the deletes that crashed on their own child rows
v1.9.18
2026-08-31 21:24:30 -04:00
Juan Denis 0e865e14a2 Merge branch 'main' into dev 2026-08-31 21:16:35 -04:00
github-actions[bot] c700e74752 chore: bump version to 1.9.18 [skip ci] 2026-09-01 01:11:27 +00:00
Juan DenisandClaude Fable 5 cc0597bf28 refactor(admin): move the owned-apps delete guard behind the service door
The user-delete guard 79a78c3c added queried Application straight from
the controller, tripping the API controller boundary ratchet. The count
now lives in application_restore.applications_owned_by — the module
that owns Application's recycle-bin semantics and documents why
tombstoned rows block too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 21:04:21 -04:00
Juan DenisandClaude Fable 5 27e6c0ba9a fix(i18n): prune the recycle-bin header keys the refactor orphaned
5bfb0f67 dropped the tab-local header but did not regenerate en.json,
so the lint gate failed on catalog drift. Regenerate en.json and delete
the three now-unused recycleBinTab keys from every locale bundle.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 20:55:35 -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 Fable 5 f34969eaac fix(models): close all 26 dangling-on-delete FKs + orphan sweep migration
Every NOT NULL FK from the KNOWN_DANGLING_ON_DELETE audit now declares
its parent-side relationship, so the delete-cascade policy door covers
it: AI conversations/pending actions, API keys, dashboards, event
subscriptions, login links, sent invitations, resource grants (users);
env vars + history, previews + settings, WAF policy (applications);
surveys, doctor results, proxy stack, tunnels (servers); manifests
(projects); DDNS hosts (zones); exposed services (tunnels); drills
(backup policies); rollouts (agent versions); site plugin installs
(WordPress sites); queue messages (groups). The ratchet set is now
EMPTY and stays that way.

One new deliberate exception: Workspace.projects — projects carry
environments and app groupings, so delete_workspace now refuses (409)
while any remain instead of silently destroying them (previously it
crashed with IntegrityError).

Migration 095 sweeps rows already orphaned on existing installs by
pre-fix hard-deletes — including an application's leftover env-var
secrets, which a reused SQLite row id could have attached to a future
app.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 20:25:29 -04:00
Juan DenisandClaude Fable 5 3a3ee9a1ba test(models): ratchet the FKs the cascade door cannot protect
Companion sweep to _delete_cascade_policy: 26 NOT NULL FK columns have
no parent-side relationship, so a parent hard-delete leaves their rows
dangling (SQLite FK enforcement is off, and a purged parent id can be
reused). Freeze the audited set so it may only shrink — a new model
declares the parent-side relationship (the door then handles its
delete) or consciously adds itself with a reason.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 20:11:35 -04:00
Juan DenisandClaude Fable 5 7fd90a6c87 refactor(models): one door for delete cascades on NOT NULL child FKs
Replaces the 29 hand-written cascade='all, delete-orphan' declarations
from c1d4c0af/1704d60b/79a78c3c with a single mapper hook
(_delete_cascade_policy): at configure time, every one-to-many whose
child FK is NOT NULL gets the delete cascade automatically — with a
NOT NULL FK the children cannot outlive the parent, so the only choice
is cascade or crash. New models (extensions included — registering one
re-fires the hook) are covered with zero ceremony; nothing to remember.

Relationships where the parent delete should be REFUSED instead opt
out in the module's DELIBERATELY_UNCASCADED registry with their
justification (User.applications guarded by delete_user's 409,
CloudProvider.servers with no delete path). The sweep test now proves
the door works rather than policing model authors, and pins the
registry so it can only shrink.

Model files revert to their pre-fix text: delete semantics live in one
place instead of twenty.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 20:04:12 -04:00
Juan DenisandClaude Fable 5 79a78c3c38 fix(models): repo-wide delete-cascade audit for NOT NULL child FKs
Follow-up to 1704d60b, prompted by 'can this happen elsewhere?' — a
mapper-wide audit found 22 more one-to-many relationships whose child
FK is NOT NULL but which lacked a delete cascade, so hard-deleting the
parent (user/server/workspace/template/plugin delete, deployment
retention pruning, WP site teardown) raised the same IntegrityError as
the Recycle Bin purge.

20 get the cascade: plugin installs, deployment diffs, metric alerts,
template assignments (both sides), WP activities/sync/promotion jobs
(both sides), workspace members/keys, and the per-user detail rows
(passkeys, OAuth identities, notification prefs, sanitization
profiles, source connections, workflows, workspace memberships).

2 stay uncascaded on purpose, now enforced: User.applications (delete_
user refuses with 409 while the user owns apps — apps must go through
app delete + Recycle Bin purge so containers/volumes are cleaned) and
CloudProvider.servers (no delete path exists; dropping VM inventory
must stay a deliberate decision). The invariant sweep is now global
across every mapper with exactly that allowlist, which may only shrink.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 19:47:32 -04:00
Juan DenisandClaude Fable 5 5bfb0f6747 refactor(recycle-bin): drop the page-local header
The tab-local title, retention blurb and standalone Refresh button
duplicated chrome the grid already provides (Refresh data lives in the
grid tools menu; per-row 'purges in Xd' carries the retention signal).
The page now leads with the view picker and table like other grids.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 19:35:53 -04:00
Juan DenisandClaude Fable 5 1704d60b05 fix(models): cascade ALL not-null children on application purge
Field follow-up to c1d4c0af: the live box then crashed on
deployments.app_id — dynamic relationships get the same FK null-out
on parent delete as the one-to-ones. Cascade every one-to-many from
Application whose child FK is NOT NULL (deployments, config
snapshots, image scans, SBOMs, image update checks, git deployments),
and pin the invariant with a mapper-inspection sweep so a future
NOT NULL child cannot reintroduce the purge crash.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 19:28:35 -04:00
Juan DenisandClaude Fable 5 c1d4c0af12 fix(models): cascade one-to-one child rows on application purge
Purging an app from the Recycle Bin crashed with IntegrityError
(NOT NULL constraint failed: container_scale_policies.application_id)
when the app had an auto-scale policy: with no delete cascade,
SQLAlchemy nulls the child FK on parent delete, and these one-to-one
rows declare it NOT NULL. Scale policy, sleep policy and wp_site all
share the shape; all three now ride the purge.

Reported on Discord (vaultwarden/hedgedoc delete-reinstall flow).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 19:22:43 -04:00
Juan Denis 339b5febe3 Merge pull request #124 from jhd3197/dev
Update the panel from the About tab, and stop shipping vite apps that host-block their own visitors
v1.9.17
2026-08-31 09:21:43 -04:00
Juan Denis b966f57533 Merge branch 'main' into dev 2026-08-31 09:06:19 -04:00
github-actions[bot] 846c52d665 chore: bump version to 1.9.17 [skip ci] 2026-08-31 12:41:00 +00:00
Juan Denis d01f7a2ee9 Merge branch 'dev' of https://github.com/jhd3197/ServerKit into dev 2026-08-31 08:34:51 -04:00
Juan DenisandClaude Opus 5 ebb53d8fcc fix(buildpack): serve vite builds with a host-agnostic static server
`vite preview` host-checks every request against `preview.allowedHosts`
and blocks any domain the repo's vite config doesn't list -- the deployed
container answered localhost probes fine and refused every request the
panel proxy forwarded ("Blocked request. This host ... is not allowed").
Preview is a dev convenience, not a production server, per vite's own docs.

The vite default start command is now `serve -s dist -l tcp://0.0.0.0:4173`
(SPA fallback, no host check); the generator preinstalls serve globally
when the start command needs it, since the repo never declares it. A
repo-authored start script still wins and gets no serve install.

The @docker_builds leg now sends a foreign `Host:` header to every booted
container -- the localhost-only probe was exactly the blind spot that let
this class through.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 08:31:34 -04:00
github-actions[bot] 8c915794c2 chore: bump version to 1.9.16 [skip ci] 2026-08-31 12:27:30 +00:00
Juan DenisandClaude Fable 5 fc89a979ef i18n: translate panel self-update strings in all 15 remaining locales
The self-update feature commit added the 10 new app.aboutTab keys to en
and es only; every other locale fell back to English in an otherwise
fully translated About tab. Add ar, bn, de, fr, id, it, ko, pl, pt, ru,
th, tr, vi, zh-Hans and zh-Hant, matching each file's existing register
(Sie/vous/formal).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 08:17:47 -04:00
Juan DenisandClaude Fable 5 160bf6dab7 feat(panel): one-click self-update from the About tab
The About tab could only detect a new release and link to it; applying the
update meant SSH and scripts/update.sh. Wire that script to the UI:

- panel_update_service launches update.sh via systemd-run in its own
  transient unit — a plain subprocess child would be killed when the
  updater restarts the panel's own systemd unit. Status is stateless
  (transient-unit state + the updater's own update-*.log) so it survives
  the mid-update backend restart.
- Capability gates keep the button honest: root systemd installs only.
  Docker installs get the docker compose pull instructions; dev servers
  and hosts without systemd-run get a reason instead of a button.
- GET/POST /api/v1/system/update (admin_required, confirm-gated,
  audited). API surface doc regenerated.
- About tab gains Update Now with inline confirm, live log tail while
  the update runs, a restarting phase that tolerates the panel being
  briefly unreachable, and success/rollback/timeout outcomes. New
  aboutTab locale keys in en + es.

The heavy lifting (blue/green switch, migrations, health check,
automatic rollback) stays in update.sh, which is unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 08:09:52 -04:00
Juan Denis a96fab2f7d Merge pull request #123 from jhd3197/dev
Generated node deploys install their build tooling, and CI now proves it
v1.9.15
2026-08-31 02:27:57 -04:00
github-actions[bot] 4b2e7ecbb5 chore: bump version to 1.9.15 [skip ci] 2026-08-31 06:11:32 +00:00
Juan DenisandClaude Opus 5 20bfcd4947 test(buildpack): run generated Dockerfiles through the real docker daemon
test_buildpacks.py asserts on Dockerfile TEXT, which cannot see a bug that
only fails at build or boot time -- the `--omit=dev` dead fallback shipped
`vite: not found` to a user's deploy. New @docker_builds leg (gated on
SERVERKIT_DOCKER_BUILDS=1, sibling of @real_binaries) runs the full path
per stack: detect() -> generate_dockerfile() -> `docker build` ->
`docker run` -> HTTP 200. Fixtures: vite (the regression), express with no
build step (prod-only install must still boot), flask, static. Wired as a
backend-ci job beside real-binaries.

Writing the flask fixture surfaced the same dead-at-boot class one layer
up: detection invents `gunicorn app:app` / `uvicorn main:app` as the start
command but never installed the server, so a repo that doesn't declare
gunicorn built green and died at boot. The build command now installs the
invented server (a no-op when the repo already pins it); Procfile-authored
start commands remain the author's contract.

All four stacks verified locally: build + boot + HTTP 200 in 53s.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 01:58:57 -04:00
Juan DenisandClaude Opus 5 4c95a7da9f fix(buildpack): keep devDependencies when a node app has a build step
The generated node Dockerfile ran `npm install --omit=dev || npm install`.
`--omit=dev` exits 0, so the fallback never fired and `npm run build` died
with `vite: not found` -- build tooling (vite, tsc, webpack) is a
devDependency, and so is the server behind preview-style start commands.

Install the full dependency set whenever a build command exists, and put
node_modules/.bin on PATH so locally installed CLIs resolve in both RUN and
CMD. Apps with no build step still install production-only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 01:48:02 -04:00
Juan Denis 2ef2d775a0 Merge pull request #122 from jhd3197/dev
The last four bundled flagships move out to their own repos
v1.9.14
2026-08-31 01:25:10 -04:00
github-actions[bot] 696dde66d8 chore: bump version to 1.9.14 [skip ci] 2026-08-31 05:00:01 +00: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 Opus 5 b764418b09 fix(status): re-sync the pre-bundled frontend manifest
30b780f4 corrected the serverkit-status description in two of its three
copies. The extension's manifest also exists at
frontend/src/plugins/serverkit-status/plugin.json, which is generated from
the builtin-extensions/ source by scripts/sync-builtin-frontends.mjs --
so Extensions CI went red on the builtin-frontend drift check, and
bump-version failed downstream of it rather than on its own.

Regenerated with the sync script rather than hand-edited; the diff is the
one description line.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 00:44:04 -04:00
Juan Denis 1cc2a1b142 Merge remote-tracking branch 'origin/dev' into dev 2026-08-31 00:42:09 -04:00
Juan DenisandClaude Opus 5 a31cc5d3a8 test(authz): ratchet plugin routes too, and write down why not to sweep them
PR #122 review suggested converting the serverkit-status routes from
@jwt_required() + an inline is_admin check to rbac decorators. Declined:
plan 76's recorded decision (2026-08-19) is that JWT-only is the
deliberate default, because every rbac decorator authenticates through
auth_required() and therefore GRANTS API-key callers access -- an
expansion of the authenticated surface, never a mechanical sweep.

Two gaps let that suggestion look uncontroversial:

1. jwt_only_census.py scanned only app/api, so the extension's 15 bare
   @jwt_required() routes were invisible to the ratchet. It now scans
   app/plugins as well; ceiling 577 -> 611 for the newly visible
   population (a scope widening, not a regression). Only the live copy
   is counted -- builtin-extensions/ is the same code twice and the
   drift guard pins them equal.

2. Neither CLAUDE.md nor AGENTS.md said the inline shape is already a
   valid explicit decision (test_route_authz_static counts .is_admin via
   ATTRIBUTE_GATES), or that ~91 routes across 21 files use it. Both now
   do, with the plan 76 quote and a note that automated reviewers raise
   this routinely.

Audited 2026-08-31 by AST over app/api + app/plugins + builtin-extensions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 00:41:54 -04:00
Juan DenisandClaude Opus 5 30b780f4e4 fix(status): a null JSON body no longer 500s the status-page routes
PR #122 review. Four routes passed request.get_json() straight into the
service, which does `if field in data` and raises TypeError on None.

Copilot reported the trigger as "empty/invalid JSON bodies"; measured on
Flask 3.1.3 that is wrong — an empty body is already a 400 and a wrong
content-type a 415, both raised by Flask before the view runs. The body
that actually reaches the view is literal `null`, which is valid JSON and
decodes to None. Same fix, accurate reason.

Also corrects plugin.json, which claimed installing the extension serves
the public /status/<slug> pages; the registry catalog says that page
stays core, and the two contradicted each other.

Both copies edited — app/plugins/ is the live copy the loader imports,
builtin-extensions/ is the source — and a test pins that they agree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 00:34:20 -04:00
github-actions[bot] 41405c1257 chore: bump version to 1.9.13 [skip ci] 2026-08-31 04:21:47 +00:00
Juan Denis bcf53f3e84 Merge branch 'dev' of https://github.com/jhd3197/ServerKit into dev 2026-08-31 00:11:38 -04:00
Juan DenisandClaude Opus 5 f724a1fbca fix(notifications): escape interpolated values in Telegram alerts
send_telegram posts with parse_mode='HTML' but interpolated the hostname,
alert type, message, value and threshold straight into the body. Telegram
then parses those as markup, so a message as ordinary as

    Disk usage > 90% on /var & /tmp

is rejected with "can't parse entities" and the entire notification is
dropped — precisely when something is already wrong on the box. A value
containing <b> could also inject markup into the rendered alert.

Escape every substituted value with html.escape(quote=False); the
template's own tags are untouched.

Proving tests pinned against the unfixed service first: the three
escaping cases fail without this change, the literal-markup case passes,
so they test the behaviour and not the implementation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 00:07:47 -04:00
Juan DenisandClaude Opus 5 48871b07e6 fix(dev): dev.sh notices a new requirement instead of booting into ImportError
The venv check probed four imports (dotenv, flask, flask_socketio,
sqlalchemy). Those catch a missing or broken venv, but they are blind to
the common case: a dependency is added to requirements.txt, all four
imports still succeed, dev.sh reports nothing, and the backend dies on
the fifth import at startup. That has derailed several sessions.

Stamp a sha256 of requirements.txt into the venv after a successful
install and compare on each start, so drift triggers a sync. The import
probe stays as the broken-venv check; the hash covers what it cannot see.

Guards: the hash is computed with the venv's own python (no dependency on
sha256sum being installed), an unreadable requirements.txt yields an
empty hash and leaves a working venv alone rather than reinstalling every
start, and the marker is written only after a successful install so a
failed one retries.

Verified by driving the decision logic with pip stubbed: no marker →
install, marker current → skip, requirement appended → install, settled →
skip.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 00:05:47 -04:00
github-actions[bot] d39f2a8d9d chore: bump version to 1.9.12 [skip ci] 2026-08-31 03:54:38 +00:00
Juan DenisandClaude Opus 5 12c863b230 fix(templates): repo deploys send the build inputs the template declares
The deploy drawer posted only url/branch/app_type/build_method/port, so a
repo template's `build_command`, `start_command` and `dockerfile_path`
were decorative — declared in the YAML, read by nobody. The endpoint has
always accepted custom_build_cmd / custom_start_cmd / dockerfile_path;
only the caller was dropping them.

The visible cost was nixpacks builds: with no start command it falls back
to `npm start`, so any repo whose package.json has no `start` script
produced an image that built fine and could not boot.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 23:47:14 -04:00
Juan Denis 37cf8b6e84 Merge plan 52 cutover: the published extensions leave the tree
localkit, email, cloudflare-ops and git are now published, signed and in
the registry index, so the panel no longer needs in-tree copies of them.
This removes ~29k lines that existed in two places at once and flips the
bundled registry so they install from the catalog like any other
extension.

Unblocked by the operator publishing all nine repos (2026-08-30); the two
registry-mirror tests that plan 83 expected to stay red now pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

# Conflicts:
#	builtin-extensions/serverkit-cloudflare-ops/frontend/components/StoragePanel.jsx
#	frontend/src/plugins/serverkit-cloudflare-ops/components/StoragePanel.jsx
2026-08-30 22:13:01 -04:00
Juan DenisandClaude Opus 5 7b9eb43a9a test: lower the count floor for the extensions that left the tree
The cutover removes four builtin extensions, so collection legitimately
drops. Verified against dev by diffing collected node ids rather than
trusting the delta:

  -7  test_extension_manifests  the 7 plugin.json files of the four
                                departing extensions (builtin + frontend)
  -4  test_builtin_extension_drift  anchor swap, email (7 py files) ->
                                    status (3)
   0  test_files_rbac / *_extraction  param-id churn only

No feature lost its proving tests. 4799 -> 4791.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 22:12:05 -04:00
Juan DenisandClaude Opus 5 18767c2244 test(drift): re-anchor the builtin-copy guard on serverkit-status
serverkit-email's live copy under app/plugins/ was the only git-TRACKED
one, which is what kept test_builtin_extension_drift non-vacuous on a
fresh clone. Plan 52's cutover deletes it, so on CI the guard had zero
pairs to compare and test_drift_check_is_not_vacuous fired -- correctly.
It still passed on a dev box, where the gitignored live copies exist, so
the failure only reproduces in a clean checkout.

Track serverkit-status's live copy instead (verified byte-identical to
builtin-extensions/serverkit-status/backend), un-ignore just that path,
and update both docstrings to name the new anchor.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 22:07:53 -04:00
Juan Denis d597173ed0 Merge remote-tracking branch 'origin/main' into dev 2026-08-30 21:48:27 -04:00
Juan Denis 3ff85b1598 Merge pull request #121 from jhd3197/dev
Split the security suite and the last built-ins into extensions
v1.9.11
2026-08-30 21:30:45 -04:00
Juan DenisandClaude Fable 5 cea1770562 fix(review): posture installs survive unchecked overlaps; translatable storage empty-states
PR #121 review follow-ups. The setup summary de-duplicated the install
queue with recommendations winning, so a security-posture extension
that also appeared as an unchecked recommendation was silently dropped
— the posture half now wins regardless of checkbox state. The
Cloudflare storage panel templated English around a translated title
(`No ${title.toLowerCase()} yet.`); each section now passes a fully
translated emptyHint instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 20:25:17 -04:00
Juan Denis 4b65c1e638 Merge remote-tracking branch 'origin/main' into dev 2026-08-30 20:22:18 -04:00
github-actions[bot] 3df4fb8446 chore: bump version to 1.9.11 [skip ci] 2026-08-30 20:32:52 +00:00
Juan Denis d764f50809 Merge branch 'dev' into feature/plan52-cutover
# Conflicts:
#	backend/app/data/registry_index.json
#	frontend/src/plugins/serverkit-git/plugin.json
2026-08-30 16:24:27 -04:00
Juan DenisandClaude Fable 5 2c3b63b1f6 chore: re-vendor the bundled registry index from the published catalog
The nine extracted extensions are now published, signed, and merged
into the serverkit-extensions main catalog; the bundled offline copy
picks up the five new security-suite entries and the source/sha256/
signature fields on localkit, email, cloudflare-ops, and git.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 16:23:48 -04:00
Juan DenisandClaude Fable 5 6234832c46 chore(preflight): gate builtin-frontend drift locally
The Extensions CI drift check (sync-builtin-frontends.mjs --check) had
no local counterpart, so a builtin-extensions/ manifest edit without a
re-sync reached CI unrun — exactly how the serverkit-git split went red
on the next PR. Same contract as the migration-inventory gate: a stale
copy is re-synced on the spot and flagged so the diff gets committed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 14:08:09 -04:00
Juan DenisandClaude Fable 5 91d464f871 chore: re-sync the pre-bundled serverkit-git plugin.json
The plan 52 Phase 6 git split updated the source manifest under
builtin-extensions/ (shell permission, entry_point, url_prefix,
min_panel_version) without running sync-builtin-frontends.mjs, so the
Extensions CI drift check went red on the next PR.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 14:00:50 -04:00