Bobsled's production contract is an unprivileged Linux container or virtual machine with a locked service account. The checked-in Incus cloud-init and systemd unit are templates: review their resource, network, user, and filesystem assumptions before applying them to another host.
- Replaceable application release:
/opt/bobsled/current - Durable Flue and factory databases:
/var/lib/bobsled/data - Repository sources, credential-free tool caches, disposable worktrees, and evidence:
/var/lib/bobsled/workspaces - Protected runtime configuration and OAuth state:
/etc/bobsled - Service account and unit: locked
bobsleduser andbobsled.service - HTTP: loopback or private network only until authenticated ingress is deliberately configured
The systemd unit gives the service a read-only system view and permits writes only to protected configuration, durable data, and workspaces. Releases are replaceable; state does not live beneath /opt/bobsled.
- Build and install dependencies inside Linux so native modules match the target architecture.
- Keep application releases immutable and versioned; activate them by changing the
/opt/bobsled/currentsymlink. - Keep the previous release until the new health check passes so rollback is a symlink change rather than a rebuild.
- Set
BOBSLED_DATA_DIR,BOBSLED_WORKSPACE_DIR, andBOBSLED_REPOSITORY_SOURCE_ROOTto durable locations outside the release. Each enrolled checkout lives beneath the source root at itsowner/repositorypath.BOBSLED_CLIX_SOURCE_PATHremains a legacy clix-only override. - Keep OAuth and GitHub App configuration outside the release and readable only by the service account.
- Use a temporary data directory during staged verification so importing the application cannot create state beneath the release.
- Normalize staged ownership and traversal permissions before activation; source archives can otherwise preserve restrictive developer modes.
- Back up durable databases and protected configuration separately from application releases. Treat verified webhook bodies and complete Flue observations as sensitive.
Store the GitHub App private key as a separate PEM file rather than multiline dotenv content. A typical protected deployment uses /etc/bobsled/github-app.pem, owned by root:bobsled with mode 0640, and sets BOBSLED_GITHUB_PRIVATE_KEY_FILE=/etc/bobsled/github-app.pem. Transfer the downloaded PEM directly to the host; do not paste it into chat, command arguments, logs, or repository files. After proving a newly generated key can mint a scoped installation token, revoke the superseded key in GitHub and remove any legacy BOBSLED_GITHUB_PRIVATE_KEY assignment.
GitHub integration requires a stable HTTPS origin and a narrowly configured reverse proxy or tunnel:
- Route the GitHub webhook path to
/channels/github/webhook. - Route the registered OAuth callback to
/auth/github/callback. - Put the operator UI behind Bobsled's authenticated session boundary or an equally strong private access layer.
- Do not expose raw agent routes.
- Preserve
@flue/githubsignature verification and Bobsled's durable delivery-deduplication boundary. - Restrict operator OAuth to active members of the configured organization.
- Keep installation tokens server-side, short-lived, and repository-scoped.
Webhook admission must remain unavailable until a protected webhook secret is configured. GitHub operator mode must fail closed until its client credentials, session secret, and exact HTTPS public origin are all valid.
deploy/caddy/Caddyfile.example is the reviewed reference for bobsled.frostyard.org. It deliberately:
- lets Caddy own public TLS and certificate renewal;
- proxies to a private-network
BOBSLED_UPSTREAMrather than embedding deployment-specific addressing in source; - returns
404for/agentsand/agents/*before a request can reach Flue's raw agent routes; - sends every remaining request to Bobsled, where the GitHub session boundary protects the board and control-plane API; and
- uses
/healthonly as an upstream liveness check.
Caddy sets X-Forwarded-For, X-Forwarded-Proto, and X-Forwarded-Host for HTTP upstreams by default and ignores spoofed inbound values unless another proxy has explicitly been trusted. Do not replace those defaults with client-supplied forwarding headers. Do not enable unredacted access logging: the OAuth callback query contains short-lived authorization material, and webhook headers contain signature evidence.
Keep the current private endpoint working during activation. A host Ethernet bridge is not required for this topology; changing the host's live uplink is a separate, console-backed infrastructure migration with an independent rollback plan.
- Confirm the private upstream responds to
GET /health; do not remove the existing Incus proxy or other known-good route. - Store a new, random
BOBSLED_SESSION_SECRETof at least 32 bytes in protected service configuration. Never put it in source, the Caddyfile, shell arguments, logs, or chat. - Set
BOBSLED_PUBLIC_ORIGIN=https://bobsled.frostyard.organdBOBSLED_OPERATOR_AUTH_MODE=githubalongside the already-protected GitHub client credentials. - Register exactly
https://bobsled.frostyard.org/auth/github/callbackas the GitHub App callback URL. - Configure
BOBSLED_UPSTREAMfor the Caddy service with the private endpoint, import the reference site block, and validate the complete Caddy configuration before reload. - Restart Bobsled while it is still reachable only through the private route. Verify
/healthreports operator modegithub; an incomplete configuration must instead reportgithub_unconfiguredand fail protected routes closed. - Reload Caddy, then prove that HTTPS is valid,
/redirects an unauthenticated browser to GitHub,/api/repositoriesreturns401without a session, and/agents/bobsledreturns404at ingress. - Complete an interactive login with an active
frostyardmember and verify a protected read-only request plus logout. Keep the prior Caddy configuration and Bobsled release available for rollback until this succeeds. - Configure the GitHub App webhook URL as
https://bobsled.frostyard.org/channels/github/webhookonly after its matching protected webhook secret is installed. Prove a GitHubping, a deduplicated delivery, and a rejected invalid signature before enabling event-driven dispatch.
The OAuth proof requires a human browser, and the webhook secret must be entered independently into GitHub and protected host configuration. Neither value belongs in repository history or command output.
A deployment that previously admitted work in local_trusted mode retains those records under the synthetic local-operator owner. GitHub authentication deliberately does not make another principal's records visible, so the board will initially appear empty even though the history remains intact.
After the intended operator has completed GitHub login, an administrator may explicitly transfer that legacy history with:
BOBSLED_DATA_DIR=/var/lib/bobsled/data npm run migrate:local-operator -- --confirm-single-active-github-userThe migration refuses to run unless exactly one distinct, unexpired frostyard GitHub principal has an active session. It checks all owner-scoped idempotency keys for conflicts, atomically transfers runs plus related issue actions and draft publications, and appends run.owner_transferred audit evidence without rewriting historical actor IDs. It is idempotent and prints only transfer counts, not the principal's identity or session data. Back up the database before invoking it.
Each host authenticates Codex and Copilot independently. Never copy auth.json between development and production: two hosts refreshing cloned OAuth state can invalidate one another.
Run npm run auth:codex and npm run auth:copilot interactively as the service user with BOBSLED_AUTH_FILE pointing at the protected host credential path. Pairing or browser authorization happens directly between the operator and provider; never relay authorization codes through chat or logs.
After authentication, restart the service and run npm run auth:status under the same environment. The status command reports provider presence without displaying credential values.
Before activation, run:
npm ci
npm test
npm run check:types
npm run build
npm audit --omit=devAfter activation, verify /health, confirm the service has not entered a restart loop, and retain the previous release until the new version has handled representative read-only requests successfully.