11 KiB
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/nccbundle, 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), readdocs/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.mdanddesign/tokens.tokens.jsonand 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/ 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'smaintracks GitHub (origin, the public upstream) with agiteabackup — plaingit pushgoes to GitHub, so alsogit push gitea main; root + plans are Gitea-only. Rungit 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.shis 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-verifyor 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.envand/data/keysat-license.txtoutside 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
riscvbuild target is unverified and not declared in the manifest (somake universalexcludes 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:readout of the blanket:readscope 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_profilesto the Pro and Patron tiers on the live master. Confirmed 2026-06-16 againstlicensing.keysat.xyzthat the slug is absent from all three keysat policies (Creator/Pro/Patron), from the master's own Patron self-license, and from the productentitlements_catalog. Steps: add the slug to the keysat productentitlements_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.s9pkpublish0.2.0:57— universal multi-arch (x86_64 + aarch64), GitHub releasev0.2.0-57. Migrations 0020–0023; four SDKs published;keysat.xyz+docs.keysat.xyzdeployed.:57shipped themerchant-onboardscoped role (catalog + license self-serve, no master key;src/api/api_keys.rs) — see git log for detail. -
Live box
immense-voyage.local::57was deployed this session viamake install(start-cli package installreturned clean; StartOS applies the swap async — not independently confirmed; verify the StartOS UI shows0.2.0:57).publish.shnow runsmake installas step 5, so future ships auto-deploy (best-effort, non-fatal). -
Onboarding doc-harness — Stage 1 (no payments):
completed-cleanthis session, committed + pushed.licensing-service-startos/onboarding-harness/runs the globalonboarding-testeragent docs-only against the SDK-integration journey (loop converged 5→1→0 stumbles over 3 runs). Doc fixes shipped tokeysat-docs(integrate/agent/wire-format) + the servedopenapi.rsspec; the publishable walkthrough is harvested toagent.html#worked-example. Theopenapi.rsfixes reach the live spec only on the next daemon release; keysat-docs deploys independently. Full detail:onboarding-harness/STAGE1-RESULT.md. Stage 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(envKEYSAT_SANDBOX_MODE, never API-settable; surfaced in/v1/admin/tier); migration 0024scoped_api_keys.extra_scopes; per-key à-la-carte scopes (GRANTABLE_EXTRA_SCOPES=["payment_providers:write"], granted viaextra_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_connectgate (master→any; scoped+payment_providers:write→only ifsandbox_modeAND non-mainnet); BTCPay OAuth wiring (recordscoped_initiatorinbtcpay_authorize_stateatstart_connect, network-check atfinish_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 prefixbc1/tb1/bcrt1, fail-closed to mainnet; the payment-method id isBTC-CHAINvsBTCby version).
- Foundation committed this session (
-
Work queue (next, in order):
- Build gate slices 3–5 (above) — validate the BTCPay address fetch on regtest.
- Confirm
:57is live onimmense-voyage.local(StartOS UI). - Operator data action (master key): grant
unlimited_merchant_profilesto Pro/Patron. - 3 multi-profile UIs + split
audit:read(ROADMAP / Open TODOs).
-
P2/P3 debt (unchanged):
set_product_entitlements_catalogmissingrows_affectedguard; no rate-limit on purchase/redeem (spoofable XFF);422/415plain-text not JSON;slugunvalidated;GET /v1/admin/products405 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 checkclean (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 setsforeign_keys(true)per connection.