# 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.

**Inbox check:** At session start, if `~/Projects/standards/INBOX.md` exists, scan it for items tagged
`(keysat)` and surface them before proposing next steps; triage with `/triage`.

## 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 <suite>` | `cargo test <name>`.
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-20)

- **Live / canonical: `0.2.0:62`** — universal s9pk (x86_64 + aarch64) published this session via `publish.sh`:
  byte-verified at `files.keysat.xyz/keysat.s9pk` (63001063 bytes) + GitHub release `v0.2.0-62` + registry-registered
  (git `cc08230`); installed on `immense-voyage.local` (`start-cli package installed-version keysat` → `0.2.0:62`),
  master `licensing.keysat.xyz` `/healthz` 200. `:62` is the buy-page single-quote-escape fix (forked `html_escape`
  deduped onto the canonical `api::mod.rs` impl + unit test). Migrations through 0025.
- **This session — keysat inbox triage, buy-page fix, `:62` release.** Routed captured items into ROADMAP (new
  sections): design-contract cleanup, registry version-retention research, reorder-entitlements UI, refactor-scout
  code-health cluster. Fixed + shipped the buy-page HTML under-escape. Discarded 4 done/duplicate items. Added the
  portable inbox-check line to AGENTS.md.
- **Shipped earlier in `:61` (still true):** self-license tier clamp + re-verify-on-refresh hardening; all four
  SDKs at offline-expiry parity (Python 0.3.0 PyPI, Go v0.2.0). Four SDKs + two public sites (keysat.xyz,
  docs.keysat.xyz) live. All repos on **GitHub + gitea**.
- **Next (priority):** 1) eval P2 hardening (XFF rate-limit, dep bumps, admin/public port split — see ROADMAP).
  2) split `audit:read` scope. 3) work the newly-routed ROADMAP items (design-contract cleanup, etc.).
- **Tests/build:** daemon `cargo check` + `cargo test --lib` green (new `html_escape` single-quote test passes);
  wrapper `tsc` clean; universal s9pk published clean. No CI.
