Conversation
…ervers Four boxes were unchecked for things that have shipped. Signed extension releases with install consent landed with plan 55's ed25519 signing service and the marketplace consent modal. WordPress is out of the tree and installs from the Marketplace like any other extension. The Minecraft extension, the reusable gamekit framework and the connect card are all published at v1.0.0 and carried in the registry index. SRV-record domain support is split out as the one game-server item that is genuinely still open — grep finds no SRV handling in the extension — rather than leaving it hidden inside a bullet whose other half was done. The built-ins bullet now names only what is left, since the WordPress flagship it called out has been extracted.
… not match app/data/registry_index.json is what the Marketplace falls back to when the live registry is unreachable, and what the test suite reads. Seven of its ten entries carried a sha256 that does not match the artifact their own source URL resolves to, at the same version: serverkit-gpu's zip hashes 3872d71f..., the bundled copy claimed c861dafa.... A panel that fell back to this file would download the correct, untampered release and reject it as tampered, on exactly the panels already cut off from the network. It also sat nine entries and one version behind (WordPress 1.0.0 against a published 1.0.1). Nothing caught it because nothing checked it: the file was hand-maintained, had no generator, and no test referenced it. A wrong-but-well-formed digest is indistinguishable from a right one without fetching the artifact or the published index. Regenerated as a straight mirror of serverkit-extensions/index.json, and the note now says it is a mirror and must not be hand-edited. The new test compares the pinned (version, source, sha256) tuples against the published index and skips only when the index is unreachable, so offline is not a failure but a reachable mismatch is. Verified it fails against the old file, naming all seven digests and the WordPress version, and passes against the regenerated one. Also corrects the signed-releases roadmap line I marked done earlier today: the verification path and the consent UX shipped, but no published release actually carries a signature and the registry schema did not even document the field, so every first-party install still takes the unsigned-consent path.
The panel serves the fleet-agent installers at /api/v1/servers/install.sh and /install.ps1 by reading them off its own filesystem, resolving <tree root>/scripts four dirnames up from app/api/servers.py. In the image that is /app/scripts -- and the Dockerfile only ever copied backend/, VERSION and frontend/dist, so the directory did not exist. Every containerised panel answered the enrollment one-liner printed in its own Add Server dialog with a 404, and no server could join the fleet. Verified against the published image: `ls /app` has no scripts, and a booted container returns 404 while /api/v1/system/health returns 200. Bare metal was unaffected -- install.sh git-clones the whole repo, and the release tarball excludes only scripts/test/output. Ship just the two installers: both are standalone (they source nothing from scripts/lib), so scripts/lib, keys, the test harnesses and stage-remote.sh stay out of the image. The .dockerignore comment claiming the build "only needs frontend/, backend/, VERSION" was wrong and authoritative, which is a large part of why this went unnoticed; it now states the real list. Closes #101 (packaging half). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
When the Go agent moved out of this monorepo into jhd3197/serverkit-agent, the
installers did not follow it. They still asked jhd3197/ServerKit for `agent-v*`
tags -- coordinates that exist in no repository: jhd3197/ServerKit has zero
agent-v* releases, and the binaries are published as vX.Y.Z in the agent repo.
Version discovery therefore failed outright, and the download URL was a 404.
Everything downstream was already correct and is unchanged: the asset name
serverkit-agent-<ver>-linux-<arch>.tar.gz, the entry inside the tarball
(serverkit-agent-linux-<arch>) and the checksums.txt entry names all match the
real v1.2.0 release. Only the repo and the tag prefix had rotted.
Version discovery now requires a digit after the "v". The agent repo carries a
malformed release tagged literally "v", which would otherwise parse to an empty
version and build .../releases/download/v/serverkit-agent--linux-amd64.tar.gz.
test_agent_install.sh encoded the dead scheme in its fixtures (agent-v0.3.2), so
it stayed green throughout. It now pins the release coordinates as a contract:
the repo, the /v${VERSION}/ URL scheme, the absence of any live agent-v
reference, and the checksums URL exercised through the curl stub. All four fail
against the pre-fix script.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… repo Three changes to the endpoints that serve the fleet-agent installers. A missing installer answered 404. That is what sent issue #101's reporter looking for a wrong URL -- they filed it as "incorrect paths in setup" -- when the file had simply never been shipped into the image. The status code produced the misdiagnosis. It is now 503, the response names the path that was searched, and the same path is written to the panel log, because the response body does not survive the `curl -f` the enrollment one-liner uses: curl discards it and prints only "The requested URL returned error: 404". For an endpoint consumed by curl, the status code and the server log are the only diagnostics that reach anyone. The panel's release lookup pointed at jhd3197/ServerKit and filtered for `agent-v*` tags, which exist nowhere since the agent moved to its own repo, so /api/v1/servers/agent/version returned 503 to every agent polling for an update. It now uses AGENT_GITHUB_REPO (env SERVERKIT_AGENT_GITHUB_REPO) and a ^v(\d…) tag filter that skips the agent repo's malformed "v" release. _load_installer() is shared by the .sh and .ps1 routes so they cannot drift, and SERVERKIT_SCRIPTS_DIR overrides the resolved directory for unusual layouts. New tests cover the endpoints, the release lookup, and -- the one that would have caught the original bug -- a packaging invariant that parses the Dockerfile's COPY sources and asserts every installer the panel serves is actually copied into the image. No endpoint test could have caught it: pytest runs against the source tree, where the file always exists. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The command the panel prints for enrolling a server was `curl -fsSL <url> | sudo bash -s -- …`. In a pipeline the shell reports bash's exit status, not curl's, so a failed download exits 0: the operator sees one line of curl noise, the shell reports success, and nothing was installed. Measured against a 404 -- `curl -fsSL <404> | bash -s -- --token TOK; echo $?` prints 0; with `set -o pipefail` it prints 22. Download to a file first and run it on success, so a failed fetch stops the command and surfaces curl's non-zero exit. Applied to the Add Server drawer, the server-detail connection modal and the Downloads page. The Windows one-liner is left alone: `irm` writes an error and the pipeline stops, so its failure is already visible. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Tracing issue #101 turned up that a panel failing to serve its own agent installer left no diagnostic trail at all. The backend had exactly one error handler -- the 404/SPA one -- no 500 or HTTPException handler, and nothing anywhere configured logging, while only 2 of 100 app/api modules ever call logger.error. Adds: - an errorhandler(500) that logs the unhandled exception with its method and path and returns JSON. Flask routes here only when it is not propagating, so pytest and the dev server still re-raise with a full traceback. - an errorhandler(HTTPException) so framework-generated errors under /api/ (405 from the router, 413, abort()) return JSON instead of Werkzeug's HTML page to clients that are parsing JSON. Non-API paths keep the HTML page, and the more specific 404/500 handlers still win for those codes. - _configure_logging(): a stderr handler with a timestamp/level/logger-name formatter, level from LOG_LEVEL. Services across the codebase log through logging.getLogger(__name__); with no root handler those records fell through to logging's last-resort handler -- WARNING and above only, unformatted, INFO dropped on the floor. Two things the container run caught while writing this: Flask's default handler has to come off app.logger or every record prints twice (once as %(module)s via Flask's handler, once as %(name)s via ours, both reaching stderr through propagation), and the level must not key off DEBUG alone -- TestingConfig sets DEBUG=True, which would have moved the whole test suite to DEBUG logging as a side effect. Both are pinned by tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Issue #101 shipped because release.yml went straight from `build` to `push` with nothing in between. The image was missing scripts/, so every containerised panel 404'd its own enrollment URL, while the entire test suite stayed green -- pytest runs against the source tree, where the file always exists. Only booting the artifact catches that class of bug. scripts/test/smoke-docker-image.sh builds (or takes) an image, asserts the files the panel reads at request time are present, boots it, and requests every route that serves a file off disk. release.yml now builds amd64, runs it, and only then pushes multi-arch (layers are cached, so the push reuses the build). release-smoke.yml gets the same job on dev and PRs, and its path filters now include Dockerfile, .dockerignore and scripts/ -- none of which used to trigger it. scripts/test/verify-agent-release-chain.sh covers the other half, which unit tests structurally cannot: they stub curl, so they only ever prove the installer is self-consistent, never that the URL it builds resolves to a real file. This one reads the coordinates back out of the script, asks GitHub, downloads the archive, verifies the checksum and confirms the tarball entry name the installer moves. It runs nightly, and is also the tripwire for the reverse case -- the agent repo changing its release naming and breaking the panel from the other side of the repo split. Both are plain scripts so the same check runs locally: bash scripts/test/smoke-docker-image.sh bash scripts/test/verify-agent-release-chain.sh Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Also corrects the scope note, which said the agent is tagged agent-vX.Y.Z and linked to agent/README.md -- a path that no longer exists. The agent ships from jhd3197/serverkit-agent and is tagged vX.Y.Z there. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… drift scripts/install.sh and scripts/install.ps1 are now VENDORED copies. The agent repo owns them; this repo carries a copy only because the panel serves them off its own disk at GET /api/v1/servers/install.sh|.ps1 and cannot link to another repository. Two copies of one file is what caused the second half of #101. When the agent moved out of this monorepo it took its installer with it, and from then on there were two -- actually four: the agent repo also carried a third, older pair under its own scripts/ that nothing referenced. Each looked fine in isolation, nothing ever compared them, and the panel went on serving one that downloaded from `agent-v*` tags which exist in no repository. Unit tests are structurally blind to this: they exercise whichever copy lives in their own repo and have no idea another exists. So: scripts/sync-agent-installers.sh re-vendors from the agent repo (a sibling checkout if present, else the published branch), normalising to LF so this repo's .gitattributes can apply its own EOL rule scripts/test/check-installer-drift.sh fails, with the diff and the fix, if a vendored copy differs from canonical (EOL-insensitive: *.ps1 is checked out CRLF here and LF there, which is not drift) The drift check runs nightly alongside the release-chain verification, since both need to reach GitHub. Both scripts run locally against a sibling checkout, so this works offline and before an agent change is published. Both installers carry a provenance header naming the canonical location, so anyone who opens the vendored copy to patch it is told where to patch instead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The vendoring and drift-check scripts defaulted AGENT_REF to `main`, which this repo uses but the agent repo does not -- it has no `main` branch at all. Verified: raw.githubusercontent.com/jhd3197/serverkit-agent/main/install.sh is 404, /master/install.sh is 200. The bug was invisible locally because both scripts prefer a sibling ../serverkit-agent checkout and only fall back to fetching. It would have shown up first in nightly CI, as "could not fetch the canonical copy" -- which reads as an outage rather than a wrong branch name, so it is worth the comment. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…coding it The vendoring and drift-check scripts hardcoded a branch name for the fallback fetch. That was wrong twice in one day: first `main` (the agent repo used `master`, so the fetch 404'd), then `master` (which is being renamed to `main`). A hardcoded branch turns any rename into a 404 that surfaces as "cannot reach the canonical copy" rather than "wrong branch name" — a confusing way to fail for a check whose whole job is to be believed. They now ask the GitHub API for the repo's declared default_branch, falling back to probing main then master if the API is unreachable or rate-limited. AGENT_REF still pins an explicit ref (a tag, say) when you want one. Verified: the API currently reports `master`, and it will report `main` on its own once the rename lands — no follow-up edit here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Fixes broken fleet enrollment for Docker-based panel installs by ensuring the agent installers are actually shipped in the image and by updating all agent release coordinates to the dedicated jhd3197/serverkit-agent repository (plain vX.Y.Z tags). The PR also hardens operational diagnostics by configuring backend logging and returning JSON-shaped API errors, and it adds CI coverage to smoke-test the built Docker image and validate the agent download chain.
Changes:
- Copy
scripts/install.shandscripts/install.ps1into the Docker image and add smoke tests to boot the image and assert the installer endpoints are served. - Update backend + installers to resolve agent releases from
jhd3197/serverkit-agentwithvX.Y.Ztags (skipping malformedv), and update UI one-liners to “download-then-run”. - Improve backend logging + API error responses; refresh bundled Marketplace index and add tests to prevent registry drift.
Reviewed changes
Copilot reviewed 25 out of 25 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| VERSION | Bumps panel version. |
| SECURITY.md | Documents agent repo/tag scheme change. |
| CHANGELOG.md | Records the enrollment + logging + API error-shape fixes. |
| ROADMAP.md | Updates shipped/remaining roadmap items and “Last updated” date. |
| Dockerfile | Copies agent installer scripts into the image. |
| .dockerignore | Updates documentation to reflect required build-context files. |
| backend/app/api/servers.py | Serves installers via shared loader, injects resolved agent version, switches agent release lookup to agent repo with vX.Y.Z tags. |
| backend/app/init.py | Adds root logging configuration and JSON API error handling for HTTPException + 500. |
| backend/app/data/registry_index.json | Regenerates bundled registry index + fixes metadata note. |
| backend/tests/test_agent_installer_endpoints.py | Adds endpoint + packaging invariant tests for served installers and agent release coordinate behavior. |
| backend/tests/test_api_error_shape.py | Adds regression tests for JSON error shapes and logging configuration. |
| backend/tests/test_registry_bundled_index.py | Adds drift/shape tests for the bundled registry index (offline + optional network check). |
| frontend/src/pages/Servers.jsx | Updates enrollment one-liner to download-then-run. |
| frontend/src/pages/Downloads.jsx | Updates Linux installer commands to download-then-run. |
| frontend/src/components/serverdetail/ServerSettingsTab.jsx | Updates token modal Linux one-liner to download-then-run. |
| scripts/install.sh | Updates agent installer repo/tag parsing and download/checksum URLs; adds vendoring banner. |
| scripts/install.ps1 | Updates agent installer repo/tag parsing and download URL; adds vendoring banner. |
| scripts/sync-agent-installers.sh | Adds script to re-vendor installers from the agent repo (EOL-normalized). |
| scripts/test/test_agent_install.sh | Updates unit tests to the new repo/tag scheme + adds regression checks for malformed tags and checksum URL. |
| scripts/test/smoke-docker-image.sh | Adds Docker image smoke test validating filesystem + HTTP-served installer endpoints. |
| scripts/test/check-installer-drift.sh | Adds cross-repo drift check for vendored installers (EOL-insensitive). |
| scripts/test/verify-agent-release-chain.sh | Adds network validation that installer-built URLs resolve to real GitHub assets. |
| .github/workflows/release.yml | Builds/boots amd64 image before multi-arch push; enables build cache reuse. |
| .github/workflows/release-smoke.yml | Adds docker-image-smoke job and expands path filters to include scripts/Dockerfile/.dockerignore. |
| .github/workflows/scripts-nightly.yml | Adds nightly agent release-chain and installer drift checks. |
Suppressed comments (1)
scripts/install.sh:115
- The
show_help()example also useshttps://your-serverkit.com/install.sh, which doesn’t match the panel endpoint (/api/v1/servers/install.sh). Consider aligning the example (or explicitly documenting both forms) in the canonical agent-repo copy to avoid confusion.
echo "Example:"
echo " curl -fsSL https://your-serverkit.com/install.sh -o /tmp/serverkit-agent-install.sh \\"
echo " && sudo bash /tmp/serverkit-agent-install.sh \\"
echo " --token 'sk_reg_xxx' \\"
echo " --server 'https://your-serverkit.com'"
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+159
to
+161
| # Ship the fleet-agent installers. The panel serves these over HTTP at | ||
| # /api/v1/servers/install.sh and /install.ps1, reading them off its own disk at | ||
| # <tree root>/scripts — which is /app/scripts here. They were missing from the |
Comment on lines
+55
to
+64
| def test_internal_error_handler_is_registered(app): | ||
| """A 500 handler must exist, or API clients get an HTML page on a crash.""" | ||
| handlers = app.error_handler_spec[None] | ||
|
|
||
| assert 500 in handlers and handlers[500], ( | ||
| 'no 500 error handler registered; an unhandled exception returns ' | ||
| "Werkzeug's HTML page and is never logged with its request path" | ||
| ) | ||
| assert InternalServerError in handlers[500] | ||
|
|
Comment on lines
+19
to
+22
| # Usage: | ||
| # curl -fsSL https://your-serverkit.com/install.sh | sudo bash -s -- --token "TOKEN" --server "URL" | ||
| # curl -fsSL https://your-serverkit.com/install.sh -o /tmp/serverkit-agent-install.sh \ | ||
| # && sudo bash /tmp/serverkit-agent-install.sh --token "TOKEN" --server "URL" | ||
| # |
`app/data/registry_index.json` is the Marketplace's offline fallback and a mirror of serverkit-extensions/index.json on main. The registry gained `serverkit-localkit` (v0.2.0, bundled) and the mirror was never updated, so an offline panel would have offered a different extension set than an online one -- and test_registry_bundled_index::test_matches_published_index failed on every run. Copied the entry verbatim into its published position. The rest of the file already matched published field-for-field; this is the whole delta. `builtin-extensions/serverkit-localkit/` ships in the panel at the same version, so `bundled: true` is correct and the entry carries no source/sha256. Nothing automates this mirror, so it will drift again on the next registry publish. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…gistry index backend/app/data/registry_index.json is a mirror of serverkit-extensions' index.json, and nothing automated the mirror -- so every registry publish left the panel's offline fallback stale until CI complained. That is how `serverkit-localkit` sat missing from it: an offline panel offered a different extension set than an online one. Same shape as scripts/sync-agent-installers.sh: prefer a sibling checkout so it works offline and before the registry PR is merged, else the published branch, resolving the default branch from the API rather than hardcoding a name. Two things it is deliberately careful about: - It vendors the RAW index, never https://serverkit.ai/ext/index.json. The panel fetches through that proxy at runtime and the proxy rewrites every relative `logo` path to an absolute serverkit.ai URL, while the bundled loader resolves relative paths itself. Vendoring the proxied copy would bake in the wrong paths, and the tuple the tests compare (version/source/sha256) is identical either way -- nothing would have caught it. - It keeps the local `note` (which does not exist upstream) and writes with indent=2/ensure_ascii=True/trailing newline, which round-trips the existing file byte-for-byte. An in-sync run is a true no-op, not a reformat. It refuses to write an index with no entries, so a broken or truncated canonical source cannot blank the Marketplace fallback. Verified: no-op when in sync; reproducing the real bug (dropping the localkit entry) and re-running restores the file byte-identical to HEAD; repins are reported; the empty-canonical guard exits non-zero leaving the file untouched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…hips A `bundled: true` entry promises an extension that ships inside the panel, so it carries no source/sha256 -- which means every other check in this file skips it. test_matches_published_index only proves the catalog agrees with the registry, not that either agrees with the code, so a bundled entry could name a version the panel does not ship, or an extension it does not ship at all, and nothing would notice. Checks each bundled entry against builtin-extensions/<slug>/plugin.json. Offline and deterministic, so unlike the published-index comparison it fails on the PR that introduces the mismatch rather than needing the network. Only asserted in this direction: a builtin deliberately absent from the catalog is a product decision, not rot. Verified non-vacuous by mutation -- claiming a wrong version and naming a non-existent bundled slug each fail, on different assertions. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
User-facing half of the registry-mirror work: an offline panel (or one with the registry disabled) was missing LocalKit Bridge from the Marketplace because the bundled fallback had fallen behind the published index. The sync script and the test guard are tooling, so they stay in git log -- except for the one line about the new check, which is the part a reader of this file would care about. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Picks up serverkit-agent 219e75e, which points the usage and --help examples at
/api/v1/servers/install.{sh,ps1} instead of the domain root. Produced by
scripts/sync-agent-installers.sh, not hand-edited — scripts/test/check-installer-drift.sh
fails on any divergence and confirms these match the canonical copies again.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
test_internal_error_handler_is_registered read app.error_handler_spec, a Flask internal whose shape has moved between releases, and proved only that something was registered. A handler can be registered and still return HTML or log nothing, so the test could not fail for either way the guarantee actually breaks. Now driven through app.handle_exception(), Flask's own entry point for an unhandled exception, asserting the response is JSON and that the request was logged. Not done by registering a route that raises: the `app` fixture is function-scoped but wraps a session-scoped Flask object, so the route would leak into every later test and collide on re-registration. PROPAGATE_EXCEPTIONS is forced off for the call because TESTING=True turns it on, which re-raises before any handler runs. The log assertion matches this handler's own wording rather than just the path. Flask's log_exception() already emits "Exception on /path [GET]" before any handler runs, so a path-only assertion passes even with the handler's logging deleted — confirmed by mutation, which is how the first version of this test was caught being vacuous. Both mutations (return HTML, drop the log) now fail, on different assertions. Raised by a reviewer on the PR. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The comment explaining why scripts/ is copied into the image read "/api/v1/servers/install.sh and /install.ps1", which reads as though the Windows installer is served at the domain root. It is not — both live under /api/v1/servers/. A comment whose whole job is to stop the next person hunting for a wrong path should not contain one. Raised by a reviewer on the PR. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The panel has been printing an enrollment command that could not possibly work, and confidently telling operators it succeeded. Issue #101 turned out to be two separate breaks stacked on each other: the Docker image never copied
scripts/, so/api/v1/servers/install.sh404'd in every container, and the installer it would have served still pointed atjhd3197/ServerKitandagent-v*tags — coordinates that stopped existing when the Go agent moved to its own repository. Fixing either one alone just relocates the failure by one step, which is why both land here together. The whole thing stayed invisible becausecurl -fsSL … | sudo bashreports bash's exit status rather than curl's: a 404 exits 0, installs nothing, and looks like success. Everything downstream of that — the download-then-run one-liner, honest error codes, actual logging configuration, and CI that boots the artifact instead of the source tree — exists because no test could have caught this, and none did. Also included: the offline Marketplace index was pinning seven checksums that don't match their own artifacts, which would have made air-gapped panels reject untampered releases as tampered.Highlights
/api/come back as JSON instead of an HTML error page, and unhandled exceptions get logged with the request that caused them.--helpprints a command that actually works: it pointed athttps://your-serverkit.com/install.sh, a path nothing serves.Technical changes
Image packaging
Dockerfilecopiesscripts/install.shandscripts/install.ps1into/app/scripts. Only those two — both are standalone and source nothing fromscripts/lib, so the rest ofscripts/stays out of the image..dockerignore's header comment claimed the build "only needs frontend/, backend/, VERSION" while the panel was serving installers off disk; that wrong-but-authoritative note is corrected, along with a warning about why it mattered._get_scripts_dir()inbackend/app/api/servers.pydocuments each shipped layout it has to resolve and honors aSERVERKIT_SCRIPTS_DIRoverride for split deployments.Agent repository coordinates
AGENT_GITHUB_REPO(envSERVERKIT_AGENT_GITHUB_REPO) replaces the panel-monorepoGITHUB_REPOfor the release lookup, defaulting tojhd3197/serverkit-agent.startswith('agent-v')to_AGENT_TAG_RE = ^v(\d[^\s]*)$. The digit is load-bearing: the agent repo carries a malformed release tagged literallyv, which parsed to an empty version and produced download URLs like.../download/v/serverkit-agent--linux-amd64.tar.gz.scripts/install.shandscripts/install.ps1get the same treatment — repo constant,v${VERSION}download andchecksums.txtURLs, andgrep -o '"tag_name": *"v[0-9][^"]*"'/-match "^v(\d.*)$"for tag parsing.SECURITY.mdandCHANGELOG.mdrecord where the agent lives now and thatagent-vX.Y.Zwas the monorepo-era scheme.Failure honesty
_load_installer()factors the read-and-stamp path shared by both installer routes so they cannot drift, and returns503withsearched_pathwhen the file is absent — a missing installer is this panel's packaging fault, not a bad request. The old404is what sent the issue reporter hunting for a wrong path.curl -o /tmp/serverkit-agent-install.sh && sudo bash …: the APIinstall_instructionspayload,Servers.jsx'sAddServerModal,ServerSettingsTab.jsx'sTokenModal, both Linux entries inDownloads.jsx, andinstall.sh's own usage/help text.Error handling and logging
_configure_logging()inbackend/app/__init__.pyinstalls a rootStreamHandlerwith[%(asctime)s] %(levelname)s in %(name)s: %(message)s. Services across the codebase calllogging.getLogger(__name__)but nothing ever configured the root logger, so records fell through to logging's last-resort handler — WARNING and above, no timestamps, INFO dropped. Guarded by a module-level_logging_configuredflag becausecreate_app()runs many times per session and gunicorn imports before forking.default_handleris removed fromapp.loggerto stop double-printing every record (it propagates to root as well).LOG_LEVELoverrides;TESTINGpins INFO so adding a handler doesn't silently promote everything to DEBUG viaTestingConfig'sDEBUG=True.@app.errorhandler(500)logsrequest.method/request.pathwith the original exception and returns JSON. Flask only routes here when not propagating, so the dev server keeps its traceback.app.handle_exception()— Flask's own entry point — rather than by readingapp.error_handler_spec, a Flask internal whose shape has moved between releases. Registration is not behaviour: a handler can be registered and still return HTML or log nothing. The log assertion matches this handler's own wording, because Flask'slog_exception()already emits the path before any handler runs, so asserting on the path alone passes even with the handler's logging deleted.@app.errorhandler(HTTPException)returns{'error', 'status'}JSON for/api/paths —abort(), router 405s,MAX_CONTENT_LENGTH413s previously handed Werkzeug HTML to clients parsing JSON. Non-API paths keep the standard HTML page; the specific 404/500 handlers still win.Vendoring the installers
scripts/sync-agent-installers.shre-vendors both installers from a siblingserverkit-agentcheckout or from GitHub, normalizing to LF so.gitattributescan apply this repo's*.ps1 eol=crlfpolicy. Both files gain a CANONICAL SOURCE banner pointing at the agent repo.resolve_agent_ref()asks the GitHub API fordefault_branch, falling back to probingmainthenmaster, withAGENT_REFto pin explicitly. A hardcoded branch was wrong twice in one day (main, thenmasterduring a rename) and fails as "cannot reach the canonical copy" rather than "wrong branch".--helpexamples pointed athttps://your-serverkit.com/install.sh; the panel serves them at/api/v1/servers/install.shand nothing answers at the domain root, so anyone copying the command out of--helpwhile troubleshooting got a 404 — the same wrong-path hunt Incorrect paths in setup #101 started with. Fixed in the agent repo and re-vendored, not patched here, which the drift check enforces.CI gates
release.ymlbuildslinux/amd64and boots it before the multi-arch push, withcache-from: type=ghaso the push reuses those layers. The job previously went straight from build to push.release-smoke.ymlgains adocker-image-smokejob and widens its path filters fromscripts/build-release.shtoscripts/**,Dockerfile, and.dockerignore.scripts-nightly.ymlgains anagent-release-chainjob that asks GitHub whether the URLs the installer builds resolve to real assets, plus a drift check comparing the vendored installers against the agent repo's canonical copies (EOL-insensitively).Marketplace fallback index
backend/app/data/registry_index.jsonis regenerated as a straight mirror ofserverkit-extensions/index.json. Seven of ten entries pinned asha256that didn't match the artifact their ownsourceURL resolves to at the same version, so an offline panel would download a correct release and reject it as tampered. It also sat nine entries and one version behind, and was still missingserverkit-localkit— an offline panel offered a different extension set than an online one. The file's note now states it is a mirror and must not be hand-edited.scripts/sync-registry-index.shre-vendors it, so "hand-edited" stops being the only option: sibling checkout first, else the published branch with the default branch resolved from the API, same shape assync-agent-installers.sh. It writes with the exact formatting that round-trips the existing file, so an in-sync run is a true no-op rather than a reformat, and it refuses to write an index with no entries so a truncated source cannot blank the Marketplace.https://serverkit.ai/ext/index.json. The panel fetches through that proxy at runtime and the proxy rewrites relativelogopaths to absolute URLs, while the bundled loader resolves them itself. The tuple the tests compare (version/source/sha256) is identical either way, so vendoring the proxied copy would bake in wrong paths silently.test_bundled_entries_match_what_the_panel_actually_shipscloses the gap the drift test cannot see. Abundled: trueentry carries nosource/sha256, so every other check skips it — the catalog could promise a version the panel does not ship, or an extension it does not ship at all. Offline and deterministic, so it fails on the PR rather than needing the network.