0813e90510
Current state: the onboarding doc-harness and its Stage 1 completed-clean result. ROADMAP: spell out Stage 2 (regtest buyer-pays) under the agent-payment-connect item. Drop the resolved GET /v1/admin/products 405 debt item.
174 lines
11 KiB
Markdown
174 lines
11 KiB
Markdown
# 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 or migrations 0020–0022, 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
|
||
Dockerfile Makefile s9pk.mk build pipeline
|
||
keysat-xyz-landing/ keysat-docs/ keysat-registry-landing/ public sites → guides/website-copy.md
|
||
licensing-client-{rust,ts,python,go}/ the four SDK source repos
|
||
activate-license-template/ Tauri desktop template for license activation
|
||
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.
|
||
- 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 (so
|
||
`make universal` excludes it); revisit only if a riscv StartOS target appears.
|
||
- StartOS Community Registry submission criteria — Start9 hasn't published the
|
||
checklist; reach out directly when ready.
|
||
- Registry icon doesn't render in the StartOS marketplace (see `guides/startos-packaging.md`).
|
||
- 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.
|
||
- **Operator action (manual; needs the master admin key — a read-only key can't
|
||
write):** grant `unlimited_merchant_profiles` to the **Pro and Patron** tiers on
|
||
the live master. Confirmed 2026-06-16 against `licensing.keysat.xyz` that the slug
|
||
is absent from all three keysat policies (Creator/Pro/Patron), from the master's
|
||
own Patron self-license, and from the product `entitlements_catalog`. Steps: add
|
||
the slug to the keysat product `entitlements_catalog`, then to the Pro + Patron
|
||
policy entitlements (admin UI), then re-issue the master self-license so it takes
|
||
effect.
|
||
|
||
## Current state (2026-06-16)
|
||
|
||
- **Live (registry/canonical)**: `registry.keysat.xyz` + `files.keysat.xyz/keysat.s9pk`
|
||
publish **`0.2.0:57`** — universal multi-arch (x86_64 + aarch64), GitHub release
|
||
`v0.2.0-57`. Migrations 0020–0023; four SDKs published; `keysat.xyz` +
|
||
`docs.keysat.xyz` deployed.
|
||
`:57` shipped the **`merchant-onboard`** scoped role (catalog + license self-serve, no
|
||
master key; `src/api/api_keys.rs`) — see git log for detail.
|
||
- **Live box `immense-voyage.local`**: `:57` was deployed this session via `make install`
|
||
(`start-cli package install` returned clean; StartOS applies the swap async — **not
|
||
independently confirmed**; verify the StartOS UI shows `0.2.0:57`). `publish.sh` now runs
|
||
`make install` as step 5, so future ships auto-deploy (best-effort, non-fatal).
|
||
|
||
- **Onboarding doc-harness — Stage 1 (Path 1, no payments): `completed-clean` this session.**
|
||
New disposable harness at `licensing-service-startos/onboarding-harness/` boots a fresh
|
||
fixture, mints a `merchant-onboard` key, serves `keysat-docs/` as the corpus, scaffolds a
|
||
pristine Next.js/TS proof-of-work (`sandbox-template/`), then runs the global
|
||
`onboarding-tester` agent **docs-only**. Loop converged 5→1→0 stumbles over 3 runs; the
|
||
publishable walkthrough is harvested into `keysat-docs/agent.html` (#worked-example). Doc
|
||
fixes shipped: `integrate.html` (real v0.3 SDK shape — `verify()` throws + returns
|
||
`VerifyOk{payload,…}`, no `valid` bool, `LicensingError`/`.code`), `agent.html`
|
||
(merchant-onboard role row, product/policy-create workflows, `buyer_note`→`note`, license
|
||
`/search` endpoint, worked example), `wire-format.html` (issuer-pubkey response shape). Also
|
||
`openapi.rs` (licenses `product_id` filter, removed phantom `GET /v1/admin/products`, added
|
||
`/v1/admin/licenses/search`, price-field notes) — **served-spec fixes; fixture was rebuilt to
|
||
test, but these reach the live spec only on the next daemon release.** keysat-docs static
|
||
fixes deploy independently. Full record: `onboarding-harness/STAGE1-RESULT.md`. **Stage 2
|
||
(Path 2, regtest buyer-pays) is gated on agent-payment-connect slices 3–5 below.**
|
||
|
||
- **In progress — agent-payment-connect (phase 2)**. Approved spec:
|
||
`plans/agent-payment-connect-scope.md`. Lets a scoped key connect a BTCPay provider, but
|
||
ONLY on a sandbox daemon and ONLY for a non-mainnet network — never folded into a role
|
||
(a key that can repoint settlement is a fund-redirection key).
|
||
- **Foundation committed this session (`3afac07`, origin+gitea; NOT version-bumped)** —
|
||
slices 1–2 of 5: `Config.sandbox_mode` (env `KEYSAT_SANDBOX_MODE`, never API-settable;
|
||
surfaced in `/v1/admin/tier`); migration 0024 `scoped_api_keys.extra_scopes`; per-key
|
||
à-la-carte scopes (`GRANTABLE_EXTRA_SCOPES=["payment_providers:write"]`, granted via
|
||
`extra_scopes`, in NO role — `grants()` carves it out of full-admin's wildcard;
|
||
fail-closed parsing). Reviewer pass clean after fixing the full-admin-wildcard P1.
|
||
- **Pending — slices 3–5**: `require_provider_connect` gate (master→any; scoped+
|
||
`payment_providers:write`→only if `sandbox_mode` AND non-mainnet); BTCPay OAuth wiring
|
||
(record `scoped_initiator` in `btcpay_authorize_state` at `start_connect`, network-check
|
||
at `finish_connect`, migration 0025); Zaprite stays master-only. **The BTCPay on-chain
|
||
address network detection MUST be validated against a live regtest box** before
|
||
shipping (classify address prefix `bc1`/`tb1`/`bcrt1`, fail-closed to mainnet; the
|
||
payment-method id is `BTC-CHAIN` vs `BTC` by version).
|
||
|
||
- **Work queue (next, in order)**:
|
||
1. Build gate slices 3–5 (above) — validate the BTCPay address fetch on regtest.
|
||
2. Confirm `:57` is live on `immense-voyage.local` (StartOS UI).
|
||
3. Operator data action (master key): grant `unlimited_merchant_profiles` to Pro/Patron.
|
||
4. 3 multi-profile UIs + split `audit:read` (ROADMAP / Open TODOs).
|
||
|
||
- **P2/P3 debt (unchanged)**: `set_product_entitlements_catalog` missing `rows_affected`
|
||
guard; no rate-limit on purchase/redeem (spoofable XFF); `422`/`415` plain-text not JSON;
|
||
`slug` unvalidated; `GET /v1/admin/products` 405 vs OpenAPI; dep advisories (`sqlx`→≥0.8.1,
|
||
`rustls-webpki`→≥0.103.12); no CI / fmt-clippy unenforced; field-naming drift; outbound
|
||
webhook SSRF; design-contract conformance (see ROADMAP).
|
||
|
||
- **Tests/build**: `cargo check` + `npm run check` clean (1 intentional deprecation
|
||
warning); full suite green — lib unit **13**, api **59**, subscriptions 7, upgrades 9,
|
||
worker 3, crosscheck 4, migrations 9 (through 0024). No new clippy warnings. FK
|
||
enforcement confirmed — sqlx pool sets `foreign_keys(true)` per connection.
|