Files
keysat-root/AGENTS.md
T
Keysat 9c3ddc01e7 Fix doc drift; document no-enforce-mode and universal publish
Corrections surfaced by doc-auditor + start9-spec-checker:
- testing.md: api suite 47 -> 54
- payments.md: FK enforcement confirmed at db/mod.rs:29
- startos-packaging.md: publish.sh now ships a universal s9pk
- licensing-tiers.md: record enforce-mode retirement and Creator caps
Refresh Current state for the StartOS submission-blocker work.
2026-06-13 06:40:06 -05:00

12 KiB
Raw Blame History

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 00200022, 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 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
keysat-design-system/             design tokens / brand assets
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-licensing-Licensing/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

  • Verify the universal multi-arch publish end-to-end: publish.sh now runs make universal (one keysat.s9pk, both arches) instead of x86-only; the first real publish must confirm the registry index lists both arches. riscv target unverified (not in the manifest, so make universal excludes it).
  • 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.
  • Build the admin SPA "API keys" management panel (create w/ role picker, list, revoke) — backend is wired; UI deferred to a design-focused session.

Current state (2026-06-13)

  • Live: server immense-voyage.local runs daemon 0.2.0:54 (migrations 00200022). Registry registry.keysat.xyz publishes :54; four SDKs published; keysat.xyz + docs.keysat.xyz deployed. Prod is still :54 — the prior session's two P1 fixes are committed to source but NOT yet built/installed/ published. Next release builds :55.

  • This session (UNCOMMITTED across 4 repos; docs + StartOS packaging, no daemon logic changed) — doc-auditor + start9-spec-checker + 2 reviewer passes, all approved/no blockers; tsc + bash -n clean. By repo:

    • root (Gitea): testing.md api 47→54; payments.md FK confirmed (db/mod.rs:29); startos-packaging.md + this block updated for universal publish; licensing-tiers.md gained the "no enforce mode / Creator caps" note.
    • keysat-docs (Gitea): integrate.html phantom GET /v1/licenses/{id}/status → real POST /v1/validate w/ key. Needs deploy-sites.sh docs to go live.
    • keysat daemon (GitHub+gitea): new instructions.md (Start9-required); manifest packageRepo + docsUrls[1] dead-link fixes; v0.2.0.ts stale-header removed; activateLicense.ts/showCredentials.ts enforce-mode drift cleaned (enforce retired — self_license.rs:15).
    • go SDK (Gitea): README v0.1→v0.2.
    • operator-local ~/.keysat/publish.sh (gitignored, NOT committed): x86-only → make universal (one keysat.s9pk, both arches). Pending a verification build. All 4 StartOS submission blockers now addressed. Left for operator decision: integrate.html BTCPay-only prereq/refund copy (no Zaprite mention). Commit = 4 per-repo commits (root, keysat-docs, go SDK are Gitea-only; daemon is GitHub+gitea — also git push gitea main).
  • :52/:53 = multi-provider/merchant-profile model: data model + backend resolution shipped and audited sound; resolution/CRUD query surface has tests. Both :54 P0s (provider-injection test seam; Zaprite webhook-forgery re-confirm) remain fixed; live purchase + settle paths sound.

  • Unshipped source work (awaiting :55) — two P1s from the prior session:

    1. Settle-amount tripwire. get_invoice_status now returns ProviderInvoiceSnapshot { status, amount }; audit_settle_amount (shared by webhook + reconcile issue paths) WARNs + writes an invoice.amount_mismatch audit row on drift, then issues anyway (advisory, not a gate — a hard gate would fight BTCPay payment tolerance). SAT-only: skips non-SAT (fiat sub renewals) and None. Reviewed (caught + fixed a fiat-renewal false-positive). See docs/guides/payments.md.
    2. Scoped API keys wired. 58 admin endpoints migrated require_adminrequire_scope; 12 sensitive ones stay master-only (issuer key, provider connect/disconnect, set-password, api-key CRUD, db-info, operator-name, per-license tier change). require_scope re-exported from api::admin. Role boundary tests added. Boundary documented in api/api_keys.rs module doc.
  • GAP — multi-profile still non-functional end-to-end: nothing writes products.merchant_profile_id (INSERT in create_product_with_currency omits it; update_product_with_currency has no field; Product in models.rs lacks it). Resolver fully supports it; only the product→profile write path is missing. Gating piece for multi-profile.

  • Work queue (next, in order):

    1. product→merchant-profile picker (the GAP — add merchant_profile_id to Product + repo.rs SELECT; set_product_merchant_profile writer mirroring set_product_entitlements_catalog; field on CreateProductReq/ UpdateProductReq applied post-write; profile <select> from GET /v1/admin/merchant-profiles, shown only when >1 profile; no migration).
    2. 3 other deferred UIs (rail picker, per-profile SMTP, rail-pref editor); unlimited_merchant_profiles on master Pro/Patron policies.
    3. Deferred this session (now in Open TODOs): split audit:read out of the blanket :read scope; build the admin "API keys" management SPA panel.
  • Known debt (P2 — schedule, not urgent): no rate-limit on /v1/purchase + /v1/redeem; rate-limit bucket keys on spoofable X-Forwarded-For (bypass conditional on whether the StartOS proxy rewrites XFF — unverified); 422/415 errors return plain-text not JSON (breaks SDK JSON.parse); product slug has no validation (empty/300-char/meta chars stored); GET /v1/admin/products returns 405 though OpenAPI documents it; dep advisories (sqlx→≥0.8.1 RUSTSEC-2024-0363, rustls-webpki→≥0.103.12); 4 StartOS submission blockers (spec-checker-verified) all addressed and staged, pre-build — manifest packageRepo (…/keysat-startos…/keysat) and docsUrls[1] (docs/INTEGRATION.mdKEYSAT_INTEGRATION.md, the real repo-root file) fixed; instructions.md written (reviewer + doc-auditor signed off); aarch64 now shipped via publish.sh make universal (one s9pk, both arches — pending a verification build); no CI + fmt/clippy/prettier unenforced.

  • Deferred (P3+ — bulk or later decision): /v1/purchase 400 vs /v1/btcpay/webhook 503 for the same no-provider cause; undocumented required kind on discount-codes; field-naming drift (license_id/id, machines key vs license_key, redeem/purchase product vs validate product_slug); migration self-heal _sqlx_migrations allowlist foot-gun; 2 KB unauth Zaprite payload WARN-log; outbound-webhook SSRF (operator-only); re-register the master Zaprite webhook at the path-keyed URL; registry icon non-render (known platform limit); optional fmt/prettier standalone commit.

  • Tests/build: cargo check clean (1 intentional deprecation warning); full suite green — api 54 (incl. new settle-tripwire + scoped-key role-boundary tests), subscriptions 7, upgrades 9, worker 3, crosscheck 4, migrations 9. No new clippy warnings. FK enforcement confirmed — sqlx pool sets foreign_keys(true) per connection (db/mod.rs). CI/fmt status is in Known debt.