Handoff: ship 0.2.0:58 agent-payment-connect; document the connect gate

Current state rewritten to :58-shipped (both onboarding stages completed-clean,
validated separately); payments guide gains the scoped (agent) BTCPay connect
sandbox-gate section (two-gate fail-closed design, migration 0025, GET-callback
status gotcha, regtest validation facts); guide index flags it for the connect
gate + migrations 0024-0025.
This commit is contained in:
Keysat
2026-06-17 10:49:36 -05:00
parent 316d4c961c
commit 47db41a238
2 changed files with 59 additions and 51 deletions
+32 -51
View File
@@ -19,7 +19,7 @@ its guide** — see the index below.
## Subsystem guides (read before editing the area) ## Subsystem guides (read before editing the area)
- Before editing the daemon source, read `docs/guides/daemon-architecture.md`. - 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 editing payment / provider / merchant-profile code, the scoped-connect gate, or migrations 00200022 + 00240025, read `docs/guides/payments.md`.
- Before touching self-license or tier-gating code, read `docs/guides/licensing-tiers.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 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 building, bumping the version, or editing the StartOS wrapper, read `docs/guides/startos-packaging.md`.
@@ -109,59 +109,40 @@ Operator-specific memories at `~/.claude/projects/-Users-macpro-Projects-keysat/
policy entitlements (admin UI), then re-issue the master self-license so it takes policy entitlements (admin UI), then re-issue the master self-license so it takes
effect. effect.
## Current state (2026-06-16) ## Current state (2026-06-17)
- **Live (registry/canonical)**: `registry.keysat.xyz` + `files.keysat.xyz/keysat.s9pk` - **Live / canonical**: **`0.2.0:58`** published — registry + `files.keysat.xyz/keysat.s9pk`,
publish **`0.2.0:57`** — universal multi-arch (x86_64 + aarch64), GitHub release GitHub `v0.2.0-58`, universal multi-arch (x86_64 + aarch64). Live box
`v0.2.0-57`. Migrations 00200023; four SDKs published; `keysat.xyz` + `immense-voyage.local` **confirmed on `:58`** (operator-verified in the StartOS UI). All
`docs.keysat.xyz` deployed. three public sites deployed (`keysat.xyz`, `docs.keysat.xyz`, `registry.keysat.xyz`).
`:57` shipped the **`merchant-onboard`** scoped role (catalog + license self-serve, no Migrations through 0025; four SDKs published.
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-harnessStage 1 (no payments): `completed-clean` this session, committed + pushed.** - **Shipped in `:58` — agent-payment-connect complete (slices 15).** A scoped key with the
`licensing-service-startos/onboarding-harness/` runs the global `onboarding-tester` agent à-la-carte `payment_providers:write` scope connects a BTCPay provider over the API, but
docs-only against the SDK-integration journey (loop converged 5→1→0 stumbles over 3 runs). ONLY on a sandbox daemon (`KEYSAT_SANDBOX_MODE`) for a non-mainnet store;
Doc fixes shipped to `keysat-docs` (integrate/agent/wire-format) + the served `openapi.rs` master/mainnet/production + disconnect stay master-only. The gate fails closed: the store's
spec; the publishable walkthrough is harvested to `agent.html` #worked-example. The network is resolved from its on-chain receive address at callback, anything not provably
`openapi.rs` fixes reach the live spec only on the **next daemon release**; keysat-docs non-mainnet is denied. Migrations 00240025. Three reviewer passes; live gate
deploys independently. Full detail: `onboarding-harness/STAGE1-RESULT.md`. **Stage 2 (regtest `validate-gate.sh` 10/10. Detail: `docs/guides/payments.md`, `plans/agent-payment-connect-scope.md`.
buyer-pays) is gated on agent-payment-connect slices 35 below.**
- **In progress — agent-payment-connect (phase 2)**. Approved spec: - **Onboarding doc-harness — BOTH stages `completed-clean`.** Stage 1 (SDK integration, no
`plans/agent-payment-connect-scope.md`. Lets a scoped key connect a BTCPay provider, but payments) prior session; **Stage 2 (regtest buyer-pays) this session, converged run 1→3.**
ONLY on a sandbox daemon and ONLY for a non-mainnet network — never folded into a role Rig + publishable walkthrough: `onboarding-harness/stage2/STAGE2-RESULT.md`. Doc fixes live
(a key that can repoint settlement is a fund-redirection key). on `keysat-docs` (agent.html/install.html); the served `openapi.rs` BTCPay paths reach the
- **Foundation committed this session (`3afac07`, origin+gitea; NOT version-bumped)** — live spec as of `:58`. The two stages have only been validated **separately**, not as one run.
slices 12 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 35**: `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)**: - **Next (priority order)**:
1. Build gate slices 35 (above) — validate the BTCPay address fetch on regtest. 1. Operator data action (needs the master key): grant `unlimited_merchant_profiles` to
2. Confirm `:57` is live on `immense-voyage.local` (StartOS UI). Pro/Patron on the live master (confirmed-absent details in Open TODOs).
3. Operator data action (master key): grant `unlimited_merchant_profiles` to Pro/Patron. 2. 3 multi-profile UIs + split `audit:read` (ROADMAP / Open TODOs).
4. 3 multi-profile UIs + split `audit:read` (ROADMAP / Open TODOs). 3. Optional: a single combined SDK-integration + buyer-pays onboarding-tester run.
- **P2/P3 debt (unchanged)**: `set_product_entitlements_catalog` missing `rows_affected` - **P2/P3 debt (unchanged, see ROADMAP)**: `set_product_entitlements_catalog` missing
guard; no rate-limit on purchase/redeem (spoofable XFF); `422`/`415` plain-text not JSON; `rows_affected` guard; no rate-limit on purchase/redeem (spoofable XFF); `422`/`415`
`slug` unvalidated; `GET /v1/admin/products` 405 vs OpenAPI; dep advisories (`sqlx`→≥0.8.1, plain-text not JSON; `slug` unvalidated; dep advisories (`sqlx`→≥0.8.1,
`rustls-webpki`→≥0.103.12); no CI / fmt-clippy unenforced; field-naming drift; outbound `rustls-webpki`→≥0.103.12); no CI / fmt-clippy unenforced; outbound webhook SSRF;
webhook SSRF; design-contract conformance (see ROADMAP). design-contract conformance.
- **Tests/build**: `cargo check` + `npm run check` clean (1 intentional deprecation - **Tests/build**: full suite green — lib **18**, api **65**, subscriptions 7, upgrades 9,
warning); full suite green — lib unit **13**, api **59**, subscriptions 7, upgrades 9, worker 3, crosscheck 4, migrations 9 (through **0025**); `cargo check` + `npm run check`
worker 3, crosscheck 4, migrations 9 (through 0024). No new clippy warnings. FK clean (1 intentional deprecation warning); new code clippy-clean.
enforcement confirmed — sqlx pool sets `foreign_keys(true)` per connection.
+27
View File
@@ -108,6 +108,33 @@ response carries no parseable positive amount. Regression tests in `tests/api.rs
- Zaprite is optional, gated by the `zaprite_payments` entitlement; recurring - Zaprite is optional, gated by the `zaprite_payments` entitlement; recurring
auto-charge works via `charge_order_with_profile`. auto-charge works via `charge_order_with_profile`.
### Scoped (agent) BTCPay connect — the sandbox gate
Shipped `:58` (spec: `plans/agent-payment-connect-scope.md`). Provider connect is
master-only EXCEPT: a scoped key carrying the à-la-carte `payment_providers:write`
scope (in NO role, granted per-key via `extra_scopes`) may connect a BTCPay provider
**iff the daemon is in sandbox mode AND the target store is non-mainnet**. Disconnect,
mainnet, and any production daemon stay master-only — a key that can repoint settlement
is a fund-redirection key. Two gates, both fail-closed:
- **Outer (`api_keys.rs::require_provider_connect`)**: replaces `require_admin` on
`start_connect`. Master → any. Scoped+`payment_providers:write` → only if
`Config.sandbox_mode` (env `KEYSAT_SANDBOX_MODE`, boot-only, never API-settable).
- **Inner (`btcpay_authorize.rs::finish_connect`)**: the initiator (master vs scoped) is
carried across the OAuth round-trip in `btcpay_authorize_state` (migration 0025:
`scoped_initiator` + `initiator_actor_hash`). For a scoped initiator, the store network
is resolved from its on-chain receive address (`btcpay/network.rs::classify_address_network`
via `client::fetch_onchain_network`) and a mainnet/undetermined result is refused **before**
any webhook/persist. Fail-closed: no on-chain wallet, unreachable BTCPay, non-JSON, or
unknown prefix all → mainnet → deny. Scoped connects write an audit row.
Validated on a live regtest BTCPay (`onboarding-harness/stage2/`): the on-chain pmid is
`BTC-CHAIN` (BTCPay 2.x; legacy `BTC` is normalized to it — don't hardcode), the
`wallet/address` endpoint needs `btcpay.store.canmodifystoresettings` (which the daemon's
OAuth already requests), and a regtest address is `bcrt1…`. Gotcha: both callback forms
(GET + POST) must return the error's real HTTP status on a denied connect — the GET handler
uses `AppError::status_code()` so a refusal is a 4xx, not a misleading 200.
## Migrations & gotchas ## Migrations & gotchas
- Migrations 00200022 are **one-way**. 0020 ports the singleton - Migrations 00200022 are **one-way**. 0020 ports the singleton