# Acceptance 0005: record association, guest requester R2, cascade release (plan 0036) Proves an agent that cannot do OAuth can still be a market requester the owner vouches for: alice's VM starts a second requester R2 (own did:plc + PDS in the VM), alice writes a contract-scoped `market.association` naming R2, bidders allow R2's RFP via that record, R2 rents a nested VM, and releasing alice's contract cascades: R2 releases first, every token in the chain is revoked, the association record is deleted. - Plan [0036](../plans/0036-record-association.yaml) item V1 (A4-A8) - Last pass: rc128 cascade 2026-10-30 20:34Z (atirc-dev confirmed server-side); rc127 V1 all 3 tokens revoked ```mermaid flowchart LR alice[alice requester
CLI or page] -- RFP / accept --> b1[bidder] b1 --> vm1[alice VM
guest agent] vm1 -- guest.requesterDid --> alice alice -- market.association
contract-scoped, expiresAt --> repoA[(alice repo)] vm1 --> r2[R2 requester
own did:plc + PDS] r2 -- RFP allowed via association --> b2[bidder] b2 --> vm2[R2 nested VM] alice -- mint r2-RKEY.alice --> atirc((atirc)) r2 -- mint vm-RKEY.r2-RKEY.alice --> atirc ``` ## Preconditions | Item | Need | | --- | --- | | Accounts | alice (authority) signed in by QR; R2 is created inside the VM, nothing to prepare | | Vouches | bob + john vouch alice; alice vouches bob + john (R2 inherits trust only through the association) | | Versions | **all** bidders on the same rc (>= rc128): a bidder that ignores `guestTools` can win and the guest has no `socialweb-computer` | | Scopes | requester `guest.secretsUpdated` c/u, `market.associationRequest` c/u/d; bidder `guest.requesterDid` c (re-sign-in after these were added) | | atirc | child mint below the r2 token (any depth), lease server | | Watchers | host watchers (both VMs may land on different hosts), atirc-dev for revocations | ## Steps ```bash SWC=socialweb-computer; R=~/.tmp/acc36; mkdir -p "$R"; chmod 700 "$R" NAME=cli-v1-$(date +%s) # 1. outer VM: agent + socialweb-computer in the guest + guest association slot $SWC run request --state-dir "$R" --name "$NAME" --hold --guest-tools socialweb-computer \ --associate guest --agent -- --atproto-oauth-qr > "$R/$NAME-request.log" 2>&1 & # sign alice in (pendingAction url), wait Held + requester_atirc_minted # 2. zero-command R2: guest.r2Start -> socialweb-r2.service in the guest $SWC run request r2 "$NAME" --state-dir "$R" --r2-name r2-v1 --agent --hold # 3. optional: VM agent for the outer VM $SWC run request agent "$NAME" --state-dir "$R" # 4. wait: R2 Held on its nested VM, grandchild nick vm-.r2-.aliceoa joins # 5. release ONLY the outer contract: cascade must release R2 first $SWC run request release "$NAME" --state-dir "$R" ``` Page: Request VM with Associate DID / R2 box (`guestTools`, `fromGuest`, `secretsRef`), then **Start R2**. Manual fallback in the guest (debug only): `socialweb-computer run request --guest-requester ...` as the agent user (never root). ## Expected observations Outer requester (alice) log: | msg | meaning | | --- | --- | | `requester_guest_requester_reported` / `requester_guest_requester` | R2 DID arrived via `guest.requesterDid` | | proof `at:///...market.associationRequest/request` verified | possession proof of the R2 repo | | `requester_association_written` | `at:///com.publicdomainrelay.temp.market.association/requester_associate--` | | `requester_atirc_minted` subhandle `r2-.aliceoa.bsky.social` | child token into `/etc/socialweb-computer/r2-atirc.env` | | `requester_secrets_updated_sent` | `guest.secretsUpdated` delivered | | `requester_guest_requester_release` ... `r2Released` | cascade on outer release | | `requester_atirc_revoked` x2 (r2 + VM agent), `release_revoke left=0` | grandchild covered by suffix | Guest / R2: `socialweb-r2.service: active` (started by `guest.r2Start`), `association ref arrived`, identity Ready, R2 RFP, Held, in-guest `socialweb-r2-release ok`. Timings (V1 rc125): R2 DID reported 17:15:48 -> Verified 17:15:52 -> hat-7 bid on R2 RFP 17:16:09 -> R2 joined channel 17:16:38. Cascade revoke ~32 s after outer release. ## Pass criteria - Bidder policy allows R2's RFP **only** via the association (forged self-claim by R2 denied; a DID not named by alice denied) - R2 rents a nested VM, grandchild agent joins (3 levels: alice -> r2 -> vm) - Outer-only release: R2 contract Released first, outer Released, exit 0 - `revocations.ndjson` holds r2, VM agent (grandchild by suffix) before / at termination - Association record gone after release (`getRecord` -> RecordNotFound, checked 3 times) - Both hosts empty (watchers) ## Cleanup - If the cascade failed: release R2 contract first (from the guest or bidder-side delete), then outer - Orphan R2 VM: [0007](0007-bidder-termination.md) on its host; token dies by lease - Unrevoked token: atirc-dev server-side revoke by subhandle (suffix covers children) - R2 runs **no** `atirc sub` (owner rule); kill any test sub ## Known pitfalls | Symptom | Cause / fix | | --- | --- | | guest has no `socialweb-computer` | `-- --guest-tools` went to `spec.options` (bare `--agent` swallowed the next arg, pre rc126); or a pre-rc125 bidder won | | R2 `run request` refuses root, bundle root-only | rc126 mirrors into `/run/socialweb-r2`, runs as the agent user | | `run request r2` rejected by CLI | missing from the capabilities registry (rc127); fixed rc128 | | outer release leaves R2 VM running | no cascade before A8 (rc127) | | R2 token alive after release | revoke ran in a goroutine after Released and the CLI stopped kcp (rc126); fixed rc127 | | cross-contract clobber / impersonation via authority vouch | review HIGHs fixed: contract-scoped rkey, nonce proof record in the R2 repo | | denied accept, no receipt, guest stays | bounded denied-accept release (A8) |