~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
286 lines · 18.2 KB Raw History

guest-events: events under a receipt

  • after the receipt (market.md), the bidder, the requester and the guest talk only through market.event records and two guest HTTP paths
  • steps: S7 boot and vm events, S8 guest events and atirc, S9 release
  • one row per lexicon: lexicons-market.md
  • constants: internal/atproto/market/nsid.go, internal/atproto/market/guest.go; guest side: internal/guestagent (binary cmd/socialweb-guest-agent); guest HTTP plane: internal/services/guestcontrolplane, internal/guestidentity
  • names drop the prefix com.publicdomainrelay.temp. (guest.publicKey = com.publicdomainrelay.temp.compute.events.guest.publicKey)

The Envelope

  • every event is two signed records in the sender's repo: the payload (compute.events.vm.* or compute.events.guest.*) and market.event {receipt, payload, createdAt, signatures}
  • receipt = strongRef to the bidder's market.receipt; payload = strongRef to the payload record in the same repo as the event
  • the receiver reads the payload through the strongRef, checks its CID, its author and both signatures (applyGuestEvent in internal/market/bidder/guestevents.go)
  • delivery: market.submitEvent to the other party, and the relay / Jetstream streams as fallback (market.md)
  • the guest holds no repo key. the bidder signs and writes every guest-originated event in its own repo

Who Sends What

payloadoriginatorwritten inconsumerfile of the consumer
vm.onNetworkguest (iroh ticket and https tunnel URL from the guest-onnetwork script, webrtc fingerprint from the guest agent) or bidder (sandbox)bidder reporequester: status.contract.vmAddress, status.contract.networksinternal/market/requester/runner.go waitGuest
vm.hostKeysbidder (the host key the guest bound at its workload identity login)bidder reporequester: status.contract.hostKeyssame
vm.sandboxbidder (deno sandbox ended)bidder reporequester: status.contract.sandboxinternal/market/requester/events.go
vm.terminatedbidderbidder reporequester: Releasedinternal/market/requester/market.go FindTermination
vm.deleterequesterrequester repobidder: releaseinternal/market/bidder/adapters.go applyRelease
guest.publicKeyeither sidesender repothe other sideguest: internal/guestagent/subscribe.go; requester: inbound ContractEvent
guest.signalWebRTCeither sidesender repothe other sidesame
guest.secretsrequester onlyrequester repoguestinternal/guestagent/secrets.go
guest.secretsRequestedguest onlybidder reporequester: seals the bundleinternal/market/requester/guestsecrets.go
guest.secretsAppliedguest onlybidder reporequester: condition SecretsAppliedinternal/market/requester/events.go
guest.secretsUpdatedrequester onlyrequester repoguest: fetches the bundle againinternal/guestagent/subscribe.go
guest.agentStartrequester onlyrequester repoguest: systemctl start --no-block atirc-agent.serviceinternal/guestagent/agentstart.go
guest.requesterDidguest onlybidder reporequester: status.contract.guestRequesterinternal/market/requester/association.go
guest.r2Startrequester onlyrequester repoguest: starts socialweb-r2.serviceinternal/guestagent/association.go
guest.r2Releaserequester onlyrequester repoguest: runs socialweb-r2-release.servicesame
guest.r2Releasedguest onlybidder reporequester: status.contract.r2Releaseinternal/market/requester/guestrequester.go
vm.started, vm.registerIdentitydefined, no Go writernonenoneinternal/atproto/market/nsid.go constants only
  • guest-originated set: market.IsGuestOriginatedEventNSID (publicKey, signalWebRTC, secretsApplied, secretsRequested, requesterDid, r2Released). the guest control plane refuses any other kind (guestEvent in internal/guestidentity/service.go)
  • requester-originated set: market.IsRequesterGuestEventNSID (publicKey, signalWebRTC, secrets, agentStart, secretsUpdated, r2Start, r2Release). the bidder refuses any other kind from a requester (applyGuestEvent)
  • kinds the requester's submitEvent takes: isGuestEventNSID in internal/market/requester/events.go
  • direction of publicKey and signalWebRTC comes from the author: requester-authored ones go to the guest, bidder-authored ones carry what the guest said

Guest To Requester

sequenceDiagram
  autonumber
  participant G as guest
  participant CP as GuestControlPlane (bidder)
  participant B as Bidder
  participant BP as bidder repo
  participant R as Requester

  G->>CP: POST /v1/events acceptUri acceptCid record, Bearer workload token
  CP->>CP: verify token, accept is this guest's, kind is guest-originated
  CP->>B: vm.onNetwork onto ComputeVM status, other kinds PublishGuestEvent
  B->>B: StateSync guest-<hash> or report-<hash>, key per kind
  B->>BP: payload + market.event at EventRKeys(key), both signed
  B->>R: submitEvent (accept.submitEvent) or the stream carries it
  R->>BP: getRecord payload by strongRef
  R->>R: status.contract or inbound ContractEvent
  • the POST body: {acceptUri, acceptCid, record}; the record has $type = the payload NSID and no signature (internal/guestagent/events.go)
  • the bearer is the Authorization header only (Guest.Events in internal/guestidentity/service.go)
  • answers: 200 {ok, name}; 401 AuthenticationRequired / InvalidToken; 400 InvalidRequest; 404 UnknownGuest / UnknownAccept; 503 Unavailable
  • the bidder keys: reportSyncKey and guestEventSyncKey in internal/market/bidder/syncs.go; the rkeys: market.md
  • the bidder never opens a sealed body; it routes by NSID prefix

Requester To Guest

sequenceDiagram
  autonumber
  participant C as CLI / browser / requester
  participant R as Requester
  participant RP as requester repo
  participant B as Bidder
  participant Q as per-ComputeVM queue
  participant G as guest agent

  C->>R: ContractEvent Outbound (nsid, tag, kind, key or ciphertext)
  R->>RP: payload at SessionRKey contract-event + market.event at contract-event-envelope
  R->>B: submitEvent
  B->>RP: getRecord payload, verify event + payload signatures
  B->>B: author = accept author, kind is requester-originated
  B->>Q: Append uri cid record
  G->>B: WebSocket subscribeEvents cursor
  B-->>G: frames seq uri cid record, keepalive every 25 s
  G->>G: act on the kind, save cursor
  • outbound ContractEvents (socialweb.computer/v1alpha1, cluster scoped, owned by the ComputeContract): made by the CLI (internal/stack/request/webrtc.go), the browser page, or the requester itself (secrets-<contract>, secrets-updated-<hash>)
  • published by publishContractEvent in internal/market/requester/contractevents.go; field rules in validateGuestFields there
  • inbound ContractEvents (guest publicKey, signalWebRTC) are named InboundContractEventName(eventURI)

compute.guest.subscribeEvents

itemvalue
routeGET /xrpc/com.publicdomainrelay.temp.compute.guest.subscribeEvents on the GuestControlPlane (internal/services/guestcontrolplane/subscribe.go)
authAuthorization: Bearer <workload identity token>; names the ComputeVM (guest.Authenticate)
paramcursor: last seq seen. absent = oldest entry kept
frameJSON text {"$type": "...subscribeEvents#event", seq, uri, cid, record}; record.$type is a compute.events.guest.* NSID
keepalivean empty binary frame every 25 s (SubscribeKeepalive)
errorFutureCursor: the guest agent resets its cursor to 0 and reconnects (internal/guestagent/subscribe.go)
reconnectbackoff 2 s doubling to 30 s

Each Kind

NSID tailkey fieldsnotes
vm.onNetworkkind iroh / https / webrtc / sandbox, address, tagkind absent: iroh without tag, https with tag (market.OnNetworkKind). webrtc address = the guest agent DTLS fingerprint sha-256 AB:CD:...
vm.hostKeyskeys[] {type, publicKey, fingerprint}, observedAtat most 8 keys, an ed25519 key required (internal/guestidentity/guestrecord.go)
vm.sandboxexitCode, timedOut, stdout, stderrcompute.vm.kind: deno only (internal/market/bidder/sandbox.go)
vm.deletereasonrequester writes released
vm.terminatedreason, exitCode, observedAt, requestrequest -> the vm.delete when reason is deleted_on_request
guest.publicKeykind: age-x25519, tag, key (age1...)one per tag per side; anything sealed for a tag goes to the other side's latest key
guest.signalWebRTCkind offer / answer, tag, ciphertextplaintext {sdp, replyTo}; non-trickle ICE, one offer, one answer
guest.secretsciphertextage armored, at most 65536 bytes (MaxGuestSecretsCiphertext); plaintext = the getSecrets JSON array
guest.secretsRequestednoneonly with secretsFallback: Sealed
guest.secretsAppliedvia getSecrets / sealed, count
guest.secretsUpdatednonethe bundle changed (atirc mint, association ref)
guest.agentStartnoneneeds spec.agent (contractevents.go)
guest.requesterDiddiddid:plc only, never the authority's own DID (market.ValidGuestRequesterDID)
guest.r2Startname (DNS label), agent, holdneeds spec.association.fromGuest
guest.r2Releasenone
guest.r2Releasedok, message

Boot: hostKeys And onNetwork

sequenceDiagram
  autonumber
  participant B as Bidder
  participant G as guest
  participant CP as GuestControlPlane
  participant BP as bidder repo
  participant R as Requester

  B->>G: boot user_data (cloudinit modules + setup-guest-binaries)
  G->>CP: workload identity login, binds its ssh host key
  CP->>B: RecordBoundHostKey onto ComputeVM guest report
  G->>G: iroh.service up, writes /run/guest-fqdn
  G->>CP: guest-onnetwork POST /v1/events vm.onNetwork address = iroh ticket
  CP->>B: OnNetwork onto ComputeVM status
  B->>BP: report: vm.onNetwork (key onNetwork:receipt) + market.event
  B->>BP: report: vm.hostKeys (key hostKeys:receipt) + market.event
  B->>R: submitEvent each
  G->>CP: guest agent POST vm.onNetwork kind webrtc per tag (DTLS fingerprint)
  B->>BP: vm.onNetwork tag (key onNetwork:tag:receipt)
  R->>R: VMAddress + HostKeys, Held after secrets
  • the iroh report unit and its script: internal/guestseed/userdata.go ("Report the guest iroh ticket to the market")
  • one report per kind and tag: report in internal/market/bidder/report.go publishes only what ComputeVMPublished does not hold yet
  • Held: iroh address + host keys; with a secretsRef and no sealed fallback, also the getSecrets fetch (market.md)

Sealed Signaling (webrtc tunnels)

  • each side publishes a guest.publicKey (kind age-x25519) per tag
  • the offer is sealed to the guest's latest key for the tag, plaintext {sdp, replyTo}; the answer is sealed to replyTo
  • no record names its recipient: a reader trial-opens every signal for its tag and keeps the ones that open
  • no private key, SDP, ICE candidate or IP address is ever in a record, a ContractEvent, a StateSync, kcp or a log
  • CLI side: run request proxy <name> --tag <tag> (internal/webrtcproxy); STUN stun:stun.cloudflare.com:3478 only, no TURN
sequenceDiagram
  autonumber
  participant C as CLI / browser
  participant R as Requester
  participant B as Bidder
  participant G as guest agent

  G->>B: POST guest.publicKey tag t (age1 guest key)
  B->>R: market.event guest.publicKey (bidder repo)
  R->>C: inbound ContractEvent publicKey
  C->>R: ContractEvent publicKey tag t (age1 requester key)
  C->>R: ContractEvent signalWebRTC offer, sealed to guest key, replyTo = requester key
  R->>B: submitEvent publicKey, signalWebRTC offer
  B->>G: subscribeEvents frames
  G->>G: open offer, answer, check nothing else
  G->>B: POST guest.signalWebRTC answer sealed to replyTo
  B->>R: market.event signalWebRTC answer
  R->>C: inbound ContractEvent answer
  C->>G: datachannel tag t, DTLS fingerprint = vm.onNetwork webrtc address

Secrets: getSecrets Or Sealed Fallback

  • spec.secretsRef names a bundle [{path, value, mode?}] in the requester's OpenBao; mode octal, at most 0600 (ParseSecretsBundle in internal/market/requester/secrets.go)
  • primary: getSecrets. setup-secrets.service exchanges its workload identity token and GETs /xrpc/com.publicdomainrelay.temp.compute.secrets.getSecrets/<contract> from the requester ingress (lexicons-market.md)
  • the requester checks the token audience api://ATProto?actx=<requester DID> and the subject claim (secretsState.fetch)
  • fallback: only when the requester's spec.secretsFallback is Sealed, and only on demand. never a sealed bundle unasked: published ciphertext stays on the protocol and can be broken later
  • setup-secrets gives getSecrets 90 s (cloudinit.SecretsFetchFallbackDeadlineSec), then touches /run/socialweb-guest-agent/secrets.request; comms mode Firehose has no setup-secrets, the agent asks at once
  • both paths end in /run/socialweb-guest-agent/secrets.ready; consumers order After= cloudinit.SecretsReadyUnits
  • status: status.contract.secretsRequested, status.contract.secretsApplied, condition SecretsApplied reason GetSecrets or SealedFallback
sequenceDiagram
  autonumber
  participant G as guest
  participant CP as GuestControlPlane
  participant R as Requester ingress
  participant B as Bidder

  G->>CP: GET /v1/identity/oidc/token/role (workload identity)
  CP-->>G: token
  G->>R: GET getSecrets/contract, Bearer token
  alt ingress answers within 90 s
    R-->>G: [{path, value, mode}]
    G->>G: write files, secrets.ready
    G->>B: POST guest.secretsApplied via getSecrets
  else no answer and secretsFallback Sealed
    G->>G: touch secrets.request
    G->>B: POST guest.publicKey tag secrets
    G->>B: POST guest.secretsRequested
    B->>R: market.event publicKey, secretsRequested
    R->>R: seal bundle to that key, ContractEvent secrets-contract
    R->>B: submitEvent guest.secrets
    B->>G: subscribeEvents guest.secrets
    G->>G: open, write files root 0600, secrets.ready
    G->>B: POST guest.secretsApplied via sealed
  end
  B->>R: market.event secretsApplied
  • the requester reseals for a newer key while the request is unanswered (sealGuestSecrets in internal/market/requester/guestsecrets.go)
  • a Firehose requester with None and a secretsRef fails the contract
  • a bundle change (guest.secretsUpdated) makes the guest fetch again (SECRETS_REFETCH=1 setup-secrets.sh) or ask for the sealed fallback

Agent Start And atirc Mint

  • spec.agent {envFile} (default /etc/atirc/atirc.env) needs spec.secretsRef
  • after VerifyReceipt, ensureAtircToken (internal/market/requester/atirc.go) mints with chat.atirc.mint (outside.md), lease = {receipt, termination: at-uri of the bidder's vm.terminated payload at EventRKeys(TerminationKey(receipt)), contract: accept uri} (contractLease)
  • the token goes into the bundle entry at envFile as ATIRC_TOKEN; getSecrets and the sealed fallback serve the bundle without that entry until it is minted
  • every fresh mint publishes guest.secretsUpdated; the guest fetches again; atirc-agent-env.path runs systemctl try-restart --no-block atirc-agent.service when the envFile changed (never a start)
  • run request agent <name> makes an outbound guest.agentStart; the guest agent runs systemctl start --no-block atirc-agent.service
  • every mint is recorded under atircMinted in the bundle secret; status status.atircRevokes (Done, Pending, Abandoned)
sequenceDiagram
  autonumber
  participant R as Requester
  participant A as atirc
  participant V as requester OpenBao bundle
  participant B as Bidder
  participant G as guest

  R->>R: receipt verified (ReceiptOk)
  R->>A: chat.atirc.mint subhandle from the RFP rkey, lease receipt + vm.terminated at-uri + accept
  A-->>R: token with explicit exp
  R->>V: ATIRC_TOKEN into envFile entry, record atircMinted
  R->>B: submitEvent guest.secretsUpdated
  B->>G: subscribeEvents secretsUpdated
  G->>R: getSecrets again (or sealed fallback)
  G->>G: envFile sha256 changed, try-restart atirc-agent.service
  R->>B: submitEvent guest.agentStart (run request agent)
  B->>G: subscribeEvents agentStart
  G->>G: systemctl start --no-block atirc-agent.service
  G->>A: atirc agent joins as subhandle
  • release: the leased token is revoked before vm.delete while its lease is alive; the rest in the background from a release marker (market.md)
  • when the VM ends without a release, atirc ends the token through its vm.terminated lease

Guest Requester (R2) Events

  • a contract with spec.association.fromGuest runs a requester inside the guest; full flow: identity.md
  • guest.requesterDid: the guest reports the DID of its requester; the requester then reads market.associationRequest by one getRecord and writes the contract-scoped market.association (lexicons-market.md)
  • guest.r2Start starts socialweb-r2.service as user r2 (run request r2 <name>)
  • release of the parent: guest.r2Release first, then wait for guest.r2Released (GuestRequesterReleaseTimeout), then atirc revokes, then vm.delete

Never

  • a guest never holds a repo key; a guest never posts guest.secrets, guest.agentStart, guest.secretsUpdated, guest.r2Start or guest.r2Release
  • the bidder never opens a sealed body
  • no private key, SDP, ICE candidate or IP address in any record, object or log
  • no sealed bundle unasked
  • no fourth guest transport (iroh, opentunnel https, webrtc only; ../SYSTEM.md)

See Also