# AGENTS.md — Keysat workspace Self-hosted, Bitcoin-native software licensing service running as a StartOS 0.4.x package, with four wire-compatible SDKs and a public landing/docs site. This file holds whole-repo, every-session facts. Subsystem detail lives in scoped guides under `docs/guides/` (symlinked to `.claude/rules/` so Claude Code auto-loads each when you edit matching files). **Before editing a subsystem, read its guide** — see the index below. ## Stack - **Daemon**: Rust 1.88, `axum`, `sqlx` + SQLite, Ed25519 signing. - **Wrapper**: TypeScript, `@start9labs/start-sdk ^1.3.2`, `@vercel/ncc` bundle, Node 22. - **SDKs**: TS (npm), Rust (crates.io), Python (PyPI), Go (proxy.golang.org). - **Platform**: StartOS 0.4.0.x (LXC under the hood — commands/paths reflect that, not Docker). - **Payment providers**: BTCPay Server (required dep); Zaprite (optional, gated by `zaprite_payments`). ## Subsystem guides (read before editing the area) - Before editing the daemon source, read `docs/guides/daemon-architecture.md`. - Before editing payment / provider / merchant-profile code, the scoped-connect gate, or migrations 0020–0022 + 0024–0025, read `docs/guides/payments.md`. - Before touching self-license or tier-gating code, read `docs/guides/licensing-tiers.md`. - Before changing the LIC1 wire format, crypto, or crosscheck fixtures, read `docs/guides/crypto-wire-format.md`. - Before building, bumping the version, or editing the StartOS wrapper, read `docs/guides/startos-packaging.md`. - Before editing the admin SPA (`web/index.html`), read `docs/guides/admin-ui.md`. - Before editing public site/docs copy, read `docs/guides/website-copy.md`. - **Before building or changing any user-facing UI (landing, docs, admin SPA), read `design/DESIGN.md` and `design/tokens.tokens.json` and conform to them** — the brand contract; pull colors/type/space/radii/shadows from the tokens, never hardcode off-scale values. - Before adding/altering tests or relying on lint/CI, read `docs/guides/testing.md`. ## Build / test / run (quick ref) From `licensing-service-startos/`: `make x86` | `make arm` | `make universal` | `make install` | `make clean` | `npm run check`. From `licensing-service-startos/licensing-service/`: `cargo check` | `cargo build --release` | `cargo test` | `cargo test --test ` | `cargo test `. Details, the version-bump-before-build rule, and release scripts: `docs/guides/startos-packaging.md`. Test suites, the no-CI / formatting-not-enforced status, and known-failing tests: `docs/guides/testing.md`. ## Directory layout ``` licensing-service-startos/ daemon + StartOS wrapper (s9pk package source) licensing-service/src/ Rust daemon → guides/daemon-architecture.md licensing-service/migrations/ SQLite migrations (numbered, additive) licensing-service/web/index.html embedded admin SPA → guides/admin-ui.md licensing-service/tests/ integration suites → guides/testing.md startos/ wrapper TS → guides/startos-packaging.md onboarding-harness/ docs-onboarding test rig → onboarding-harness/README.md Dockerfile Makefile s9pk.mk build pipeline keysat-xyz-landing/ keysat-docs/ public sites → guides/website-copy.md licensing-client-{rust,ts,python,go}/ the four SDK source repos activate-license-template/ StartOS license-activation wrapper template (drop-in actions) design/ design contract (DESIGN.md + tokens.tokens.json) + brand/ assets; original Claude Design system archived in design/_imports/ plans/ design specs (multi-provider-payment-model.md, keysat-smtp-emails.md) tests/crosscheck/ cross-language LIC1 verifier → guides/crypto-wire-format.md ``` Note: the daemon (`licensing-service-startos`, repo `keysat`), each SDK, and `plans/` are **separate git repos** — commit code/plan changes in their own repo. The root `Licensing` repo (`keysat-root`) tracks only `AGENTS.md` + `docs/guides/` + `.claude/rules/` + `EVALUATION.md` (the latest full-eval report; overwritten each run, history in git log). **Remotes differ per repo**: the daemon's `main` tracks **GitHub** (`origin`, the public upstream) with a `gitea` backup — plain `git push` goes to GitHub, so also `git push gitea main`; root + plans are **Gitea-only**. Run `git remote -v` (full) and check what the branch tracks before pushing. ## Conventions (whole-repo) - Daemon licensed `LicenseRef-Keysat-1.0` (custom, source-available); SDKs MIT. - Commits in imperative mood, body only when the "why" isn't obvious. **Sign as Keysat (Grant), not Claude** — git user is `Keysat`. - Direct push to `main` + run `~/.keysat/publish.sh` is the authorized release flow until launch. - **SDK releases are independent + manual** (no `publish.sh` equivalent): Go via a pushed git tag (go-proxy serves from GitHub); Python via `pyproject.toml` + twine→PyPI; TS via npm; Rust via crates.io — the operator runs each with their own registry credentials. - Never rewrite user-facing copy outside the explicit scope of a request. ## Never - **No AI co-authorship** on commits or PRs (no "Co-Authored-By", no "Generated with…"). - **Don't push `--no-verify`** or bypass hooks unless explicitly authorized. - **Don't commit built artifacts** (`*.s9pk`, `keysat-*.s9pk`, `javascript/`) or **secrets** — reference env-var names; real values live in `~/.keysat/filebrowser.env` and `/data/keysat-license.txt` outside the repo. ## Memory references Operator-specific memories at `~/.claude/projects/-Users-macpro-Projects-keysat/memory/` (scan before a major change): `keysat_release_workflow.md`, `no_unauthorized_copy_changes.md`, `keysat_admin_ui_pill_convention.md`, `startos_lxc.md`, `startos_registry_icon_unrenderable.md`, `keysat_open_threads.md`. ## Open TODOs - `riscv` build target is unverified and not declared in the manifest; the wrapper `Makefile` now pins `ARCHES` to `x86 arm` so no target (even a bare `make`) attempts it. Revisit only if a riscv StartOS target appears. - Split `audit:read` out of the blanket `:read` scope into its own tier so a Read-only scoped key can read dashboards/licenses but NOT the full audit log (`api/api_keys.rs::Role::grants`). Deferred from the scoped-keys session. ## Current state (2026-06-19) - **Live / canonical: `0.2.0:61`** — universal s9pk (x86_64 + aarch64) at `files.keysat.xyz/keysat.s9pk` (byte-verified) + GitHub release `v0.2.0-61` + registry-registered; installed on `immense-voyage.local`, master `licensing.keysat.xyz` returns 200. Migrations through 0025; four SDKs + two public sites (keysat.xyz, docs.keysat.xyz) live. All repos on **GitHub + gitea**. - **This session — adversarial self-license pressure-test (security-auditor → exerciser → reviewer) → two fixes shipped in `:61`.** Both in `refresh_self_tier_from_db` (see guides/licensing-tiers.md): (1) the unsigned `licenses.entitlements_json` column could *widen* the daemon's own tier past its signed key — any box-owner with any valid key could self-upgrade to Patron via a DB edit; now clamped to a signed **ceiling** (DB narrows, never widens; `clamp_to_signed_ceiling`). (2) An expired/tampered self-license lingered until restart; now re-verified each refresh and demoted like revoked/suspended. Crypto + offline master key confirmed sound (no signature-forgery path). Commit messages kept **generic** per operator request. - **SDK offline-expiry parity resolved + published (all four).** Python `Verifier.verify_with_time` + Go `ParseAndVerifyAt`/`ErrExpired` now reject expired keys offline, matching Rust/TS (reviewer-approved). **Go published** (tag `v0.2.0`, go-proxy) and **Python published** (`keysat-licensing-client 0.3.0` on PyPI). Both public sites redeployed (landing + docs, 200). - **Next (priority):** 1) eval P2 hardening (XFF rate-limit, dep bumps, admin/public port split). 2) split `audit:read` scope. (Nice-to-have: document the new SDK verify methods in keysat-docs.) - **Tests/build:** daemon `cargo test` green (~125 / 8 suites, incl. 5 new self-license clamp tests); wrapper `tsc` clean; Python SDK pytest 14 green + Go `go test` green (both incl. new expiry tests). No CI.