Move domain config from .env to a private SQLite store

Forges (multi-host) + tokens, project directories, and git identity now live in a private SQLite config store (internal/store, modernc.org/sqlite) on a /data named volume that is not bind-mounted or exposed, so credentials aren't reachable outside the container. New Settings page (/settings) + <settings-panel> with /api/config CRUD. Scanner reads roots fresh from the store each cycle; service resolves forges per-repo from the store and reapplies per-forge git auth on change. First run seeds the store from .env. Overturns the old no-datastore/.env-config laws (AGENT.md updated). Verified live end-to-end.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-09-22 05:31:28 -04:00
parent 69d38484e8
commit e30c3b632a
18 changed files with 1208 additions and 160 deletions
+61 -26
View File
@@ -121,10 +121,12 @@ values** rather than reintroducing hardcoded hex.
- **The background scanner is readonly.** It may run `status`, `rev-list`,
`for-each-ref`, `log`, and (only when explicitly enabled) `fetch`. It must never
run a command that changes local state.
- **Never invent local persistence for domain state.** If something must survive a
restart and it isn't already in Git, it is either app config (`.env`, Section
1.4) or peruser UI state (browser `localStorage`, Section 4). Adding any other
datastore (SQLite, a serverside DB) requires asking first — the default is no.
- **Git stays the system of record for repository data** — never mirror repo
contents into another store. The one sanctioned datastore is the **config store**
(§1.5): a private SQLite DB holding *app configuration* (forge hosts + tokens,
the project directories to scan, git identity), not git data. Anything beyond
that (mirroring repo/PR data, a serverside app DB) still requires asking first.
Peruser UI state remains in browser `localStorage` (§4).
### 1.4 Destructive Git operations are explicit, confirmed, and never automatic
@@ -142,12 +144,24 @@ must be treated as such:
destructive default. Prefer the safe variant (`--force-with-lease` over
`--force`) and surface it as such.
### 1.5 Configuration via `.env`
### 1.5 Configuration: bootstrap `.env` + a private config store
Repo scan roots, the `git` binary path, scan/fetch behavior, the listen address,
and any optional forge tokens are all configurable through a single `.env` file. A
committed `.env.example` documents every variable. **Never commit a real `.env`**
and never hardcode paths, tokens, hosts, or the set of watched repositories.
Configuration is split:
- **Bootstrap `.env`** — only what's needed to start the process and can't live in
the DB: listen address, TLS, `APP_ENV`, `LOG_FILE`, the `git` binary, scan
tuning (interval/depth/ignore/fetch), and the **config DB path**. A committed
`.env.example` documents every variable; **never commit a real `.env`**.
- **Config store (`internal/store`, SQLite)** — the domain config that used to
live in `.env`: **forge hosts + access tokens, the project directories to scan,
and the git commit identity.** Managed at runtime in the app's **Settings**
(`/settings`), not by editing files. On first run with an empty DB it is
**seeded** from the `.env` values (`GITEA_*`, `GIT_REPO_ROOTS`, `GIT_USER_*`);
after that those `.env` values are ignored.
- **The DB must not be reachable outside the container.** It lives on a **private
named Docker volume** (`/data`) — never bindmounted into the project, never on a
published port. Tokens are stored there relying on that isolation. Never hardcode
tokens/hosts/paths in code.
### 1.6 Everything runs in Docker / dockercompose
@@ -187,14 +201,15 @@ MCP handler.
| MCP server | **`github.com/modelcontextprotocol/go-sdk`**, served over **Streamable HTTP** at `/mcp` | Claude Desktop connects as a custom connector. Tools are thin adapters over the shared service layer (§1.7). See Section 8. |
| Realtime (app → browser) | **ServerSent Events** (`net/http`, stdlib) | Push activity + handoff notifications and live repo updates to the components; replaces list polling over time. |
| Diff rendering | Server produces unified diff from `git`; client renders it in a component | No heavy client diff lib without asking. |
| Config | **`.env`** via `github.com/joho/godotenv` + a typed config struct | Section 1.5. |
| Logging | **`log/slog`** → stdout/stderr (structured), optional rotating file sink | See Section 7. No database sink (there is no database). |
| Bootstrap config | **`.env`** via `github.com/joho/godotenv` + a typed config struct | Section 1.5 (bootstrap only). |
| Config store | **SQLite via `modernc.org/sqlite`** (pure Go, no CGO) in `internal/store`, on a private `/data` volume | Forges + tokens, project dirs, git identity. Not the git data store (§1.3). |
| Logging | **`log/slog`** → stdout/stderr (structured), optional rotating file sink | See Section 7. |
| Hot reload (dev) | **air** (`github.com/air-verse/air`) | Inside the app container. |
Anything not in this table that you want to add as a dependency: **propose it and
wait for approval.** Keep the dependency surface small. Note in particular there is
**no database and no auth library** — do not add one without asking (Section 1.3,
Section 0).
wait for approval.** Keep the dependency surface small. There is **no auth library**
(§0) and **no datastore beyond the config store** (§1.3, §1.5) — do not add one
without asking.
---
@@ -211,7 +226,8 @@ Section 0).
├── cmd/
│ └── server/main.go # entrypoint: wire config, git, scanner, router
├── internal/
│ ├── config/ # .env loading, typed config struct
│ ├── config/ # bootstrap .env loading, typed config struct (§1.5)
│ ├── store/ # SQLite config store: forges+tokens, project dirs, identity (§1.5)
│ ├── git/ # THE Git boundary: interface + os/exec impl (all git ops)
│ ├── repos/ # discovery, in-memory index/cache, refresh scanner worker
│ ├── service/ # the ONE service layer both the HTTP API and MCP call (§1.7)
@@ -231,8 +247,10 @@ Section 0).
```
> If a new concern doesn't fit cleanly, **ask** before inventing a new toplevel
> directory. Keep `components/` strictly for web components. There is deliberately
> **no `migrations/` and no `db/`** — see Section 1.3.
> directory. Keep `components/` strictly for web components. The only datastore is
> the SQLite **config store** (`internal/store`) on the private `/data` volume
> (§1.5) — schema is created incode (`CREATE TABLE IF NOT EXISTS`); there is no
> `migrations/` framework yet (add one if the schema grows nontrivially).
---
@@ -432,9 +450,12 @@ later behind the same interface).
per §1.4**, names the PR/branch, and prefers the tidy default (squashmerge +
delete branch). Note a merged PR remains in the host's history; "clean up"
means removing the **branch**, not falsifying history.
- **Enablement is perhost and tokengated.** Tokens come from `.env`
(e.g. `GITEA_TOKEN`); with no token the forge features are simply absent and
the rest of the app works unchanged (**graceful degradation**).
- **Multiple hosts, configured in the store.** Forges (base URL + token) live in
the **config store** (§1.5) and are managed in Settings — not `.env`. A repo is
forgeenabled when a remote's host matches a configured forge; with none
matching the forge features are simply absent (**graceful degradation**). The
service resolves a repo → provider by matching remotes (preferring `origin`)
against the stored forges, caching a client per host.
- The provider is inferred from a repo's remote URL. Never send repo data to a
host the user didn't configure.
@@ -502,9 +523,10 @@ component carries its own context.
4. If it touches repositories or forges: put the logic in the **service layer**
(§1.7) over the `internal/git` / `internal/forge` boundaries — never in an HTTP
or MCP handler — so both the GUI and Claude get it. Respect **Git = system of
record** (§1.3), treat **destructive operations** per §1.4, and never add a
datastore. A new capability generally means: service method → HTTP handler →
MCP tool → UI control.
record** (§1.3), treat **destructive operations** per §1.4, and don't mirror
repo/PR data into a datastore (app *config* goes in the store, §1.5). A new
capability generally means: service method → HTTP handler → MCP tool → UI
control.
5. If it needs a **new dependency** or a **new toplevel folder**, propose it and
wait for approval.
6. Implement, run it in the **dockercompose dev environment**, verify hot reload
@@ -518,9 +540,22 @@ component carries its own context.
*(Claude Code: surface these to the human at the first relevant moment; don't
silently guess.)*
- **Repo discovery strategy:** recursive scan of `GIT_REPO_ROOTS` (max depth?
ignore globs?) vs an explicit list of repo paths. Default assumption: recursive
scan with a configurable depth.
- **RESOLVED 2026-09-22:** **Domain config moved from `.env` to a private
SQLite store** (`internal/store`, §1.5): forges+tokens, project directories, git
identity — managed in Settings (`/settings`), seeded from `.env` on first run.
DB on a private `/data` volume (not bindmounted, no port). Forge is now
**multihost**; git auth sets an `http.extraheader` per forge.
- **Container mount constraint (multiple project dirs):** the container can only
scan host paths that are **bindmounted at `up` time**. Today `~/Projects` is
mounted to `/repos`, so project dirs added in Settings must resolve under a
mounted base. A dir outside it needs a new compose mount — surface this if asked
to add such a path.
- **Token encryption at rest:** tokens are stored plaintext in the private DB
(isolation is the control). Confirm before adding encryptionatrest (a key
would then need storing too).
- **Repo discovery strategy:** recursive scan of the store's project directories
(max depth / ignore from `.env` scan tuning). Default: recursive with a
configurable depth.
- **Background `fetch`:** off by default (no unsolicited network). Confirm whether
it should be enabled, and the interval / rate limit, before turning it on.
- **Git access library:** system `git` via `os/exec` is the locked default