diff --git a/apps/loopover-ui/content/docs/ams-deployment.mdx b/apps/loopover-ui/content/docs/ams-deployment.mdx new file mode 100644 index 0000000000..fe12d10194 --- /dev/null +++ b/apps/loopover-ui/content/docs/ams-deployment.mdx @@ -0,0 +1,253 @@ +--- +title: AMS deployment guide +description: Deploy @loopover/miner in laptop mode (single machine, zero Docker) or fleet mode (containerized workers) -- both 100% client-side, credentials never baked into images. +--- + +Two form factors for running `@loopover/miner`: **laptop mode** (single machine, zero Docker) and +**fleet mode** (containerized workers with a shared data volume). Both are 100% client-side for +core operation — the miner never uploads source and never requires a hosted LoopOver callback to +boot. Credentials (GitHub tokens, etc.) stay on the operator's machine or in their own secret +store; nothing is baked into images. + +=22.13.0. State is SQLite files under ~/.config/loopover-miner/ (override with LOOPOVER_MINER_CONFIG_DIR). Setup is npm install -g @loopover/miner or a workspace build — one Node process, local disk for ledgers/queues.", + }, + { + title: "Fleet mode", + description: + "Best for many parallel miner attempts on a host or small cluster. Needs Docker (or a compatible runtime) + the miner image. Same SQLite layout, but on a mounted /data (or LOOPOVER_MINER_CONFIG_DIR) volume. Setup is docker build + docker run with env and a volume — one container per worker, scale horizontally by adding containers.", + }, + ]} +/> + +## Coding-agent provider configuration + +For provider selection and the CLI-specific model/timeout overrides, see +[Coding-agent driver](/docs/miner-coding-agent). + +## Laptop mode walkthrough + +### Install Node.js and the package + + + +### Inspect the install and local state + +`status` and `doctor` stay offline; `init --verify-token` is optional and makes one authenticated +GitHub call up front: + + + + + `init --interactive` offers "Authorize with GitHub" (device flow — visit a URL, enter a short + code, no token to copy or paste) as its first option once the miner's GitHub App OAuth client is + configured; the original pasted-PAT prompt stays available as option 2, and is what the wizard + falls back to automatically on any device-flow failure. Unconfigured, the wizard is + byte-identical to the pasted-token-only prompt. Either way, the resulting `GITHUB_TOKEN` acts as + your own GitHub account — there is no separate bot identity. + + +### Expected layout after first use + +Sixteen SQLite stores default into one directory (default paths shown): + + + +Not every file appears immediately: `laptop-state` is written by `init`, and each of the others is +created the first time its subsystem actually runs (an attempt, a discovery pass, a replay, an Orb +export, …), so a fresh install that has only run `status`/`doctor` shows a subset. Override the +directory for every store at once with `LOOPOVER_MINER_CONFIG_DIR` or `XDG_CONFIG_HOME`; every +store except `laptop-state.sqlite3` (directory only) also honors its own +`LOOPOVER_MINER__DB` path override to relocate an individual file. `doctor`'s +`store-integrity:*` checks report the persistent stores, so it is the quickest way to confirm what +exists and is readable on disk. + +### Optional per-repo miner goals + +Copy `.loopover-miner.yml.example` (repo root) to a target repo as `.loopover-miner.yml`. See the +`.loopover-miner.yml` field reference in +[`packages/loopover-miner/docs/miner-goal-spec.md`](https://github.com/JSONbored/loopover/blob/main/packages/loopover-miner/docs/miner-goal-spec.md). + +## Fleet mode walkthrough + +Build the fleet image from the **monorepo root** (the Dockerfile needs the full workspace on disk +before `npm ci`): + + + +Run a disposable worker with persistent SQLite state on a mounted volume. Inject secrets at +runtime (never bake them into the image): + + + +The image entrypoint is `loopover-miner`; pass subcommands after the image name (`status`, +`doctor`, `claim`, …). + + + + + A plain `-e GITHUB_TOKEN` value is visible in plaintext via `docker inspect`/`docker compose + config` and any full-env dump of the running container. For Docker Swarm/Kubernetes-managed + secrets (mounted as a file, e.g. at `/run/secrets/github_token`), set `GITHUB_TOKEN_FILE` to that + mount path instead — the miner reads and trims the file's contents at startup and uses it exactly + as if `GITHUB_TOKEN` had been set directly. If both are set, the plain `GITHUB_TOKEN` value + always wins. A missing or unreadable `GITHUB_TOKEN_FILE` fails the container fast with a clear + error naming the file path, rather than silently proceeding with no credential. + + + + +The repo-root `docker-compose.yml` documents the **self-hosted review stack** (the LoopOver +API/Orb), not the miner CLI. Miners are clients of that stack (or of github.com directly) and do +not require it to run locally. + +### Docker Compose (fleet mode) + +Instead of a hand-assembled `docker run`, +[`docker-compose.miner.yml`](https://github.com/JSONbored/loopover/blob/main/packages/loopover-miner/docker-compose.miner.yml) +defines a long-lived `miner` service (built from this package's Dockerfile, +`restart: unless-stopped`, state on a named `miner-data` volume). Credentials come from an env +file, never inlined: + + + + + `docker compose -f docker-compose.miner.yml up -d --scale miner=N` gives every replica the + **same** `miner-data` volume — and the miner's SQLite ledgers are **not** safe for concurrent + access, so N replicas on one volume will contend/corrupt. To run N **isolated** workers, give + each its own state: run N separate compose projects (`docker compose -p miner-1 …`, `-p miner-2 + …` — `-p` namespaces the volume) or point each at a distinct `LOOPOVER_MINER_CONFIG_DIR` on its + own mount. For built-in isolated horizontal scaling, use the Kubernetes StatefulSet in + [`k8s/`](https://github.com/JSONbored/loopover/tree/main/k8s) (per-pod volumes). + + +## Bridging AMS state with ORB observability + +Running fleet mode alongside ORB's self-hosted observability profile needs one extra step so the +Grafana AMS panels actually populate — the fleet miner's named Docker volume and the host +directory ORB's exporter reads don't line up on their own. See +[Running ORB and AMS together](/docs/self-hosting-unified-ams-orb) for the full walkthrough. + +## Bare-host (systemd, no Docker) + +To run the miner continuously on a plain Linux host without Docker, supervise `loopover-miner +loop` — the autonomous discover → attempt → manage daemon — with systemd. +[`systemd/loopover-miner.service.example`](https://github.com/JSONbored/loopover/blob/main/systemd/loopover-miner.service.example) +is a ready-to-adapt persistent unit; its header carries the full install steps: + + + +Because `loop` is a **long-running daemon that schedules its own cycles**, it is a persistent +`Type=simple` service (with `Restart=on-failure`) — **not** a oneshot unit driven by a `.timer`. +Keep `GITHUB_TOKEN` (and any coding-agent credentials) in a root-owned `0600` `EnvironmentFile`, +never in the unit file. Follow the loop with `journalctl -u loopover-miner -f`; `systemctl stop` +sends SIGTERM, which the loop handles cleanly at its next kill-switch check. + + + Want the dashboard too? + [`systemd/loopover-miner-ui.service.example`](https://github.com/JSONbored/loopover/blob/main/systemd/loopover-miner-ui.service.example) + is a companion unit that serves `apps/loopover-miner-ui` persistently over the same local state. + + +## Invariants + +- Core miner bookkeeping (claims, plans, queues, ledgers) works offline after install. +- `loopover-miner status` and `loopover-miner doctor` make **no network calls**. +- Discovery/ranking primitives that touch GitHub only run when explicitly invoked and only perform + documented GETs unless a future command says otherwise. +- Operators own secret injection; images and packages ship without embedded tokens. + +For operational scenarios (ledger corruption, two miners on one state dir, post-upgrade schema +migration) and measured CPU/RAM/disk sizing, see +[`packages/loopover-miner/docs/operations-runbook.md`](https://github.com/JSONbored/loopover/blob/main/packages/loopover-miner/docs/operations-runbook.md) +and +[`packages/loopover-miner/docs/sizing.md`](https://github.com/JSONbored/loopover/blob/main/packages/loopover-miner/docs/sizing.md). + +## Optional hosted discovery plane (opt-in) + +The hosted discovery-index is **off by default** — unlike Orb fleet export (`ORB_AIR_GAP` is the +only opt-out). Operators who want cross-fleet metadata queries or soft-claim coordination must opt +in explicitly. See +[`packages/loopover-miner/docs/discovery-plane-operator-guide.md`](https://github.com/JSONbored/loopover/blob/main/packages/loopover-miner/docs/discovery-plane-operator-guide.md). diff --git a/apps/loopover-ui/src/components/site/docs-nav.tsx b/apps/loopover-ui/src/components/site/docs-nav.tsx index 1f9d0ce7f2..97404bd7cd 100644 --- a/apps/loopover-ui/src/components/site/docs-nav.tsx +++ b/apps/loopover-ui/src/components/site/docs-nav.tsx @@ -72,6 +72,10 @@ export const docsNav: DocsGroup[] = [ { to: "/docs/maintainer-install-trust", label: "Maintainer install & trust" }, ], }, + { + title: "AMS: deployment", + items: [{ to: "/docs/ams-deployment", label: "Deployment guide" }], + }, ], }, { diff --git a/apps/loopover-ui/src/lib/selfhost-docs-audit.ts b/apps/loopover-ui/src/lib/selfhost-docs-audit.ts index da1572dd6f..09e72aafa5 100644 --- a/apps/loopover-ui/src/lib/selfhost-docs-audit.ts +++ b/apps/loopover-ui/src/lib/selfhost-docs-audit.ts @@ -231,6 +231,8 @@ export const LOOSE_DOCS_ROWS: readonly LooseDocsRow[] = [ path: "packages/loopover-miner/DEPLOYMENT.md", role: "Miner CLI deployment — explicitly not the self-host review stack.", action: "keep", + notes: + "Also published at /docs/ams-deployment (#6022) for website discoverability; this file stays the canonical source since it ships inside the published @loopover/miner package.", }, ] as const; diff --git a/apps/loopover-ui/src/routeTree.gen.ts b/apps/loopover-ui/src/routeTree.gen.ts index a88b265598..b7f3143710 100644 --- a/apps/loopover-ui/src/routeTree.gen.ts +++ b/apps/loopover-ui/src/routeTree.gen.ts @@ -57,6 +57,7 @@ import { Route as DocsGithubAppRouteImport } from './routes/docs.github-app' import { Route as DocsFumadocsSpikeApiReferenceRouteImport } from './routes/docs.fumadocs-spike-api-reference' import { Route as DocsBranchAnalysisRouteImport } from './routes/docs.branch-analysis' import { Route as DocsBetaOnboardingRouteImport } from './routes/docs.beta-onboarding' +import { Route as DocsAmsDeploymentRouteImport } from './routes/docs.ams-deployment' import { Route as DocsAiSummariesRouteImport } from './routes/docs.ai-summaries' import { Route as AppWorkbenchRouteImport } from './routes/app.workbench' import { Route as AppRunsRouteImport } from './routes/app.runs' @@ -328,6 +329,11 @@ const DocsBetaOnboardingRoute = DocsBetaOnboardingRouteImport.update({ path: '/beta-onboarding', getParentRoute: () => DocsRoute, } as any) +const DocsAmsDeploymentRoute = DocsAmsDeploymentRouteImport.update({ + id: '/ams-deployment', + path: '/ams-deployment', + getParentRoute: () => DocsRoute, +} as any) const DocsAiSummariesRoute = DocsAiSummariesRouteImport.update({ id: '/ai-summaries', path: '/ai-summaries', @@ -435,6 +441,7 @@ export interface FileRoutesByFullPath { '/app/runs': typeof AppRunsRoute '/app/workbench': typeof AppWorkbenchRoute '/docs/ai-summaries': typeof DocsAiSummariesRoute + '/docs/ams-deployment': typeof DocsAmsDeploymentRoute '/docs/beta-onboarding': typeof DocsBetaOnboardingRoute '/docs/branch-analysis': typeof DocsBranchAnalysisRoute '/docs/fumadocs-spike-api-reference': typeof DocsFumadocsSpikeApiReferenceRoute @@ -498,6 +505,7 @@ export interface FileRoutesByTo { '/app/runs': typeof AppRunsRoute '/app/workbench': typeof AppWorkbenchRoute '/docs/ai-summaries': typeof DocsAiSummariesRoute + '/docs/ams-deployment': typeof DocsAmsDeploymentRoute '/docs/beta-onboarding': typeof DocsBetaOnboardingRoute '/docs/branch-analysis': typeof DocsBranchAnalysisRoute '/docs/fumadocs-spike-api-reference': typeof DocsFumadocsSpikeApiReferenceRoute @@ -565,6 +573,7 @@ export interface FileRoutesById { '/app/runs': typeof AppRunsRoute '/app/workbench': typeof AppWorkbenchRoute '/docs/ai-summaries': typeof DocsAiSummariesRoute + '/docs/ams-deployment': typeof DocsAmsDeploymentRoute '/docs/beta-onboarding': typeof DocsBetaOnboardingRoute '/docs/branch-analysis': typeof DocsBranchAnalysisRoute '/docs/fumadocs-spike-api-reference': typeof DocsFumadocsSpikeApiReferenceRoute @@ -633,6 +642,7 @@ export interface FileRouteTypes { | '/app/runs' | '/app/workbench' | '/docs/ai-summaries' + | '/docs/ams-deployment' | '/docs/beta-onboarding' | '/docs/branch-analysis' | '/docs/fumadocs-spike-api-reference' @@ -696,6 +706,7 @@ export interface FileRouteTypes { | '/app/runs' | '/app/workbench' | '/docs/ai-summaries' + | '/docs/ams-deployment' | '/docs/beta-onboarding' | '/docs/branch-analysis' | '/docs/fumadocs-spike-api-reference' @@ -762,6 +773,7 @@ export interface FileRouteTypes { | '/app/runs' | '/app/workbench' | '/docs/ai-summaries' + | '/docs/ams-deployment' | '/docs/beta-onboarding' | '/docs/branch-analysis' | '/docs/fumadocs-spike-api-reference' @@ -1155,6 +1167,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof DocsBetaOnboardingRouteImport parentRoute: typeof DocsRoute } + '/docs/ams-deployment': { + id: '/docs/ams-deployment' + path: '/ams-deployment' + fullPath: '/docs/ams-deployment' + preLoaderRoute: typeof DocsAmsDeploymentRouteImport + parentRoute: typeof DocsRoute + } '/docs/ai-summaries': { id: '/docs/ai-summaries' path: '/ai-summaries' @@ -1320,6 +1339,7 @@ const AppRouteWithChildren = AppRoute._addFileChildren(AppRouteChildren) interface DocsRouteChildren { DocsAiSummariesRoute: typeof DocsAiSummariesRoute + DocsAmsDeploymentRoute: typeof DocsAmsDeploymentRoute DocsBetaOnboardingRoute: typeof DocsBetaOnboardingRoute DocsBranchAnalysisRoute: typeof DocsBranchAnalysisRoute DocsFumadocsSpikeApiReferenceRoute: typeof DocsFumadocsSpikeApiReferenceRoute @@ -1360,6 +1380,7 @@ interface DocsRouteChildren { const DocsRouteChildren: DocsRouteChildren = { DocsAiSummariesRoute: DocsAiSummariesRoute, + DocsAmsDeploymentRoute: DocsAmsDeploymentRoute, DocsBetaOnboardingRoute: DocsBetaOnboardingRoute, DocsBranchAnalysisRoute: DocsBranchAnalysisRoute, DocsFumadocsSpikeApiReferenceRoute: DocsFumadocsSpikeApiReferenceRoute, diff --git a/apps/loopover-ui/src/routes/docs.ams-deployment.tsx b/apps/loopover-ui/src/routes/docs.ams-deployment.tsx new file mode 100644 index 0000000000..f847b68460 --- /dev/null +++ b/apps/loopover-ui/src/routes/docs.ams-deployment.tsx @@ -0,0 +1,49 @@ +import { createFileRoute, notFound } from "@tanstack/react-router"; +import { Suspense } from "react"; + +import { DocsPage } from "@/components/site/docs-page"; +import { docsClientLoader } from "@/lib/docs-client-loader"; + +// Rendered from content/docs/ams-deployment.mdx via fumadocs-mdx's browser entry +// (docsClientLoader), through the existing DocsPage/Callout/CodeBlock/FeatureRow +// primitives -- not fumadocs-ui's bundled components. See docs-source.ts's comment +// for why the loader below resolves only a plain, serializable path string. +export const Route = createFileRoute("/docs/ams-deployment")({ + loader: async () => { + const { docsSource } = await import("@/lib/docs-source"); + const page = docsSource.getPage(["ams-deployment"]); + if (!page) throw notFound(); + return { path: page.path, title: page.data.title, description: page.data.description }; + }, + head: () => ({ + meta: [ + { title: "AMS deployment guide — LoopOver docs" }, + { + name: "description", + content: + "Deploy @loopover/miner in laptop mode (single machine, zero Docker) or fleet mode (containerized workers) — both 100% client-side, credentials never baked into images.", + }, + { property: "og:title", content: "AMS deployment guide — LoopOver docs" }, + { + property: "og:description", + content: + "Deploy @loopover/miner in laptop mode (single machine, zero Docker) or fleet mode (containerized workers) — both 100% client-side, credentials never baked into images.", + }, + { property: "og:url", content: "/docs/ams-deployment" }, + ], + links: [{ rel: "canonical", href: "/docs/ams-deployment" }], + }), + component: AmsDeployment, +}); + +function AmsDeployment() { + const { path, title, description } = Route.useLoaderData(); + const Content = docsClientLoader.getComponent(path); + return ( + + Loading…

}> + +
+
+ ); +} diff --git a/apps/loopover-ui/src/routes/docs.index.tsx b/apps/loopover-ui/src/routes/docs.index.tsx index eeb2461f0e..1eb9096880 100644 --- a/apps/loopover-ui/src/routes/docs.index.tsx +++ b/apps/loopover-ui/src/routes/docs.index.tsx @@ -73,6 +73,7 @@ const AUDIENCES: Audience[] = [ links: [ { to: "/docs/maintainer-self-hosting", label: "Self-host reviews" }, { to: "/docs/self-hosting-unified-ams-orb", label: "Unified ORB + AMS" }, + { to: "/docs/ams-deployment", label: "AMS deployment guide" }, { to: "/docs/self-hosting-docs-audit", label: "Self-host docs audit" }, { to: "/docs/maintainer-install-trust", label: "Install & trust guide" }, { to: "/docs/github-app", label: "GitHub App configuration" }, diff --git a/packages/loopover-miner/DEPLOYMENT.md b/packages/loopover-miner/DEPLOYMENT.md index 1c3b2a93d2..ea9ff90027 100644 --- a/packages/loopover-miner/DEPLOYMENT.md +++ b/packages/loopover-miner/DEPLOYMENT.md @@ -1,5 +1,9 @@ # LoopOver miner deployment +> Also published on the docs website: [AMS deployment guide](https://loopover.ai/docs/ams-deployment) +> (same content, rendered with search and the rest of the maintainer docs nav). This file remains +> the canonical source and ships inside the published `@loopover/miner` package. + Two form factors for running `@loopover/miner`: **laptop mode** (single machine, zero Docker) and **fleet mode** (containerized workers with a shared data volume). Both are 100% client-side for core operation — the miner never uploads source and never requires a hosted LoopOver callback to boot. Credentials (GitHub tokens, etc.) stay on the operator's machine or in their own secret store; nothing is baked into images. | | Laptop mode | Fleet mode |