576213b0ce
Agent-onboarding doc for the workspace: stack, build/test/run commands, directory layout, conventions, and always/never gotchas, plus a Current state section. CLAUDE.md symlinks to AGENTS.md so Claude Code auto-loads it. Longer-term backlog lives in ROADMAP.md.
9.8 KiB
9.8 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.
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 and paths reflect that, not Docker).
- Payment providers: BTCPay Server (required dep); Zaprite (optional, gated by
zaprite_paymentsentitlement).
Build / test / run
From licensing-service-startos/:
make x86 # build keysat_x86_64.s9pk
make arm # build keysat_aarch64.s9pk
make universal # single multi-arch package
make install # install to StartOS; auth via the developer key at ~/.startos/developer.key.pem (private — never commit/share)
make clean
npm run check # tsc --noEmit on the wrapper
npm run prettier # format startos/*.ts
From licensing-service-startos/licensing-service/:
cargo check # type-check daemon
cargo build --release # release build
cargo test # all suites
cargo test --test api # one suite (suites: api crosscheck migrations subscriptions upgrades worker)
cargo test <name> # single test by name pattern
cargo fmt --check # TODO: confirm CI gate
Operator-local scripts (live in ~/.keysat/, gitignored, not in this repo):
~/.keysat/publish.sh # version-gate → make x86 → FileBrowser upload → registry register → GitHub release mirror
~/.keysat/deploy-sites.sh landing docs # push static sites to FileBrowser; accepts: landing | docs | registry-landing
Credentials for those scripts: ~/.keysat/filebrowser.env (chmod 600), env vars KEYSAT_FB_USER, KEYSAT_FB_PASS. Daemon env vars at runtime: KEYSAT_ADMIN_API_KEY, KEYSAT_LICENSE, KEYSAT_OPERATOR_NAME, KEYSAT_PUBLIC_URL, BTCPAY_URL, BTCPAY_BROWSER_URL, BTCPAY_PUBLIC_URL.
Directory layout
licensing-service-startos/ daemon + StartOS wrapper (s9pk package source)
licensing-service/src/ Rust daemon
licensing-service/migrations/ SQLite migrations (numbered, additive)
licensing-service/web/index.html embedded admin SPA (rust-embed)
licensing-service/tests/ integration suites
licensing-service/tests/crosscheck/ wire-format fixtures the four SDKs cross-verify
startos/ wrapper TS (manifest/, versions/, actions/, interfaces.ts, backups.ts, init/)
Dockerfile Makefile s9pk.mk build pipeline
keysat-xyz-landing/ keysat.xyz
keysat-docs/ docs.keysat.xyz (incl. shared docs.js)
keysat-registry-landing/ registry.keysat.xyz stub
licensing-client-{rust,ts,python,go}/ the four SDK source repos
activate-license-template/ Tauri desktop template for license activation
keysat-design-system/ design tokens / brand assets
plans/ historical plan files (gitignored or archived — TODO confirm)
tests/ TODO: confirm vs licensing-service/tests/
Conventions
- Daemon licensed
LicenseRef-Keysat-1.0(custom, source-available); SDKs MIT. - Commits in imperative mood, no AI co-authorship trailers, body only when the "why" isn't obvious.
- Direct push to
main+ run~/.keysat/publish.shis the authorized release flow until launch. - Admin UI pills: navy-filled for selected/on; cream-outlined for off; opacity for muted; gold reserved for marketing accents only (most-popular badge, launch-special ribbon).
- No em-dashes in user-facing copy on the website or docs. Exceptions: decorative ornaments and verbatim third-party UI labels (e.g. BTCPay's "Network — mDNS"). Comments and docstrings inside source code are fine to keep em-dashes.
- Pricing/tier copy on the docs site is a snapshot only. The canonical sources are
keysat.xyz#tiers(live tier cards rendered from the master Keysat) andGET licensing.keysat.xyz/v1/products/keysat/policies.
Always
- Bump version + add changelog entry before building. Edit
startos/versions/v0.2.0.ts— increment theversion: '0.2.0:N'field and prepend aROUTINE_NOTES[0]entry — before runningmake x86orpublish.sh. Start9 0.4.x silently no-ops an install of an un-bumped package. cargo checkfromlicensing-service/before bumping the version, so the build doesn't fail downstream.- Use the SDK's StartOS dependency discovery for BTCPay, not hardcoded hostnames. Pattern is in
licensing-service-startos/startos/main.ts:156–175(sdk.serviceInterface.getAll(effects, { packageId: 'btcpayserver' })). - For tier gates, expect LIVE entitlements from
licenses.entitlements(refreshed hourly byrefresh_self_tier_from_dbinsrc/license_self.rs), not the entitlements baked into the signed payload at issue time. - BTCPay Connect: use the one-click authorize flow.
POST /v1/admin/btcpay/connectreturns{ authorize_url }; the operator opens it, BTCPay callbacks/v1/btcpay/authorize/callback, and the daemon auto-detects store + registers webhook. The admin UI'sopenBtcpayConnectModalinweb/index.htmlis the reference flow. Don't ask the operator to paste an API key + store id by hand. - Sign all release commits as Keysat (Grant), not Claude. Git user is
Keysatper repo config.
Never
- Don't add AI co-authorship to commits or PRs. No "Co-Authored-By", no "Generated with Claude Code".
- Don't push
--no-verifyor bypass pre-commit hooks unless explicitly authorized. - Don't rewrite user-facing copy outside the explicit scope of a request.
- Don't silently expand entitlements in
tier::current()(e.g., "patron implies pro"). Tried in0.2.0:41, reverted in0.2.0:42. The right fix when an operator is stuck on an old-scheme self-license is: re-issue + run the StartOS "Activate Keysat license" action — the new key overwrites/data/keysat-license.txtandself_tierrefreshes without a daemon restart. - Don't assume
start-cli registry info set-iconis sufficient for the registry icon to render in the StartOS marketplace UI. The icon round-trips throughregistry infocorrectly at 96×96 and 256×256 PNG, but the marketplace header may still show the storefront fallback. The operator may have to paste the data URL into the local "Configure Registry" modal manually. Confirm visually before claiming "done." - Don't commit built artifacts (
*.s9pk,keysat-*.s9pk,javascript/) — they're build outputs. - Don't commit secrets. Reference env-var names; real values live in
~/.keysat/filebrowser.envand/data/keysat-license.txtoutside the repo. - Don't claim multi-arch when only x86_64 ships. Manifest currently declares
['x86_64', 'aarch64']butpublish.shonly builds + uploads x86_64. See TODO.
Memory references
Persistent operator-specific memories live at ~/.claude/projects/-Users-macpro-Projects-licensing-Licensing/memory/. Worth scanning before a major change:
keysat_release_workflow.md— push-to-main + publish.sh is authorized.no_unauthorized_copy_changes.md— never rewrite copy outside scope.keysat_admin_ui_pill_convention.md— navy/cream/gold pill rules.startos_lxc.md— StartOS 0.4 runtime model.startos_registry_icon_unrenderable.md— known issue with custom registry icons.keysat_open_threads.md— parked follow-ups from prior sessions.
TODO (unverified or pending — flag if you touch)
cargo fmt --check/clippy— confirm whether either gates CI.- Top-level
tests/directory — confirm whether it's used or stale vslicensing-service-startos/licensing-service/tests/. - Manifest
license: 'LicenseRef-Proprietary'(instartos/manifest/index.ts) vs the LICENSE file'sLicenseRef-Keysat-1.0header — align before submitting to any Start9-curated registry. - Publish pipeline only builds x86_64; the Makefile targets
armandriscvexist and work. Either tighten the manifest'sarchclaim or extendpublish.shto build + upload all declared arches. - StartOS Community Registry submission criteria — Start9 docs say "objective technical criteria" exist but don't publish the checklist. Reach out to Start9 directly when ready.
Current state
- Live: daemon
0.2.0:45onregistry.keysat.xyz(named "Keysat"); four SDKs published;keysat.xyz+docs.keysat.xyzdeployed. BTCPay one-click connect and Zaprite recurring auto-charge both work. - In progress: source is at
0.2.0:52(multi-merchant-profile / multi-provider payment model), committed, tree clean, not yet published (gap:46–:52unpublished). Migrations 0020–0022 are one-way; 0020 ports the singleton provider config into per-profile tables. - Decided, not built:
:52ships the data model but defers four UIs — buy-page rail picker, product-edit merchant-profile picker, per-profile SMTP override form, rail-preference editing. Master Keysat's Pro/Patron policies still needunlimited_merchant_profilesadded (data action on keysat.xyz admin, no code). - Known issues: custom registry icon round-trips through
start-cli registry infobut the StartOS marketplace shows the storefront fallback (likely needs the data URL pasted into the local Configure Registry modal). After:52installs, the master Zaprite webhook still points at the legacy URL — works via back-compat, needs re-register for per-provider isolation. - Next steps: (1) publish
:52via~/.keysat/publish.sh; (2) addunlimited_merchant_profilesto master Pro/Patron policies; (3) re-register master Zaprite webhook at the path-keyed URL; (4) build the buy-page rail picker + product-edit profile picker; (5) resolve the registry-icon render issue.