Skip to content

feat: materialize agent_owner_pubkey on NIP-OA auth - #491

Merged
tlongwell-block merged 1 commit into
mainfrom
feat/nip-oa-agent-owner-backfill
May 6, 2026
Merged

feat: materialize agent_owner_pubkey on NIP-OA auth#491
tlongwell-block merged 1 commit into
mainfrom
feat/nip-oa-agent-owner-backfill

Conversation

@tlongwell-block

Copy link
Copy Markdown
Collaborator

Summary

Follow-up to #490. When an agent authenticates via NIP-OA, the relay now materializes the agent→owner relationship so cross-connection features (observer frames, channel add/remove policy) work for BYO agents.

What it does

  1. Session-scoped fast pathAuthContext.agent_owner_pubkey is set on successful NIP-OA auth, enabling zero-DB-lookup observer frame authorization for the current connection.

  2. Idempotent DB backfill — On first NIP-OA auth, writes users.agent_owner_pubkey so cross-connection features (owner managing agent from a separate session) work without desktop provisioning.

Safety properties

Concern How it's handled
Does this bypass NIP-OA revocation? No. agent_owner_pubkey is not relay_members. Agent still needs valid NIP-OA every connection.
First-write-wins conflict set_agent_owner uses WHERE agent_owner_pubkey IS NULL. If already owned by someone else, session fast-path only activates after is_agent_owner DB confirmation.
Stale observer cache Pre-warms observer_owner_cache on successful backfill.
BYO agent with no users row ensure_user called for both agent and owner before set_agent_owner.
FK constraint (owner must exist) ensure_user(owner) handles this.
Backfill failure Non-fatal — auth succeeds, just no fast-path or DB record.
Desktop-provisioned agents set_agent_owner is a no-op (column already set). Zero behavioral change.

Changes

  • sprout-auth/lib.rs: AuthContext gains agent_owner_pubkey: Option<PublicKey>
  • sprout-relay/api/mod.rs: enforce_relay_membership returns Option<PublicKey> (owner on NIP-OA success)
  • sprout-relay/handlers/auth.rs: captures owner, validates against DB, backfills, stashes on context
  • sprout-relay/handlers/event.rs: observer frame fast-path from AuthContext

Testing

  • cargo build --workspace
  • cargo clippy --workspace --all-targets -- -D warnings
  • cargo test -p sprout-relay -p sprout-auth
  • E2E verified: BYO agent connects via NIP-OA → agent_owner_pubkey correctly backfilled in DB

… DB backfill)

When an agent authenticates via NIP-OA, the relay now:

1. Stashes the verified owner on AuthContext.agent_owner_pubkey (session-scoped)
   - Only set if DB confirms the relationship (first-write-wins)
   - Observer frames use this as a fast path (skip DB/cache lookup)

2. Idempotently backfills users.agent_owner_pubkey in the DB
   - Creates user rows if needed (ensure_user for both agent and owner)
   - Uses existing set_agent_owner (WHERE agent_owner_pubkey IS NULL)
   - First-write-wins: if already owned by someone else, verifies match
   - Pre-warms observer_owner_cache on successful backfill

This enables cross-connection features (observer frames, channel policy)
to work for BYO agents that were never provisioned through the desktop.

Security properties:
- Does NOT add agent to relay_members (NIP-OA still required every connect)
- Session fast-path only activates if DB confirms the owner relationship
- Conflicting owner (agent already owned by someone else) is handled safely
- Backfill failure is non-fatal (auth succeeds, just no fast-path/backfill)

Changes:
- sprout-auth: AuthContext gains agent_owner_pubkey field
- api/mod.rs: enforce_relay_membership returns Option<PublicKey>
- handlers/auth.rs: captures owner, backfills DB, stashes on context
- handlers/event.rs: observer frame fast-path from AuthContext
@tlongwell-block
tlongwell-block force-pushed the feat/nip-oa-agent-owner-backfill branch from 5a32918 to 50942e7 Compare May 6, 2026 15:59
@tlongwell-block
tlongwell-block enabled auto-merge (squash) May 6, 2026 16:05
@tlongwell-block
tlongwell-block merged commit 1f1f874 into main May 6, 2026
14 checks passed
@tlongwell-block
tlongwell-block deleted the feat/nip-oa-agent-owner-backfill branch May 6, 2026 16:08
ALLiDoizCode pushed a commit to toon-protocol/buzz that referenced this pull request Aug 4, 2026
…hannel (buzz#74) (#107)

Task: #74, part of the agent-fleet-money epic
(toon-meta#261 decision "provisioning as one action"). Unblocked by
toon-client#491 (plain sendTransfer, merged into @toon-protocol/client
0.26.0) and buzz#79 (account-index registry, merged). Before this, making a
managed agent able to pay on TOON needed manual CLI work outside the app;
this wires the desktop app itself to derive the agent's payment address,
fund it from the owner's wallet, and open its channel, with progress shown
inline in the create-agent flow.

Key decisions:
- Bumped `@toon-protocol/client` to ^0.26.0 for `sendTransfer` (issue block#491)
  and `getRoutePrice`-based quoting — 0.25.1 had no transfer primitive at
  all, only channel-collateral locking.
- Step derivation (`agentProvisioningState.ts`) mirrors
  `toonOnboardingState.ts`'s ADR exactly: key/fund/channel status comes from
  live reads (Rust-assigned account index, the agent's own derived-address
  balances), never a stored counter, so reopening the flow for a partly
  provisioned agent resumes from reality. The channel-open step is the one
  persisted flag (`agentProvisioningStore.ts`, keyed per agent pubkey) —
  same tradeoff the onboarding wizard already makes for its own channel
  step, since there's no free on-chain probe for "does this address already
  have a channel."
- Initial allowance (`agentProvisioningAllowance.ts`) prefers a measured
  burn rate (reusing `agentNetworkFlow.ts`'s `NetworkFlowRead` shape) and
  falls back to `quotedPrice × FALLBACK_WRITES_PER_DAY × FALLBACK_RUNWAY_DAYS`
  for a brand-new agent's inherent lack of history — every creation-time
  caller takes this fallback today, since the per-agent burn-rate feed
  (`agentNetworkFlow.ts`'s own documented blocker, buzz#86) isn't wired yet.
- Funding (`provisionAgent.ts`) relies on `sendTransfer`'s own built-in
  balance-delta confirmation (throws `TransferNotDeliveredError` rather than
  resolving on a send that landed but delivered nothing — the exact devnet
  faucet failure mode connector#691 documents) instead of re-verifying
  itself. The two legs (gas, USDC) run independently via `Promise.all` and
  report as a tagged result rather than throwing, so a failed gas leg never
  loses a successful USDC leg — this is what makes "token landed, gas
  didn't" a legitimate resumable state instead of a hard failure.
- Channel-open uses the actual funded balance on hand as the collateral
  amount (not a re-quoted estimate), so it's correct even resumed in a
  later session with no memory of what the fund step originally computed.
- Extracted `buildToonClientOptions` out of `toonPaidWriter.ts`'s
  `createToonClient` so the writer's client and both provisioning clients
  (owner-scoped for `sendTransfer`, agent-scoped for `openChannel`) share
  one bootstrap rather than duplicating it.
- Rust: `account_index.rs` gains a pure read-only `find_account_index`
  (buzz#79's `assign_account_index` mutates/creates; provisioning only
  needs to read what `create_managed_agent` already assigned), exposed via
  a new `commands/agent_provisioning.rs` module rather than added to
  `agents.rs`/`tauri.ts`, both already at their file-size ratchet ceiling.
- UI: `AgentProvisioningDialog` (StepProgress, mirrors `ToonOnboardingGate`)
  is handed off from `RequestedAgentCreateDialogs` once `SecretRevealDialog`
  closes for a successfully created agent — sequential, not simultaneous,
  so only one dialog is ever open at a time.

Files changed: desktop/package.json, pnpm-lock.yaml, pnpm-workspace.yaml,
desktop/src-tauri/src/{commands/agent_provisioning.rs (new),commands/mod.rs,
lib.rs,managed_agents/{account_index.rs,mod.rs}},
desktop/src/features/agents/{lib/agentProvisioningState.ts (new, +test),
lib/agentProvisioningAllowance.ts (new, +test),
lib/agentProvisioningStore.ts (new, +test),lib/provisionAgent.ts (new, +test),
useAgentProvisioning.ts (new),
ui/{AgentProvisioningDialog.tsx (new),RequestedAgentCreateDialogs.tsx}},
desktop/src/shared/api/{tauriAgentProvisioning.ts (new),toonPaidWriter.ts}.

Verified: fmt-check, desktop-tauri-fmt-check, clippy (workspace, -D
warnings), test-unit (864 tests), desktop-check, desktop-test (4291
passed), desktop-build, web-check, web-build — all green.

Blockers/notes for next iteration: desktop/src-tauri cannot be compiled or
unit-tested in this sandbox (no GTK/WebKit/sidecar stubs), so
`account_index.rs`'s new `find_account_index` and its tests are verified by
`cargo fmt --check` plus manual review, not `cargo check`/`clippy`/`test` —
upstream ci.yml's desktop-tauri-check/test job is the first real compile.
`solana`/`mina` are typed as valid `TransferChain`/faucet chains in
provisionAgent.ts but the fund step always calls with `chain: "evm"`
(TOON_DEVNET_DEFAULTS.chain is evm:84532 only) — fine today, would need a
config-driven chain pick if the devnet's default settlement chain changes.
The "one action" is UX-sequential (fund button, then open-channel button,
both inline in one dialog with derived progress) rather than one click that
silently chains both network calls — matches the onboarding wizard's own
established "every network step is a button press" convention rather than
adding a new auto-chaining pattern.

Signed-off-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant