# outside: lexicons we use but do not own - records and XRPC methods defined by atproto, Bluesky, tangled, atirc and fedproxy that socialweb-computer reads, writes or calls - the OAuth scopes each role asks for, ours and theirs - index: [README.md](README.md) ## At A Glance | NSID | owner | type | we | Go | step | |---|---|---|---|---|---| | `sh.tangled.graph.vouch` | tangled | record | read (policy), write (market network operators) | `internal/policyengine/trust/trust.go`, `internal/app/compose/records.go` | S1 | | `chat.atirc.mint` | atirc | procedure | call (requester) | `internal/market/requester/atirc.go`, `sessionatirc.go` | S8 | | `chat.atirc.revoke` | atirc | procedure | call (requester) | same | S9 | | `com.atproto.server.getServiceAuth` | atproto | query | call (every role), serve (in-repo PDS) | `internal/atproto/pdsclient/serviceauth.go`, `internal/atproto/xrpc/serviceauth.go` | S3 to S9 | | `com.atproto.repo.getRecord` | atproto | query | call (everywhere), serve | `internal/atproto/pdsclient/client.go` | all | | `com.atproto.repo.putRecord`, `createRecord` | atproto | procedure | call, serve | same | all | | `com.atproto.repo.deleteRecord` | atproto | procedure | call (own contract-scoped records only), serve | same | S9 | | `com.atproto.repo.listRecords` | atproto | query | call (legacy fallback, discovery), serve | same | - | | `com.atproto.repo.applyWrites` | atproto | procedure | serve only (one custody call) | `internal/atproto/pds`, `internal/gateway/custody/signin.go` | - | | `com.atproto.repo.strongRef` | atproto | object | vendored copy in `lexicons/com/atproto/repo/strongRef.json` | `internal/atproto/market` | all | | `com.atproto.sync.subscribeRepos` | atproto | subscription | subscribe (relay, PDS), serve (relay, in-repo PDS) | `internal/events/relay`, `internal/atproto/relay`, `internal/atproto/pdsclient/subscribe.go` | S3 to S9 | | `com.atproto.sync.getRepoStatus`, `getRepo`, `getLatestCommit`, `requestCrawl` | atproto | query / procedure | call, serve (relay, PDS) | `internal/eventstream/market.go`, `internal/atproto/relay` | - | | `com.atproto.server.createSession`, `refreshSession`, `createAccount`, `describeServer`, `getSession`, `deleteSession` | atproto | procedure / query | call (password identities, dev accounts), serve | `internal/atproto/pdsclient/client.go`, `internal/pds/remote` | S0 | | `network.bsky.jetstream.subscribeEvents` | Bluesky | subscription (Jetstream v2) | subscribe | `internal/events/jetstream`, `internal/controllers/jetstreameventsource` | S3 to S9 | | `app.bsky.feed.post`, `app.bsky.richtext.facet#mention` | Bluesky | record, facet | read (association kind `bskyPost`) | `internal/policyengine/trust/association.go` | S1 | | `town.delve.feed.post` | delve | record | read (association kind `delvePost`) | same | S1 | | `com.fedproxy.rbac` | fedproxy | record | write (requester), read (guest control plane) | `internal/market/requester/secrets.go`, `internal/guestidentity` | S5, S8 | | `com.fedproxy.sshPublicKey` | fedproxy | record | scope only | `internal/atproto/oauth/scope.go` | - | ## sh.tangled.graph.vouch | field | value | |---|---| | NSID | `sh.tangled.graph.vouch` (tangled's lexicon; no copy under `lexicons/`) | | type | record | | rkey | the subject DID | | key fields | `subject`, `createdAt`; `kind` (a `denounce` is not a vouch) | | written by | market network operator vouches, both directions (`internal/app/marketnetwork/vouches.go` -> compose `ApplyOperatorVouch`, an AtprotoRecord in the operator repo); people, from any tangled client | | read by | `trust.Resolver.Vouches`: one getRecord at `at:///sh.tangled.graph.vouch/`; requester discovery `vouchedBy` (listRecords) | | step | [S1](README.md#step-s1); read at every policy decision ([S4](README.md#step-s4), [S5](README.md#step-s5)) | - the default policy on both sides: [policy.md](policy.md#tangled-vouch) - OAuth: `repo:sh.tangled.graph.vouch?action=create&action=update` is in the requester set but optional (`OptionalScopes`) ## chat.atirc - the full protocol (token kinds, mint rules, leases, every `chat.atirc.*` NSID, frame shapes) lives in the atirc repo: `docs/protocol/atirc.md` (atirc `da089a0`, `git@tangled.org:did:plc:vtad4w7ujsvz7t6o6iuvmhjq`) - here: only what socialweb-computer uses | item | value | |---|---| | base | `https://irc.fedfork.com` (`DefaultAtircURL`) | | aud | `did:web:irc.fedfork.com#atirc` (`AtircServiceAudience`) | | call | `POST /xrpc/`, `Authorization: Bearer `, 30 s timeout, 64 KiB response cap | | mint input | `{subhandle, exp, lease?}` | | mint output | `{token}` (no whitespace) | | lease | `{receipt {uri, cid}, termination (the bidder's vm.terminated at-uri), contract (the accept at-uri), association? {uri, cid}}` | | revoke input / output | `{subhandle}` / `{revokedAt}` | | token env | `ATIRC_TOKEN`, `ATIRC_SUBHANDLE`, `ATIRC_EXP` | ### Mint ```mermaid sequenceDiagram autonumber participant R as requester participant P as requester PDS participant A as atirc (irc.fedfork.com) participant G as guest Note over R: S6 receipt verified (no mint before it) R->>P: getServiceAuth aud irc.fedfork.com atirc service, lxm chat.atirc.mint P-->>R: service auth JWT R->>A: chat.atirc.mint {subhandle LABEL-RFPRKEY.HANDLE, exp, lease} A-->>R: {token} with leases claim R->>R: leases == parent chain + own lease, else revoke and refuse R->>G: S8 token in the secrets bundle (envFile), guest.secretsUpdated ``` - issuer: the requester's own identity via service auth, or a parent atirc token (`spec.atircParentRef`, vault path `identity/atirc-parent`) for a guest requester; its child label is `vm` - exp: min(now + 24 h, contract deadline, issuer exp). an ssh session token is capped at the session cap - the guest requester (R2) token carries the association strongRef in its lease; atirc clamps its exp to the association `expiresAt` - ssh gateway sessions: child `ssh-.`, sent as `LC_ATIRC_TOKEN` (`internal/market/requester/sessionatirc.go`); scope `rpc:chat.atirc.mint` + `rpc:chat.atirc.revoke` (`internal/app/compose/atprotoscope.go` `SshGatewayAtprotoScope`) - every mint is recorded (path, subhandle, token, exp) under `atircMinted` in the contract's bundle secret ### Revoke - before vm.delete, while the lease is alive (`revokeAtircToken`, `revokeBeforeDestroy`) - bearer order (`revokeMinted`): service auth with lxm `chat.atirc.revoke` when the OAuth session was granted `rpc:chat.atirc.revoke` and the subhandle is below the requester handle; else the parent token; else the token itself - a 401 "lease is not alive" counts as revoked (byLease); other 401s for the own token count as already revoked - outcome in `status.atircRevokes` (Done, Pending, Abandoned after 24 h) - orphan sweep on every requester start (`internal/market/requester/orphanrevoke.go`); skipped when the requester has no `vaultRef` ### What atirc checks (as this repo relies on it) - mint needs an explicit `exp`, at most 24 h - a leased token dies once its lease's `vm.terminated` record exists - revoking a subhandle revokes everything below it; revoke answers 200 for any name in the caller's scope, so the requester sends a revoke only for the token's own `subhandle` claim - a contract-scoped association ref needs `expiresAt`; the minted exp is clamped to it ## com.atproto.repo.strongRef | field | value | |---|---| | NSID | `com.atproto.repo.strongRef` (atproto's; a vendored copy in `lexicons/com/atproto/repo/strongRef.json` so the embedded catalog resolves refs) | | type | object `{uri (at-uri), cid}` | | used in | `market.rfp` (`payload`, `policies[]`, `ingress[]`), `market.bid`, `market.accept`, `market.receipt`, `market.event`, `market.association` (`contract`), `badgeBlueKeys` (`proof`), `network.attested.proof` (`subject`) | | Go | `internal/atproto/market` `StrongRef`, `StrongRefNSID` (generated with the market types) | - a reader fetches the target with one getRecord and compares the cid where the flow pins it (attached policy, association proof) ## Service Auth - every cross-service call carries an atproto service-auth JWT from `com.atproto.server.getServiceAuth` (`aud`, `lxm`, `exp`) | lxm | aud | caller | verified by | |---|---|---|---| | `com.publicdomainrelay.temp.market.submitRfp`, `submitAccept`, `submitEvent` | the bidder's DID or `did#serviceId` | requester (`internal/market/requester/market.go`) | `internal/atproto/market/auth.go` via `participant/comms.go` | | `market.submitBid`, `submitEvent` | the requester's DID or `did#serviceId` | bidder (`internal/market/bidder/submit.go`, `adapters.go`, `participant/serviceauth.go`) | same | | `com.publicdomainrelay.temp.cluster.*` | gateway audience | `internal/gatewayhttp/cluster.go` | `internal/xrpcserver`, `internal/atprotoauth` | | `computer.socialweb.k8s.*` | `did:web:k8s.socialweb.computer` | cluster owners | same | | `chat.atirc.mint`, `chat.atirc.revoke` | `did:web:irc.fedfork.com#atirc` | requester | atirc | - verifiers use indigo `auth.ServiceAuthValidator`, except the ingress relay (golang-jwt plus an `lxm` equality check, `internal/atproto/ingress/auth.go`) - a `did:key` issuer (did:key ingress) signs locally, no PDS round trip (`internal/atproto/ingress/auth.go`): [identity.md](identity.md#didkey-ingress) - the in-repo PDS serves getServiceAuth (`internal/atproto/pds/service_auth.go`) - `atproto-proxy`: only forwarded as a header on ingress subscriptions; no client sets it - the in-repo PDS's own session JWTs carry `scope` `com.atproto.access` / `com.atproto.refresh` (`internal/atproto/pds/auth.go`) ## Repo Reads And Writes - one `getRecord` at a deterministic rkey beats a `listRecords` scan; `listRecords` remains only as a legacy fallback - client: `internal/atproto/pdsclient` (getRecord, putRecord, createRecord, deleteRecord, listRecords, getRepo, getLatestCommit) - payload records are read through the strongRef with getRecord, never taken from a stream | deterministic rkey | where | |---|---| | bid `market.BidRKey`, termination `TerminationKey`, events `EventRKeys`, session `SessionRKey`, rbac `RBACRKey` | `internal/atproto/market/records.go` | | receipt `receipt-<32 hex>` | `internal/atproto/market/attest.go` `ReceiptRKey` | | badgeBlueKeys signer binding | `internal/atproto/market/badgeblue.go` | | contract-scoped association / request | `internal/atproto/market/guest.go` `ContractScopedRKey` | | offering, heartbeat, relays | `self` | | infra, ephemeralInfra | object name or successor, index at `self`: [infra.md](infra.md#rkeys) | | listRecords still used | why | |---|---| | `internal/guestidentity/rbac_reader.go` | accepts from before stable rbac rkeys (`RBACAtAcceptSince`) | | `internal/market/requester/discovery.go` | trust lists (vouches) | | `internal/ssh/ownersession/ownersession.go`, `internal/gateway/custody/signin.go`, `internal/atproto/market/http.go` | legacy and custody paths | - deleteRecord: only on records the app wrote itself for a contract: the requester's `market.association` and `com.fedproxy.rbac` grants (`internal/market/requester/association.go`, `secrets.go`), a bidder outbox event it retracts (`internal/market/bidder/adapters.go`), an AtprotoRecord object whose policy is not Retain (`internal/controllers/atprotorecord/apply.go`). never on Infra records, never on someone else's records | server | serves | |---|---| | `internal/atproto/pds` | the HTTP PDS: server, repo, sync, subscribeRepos (the app's own Pds component, dev networks) | | `internal/pds/local` | in-process adapters over that PDS (`localRepo` identities) | | `internal/pds/remote` | HTTP to a remote PDS through pdsclient (`pdsAccount`, OAuth) | | `internal/services/requester/routes.go` | `repo.getRecord`, `repo.listRecords`, `server.describeServer` of the requester's repo | ## Streams | source | NSID | filter | Go | |---|---|---|---| | our relay `https://relay.mini-cloud-0002.chadig.com` (or the network relay) | `com.atproto.sync.subscribeRepos` | repeated `wantedCollections` (max 100), `wantedDids` (max 10000) | `internal/events/relay`, `internal/controllers/relayeventsource` | | Jetstream v2 `wss://jetstream.us-east.bsky.network`, `wss://jetstream.us-west.bsky.network` | `network.bsky.jetstream.subscribeEvents` | `collections`, `dids`, `kinds=commit` | `internal/events/jetstream`, `internal/controllers/jetstreameventsource` | | a Jetstream URL ending in `/subscribe` | Jetstream v1 | `wantedCollections`, `wantedDids` | same | - requester stream: `market.bid`, `market.receipt`, `market.event`; bidder stream: `market.rfp`, `market.accept`, `market.event` (`internal/app/compose/streams.go`) - after a commit, `com.atproto.sync.getRepoStatus` on the relay finds the author's PDS, then getRecord there (`internal/eventstream/market.go`) - the relay we run (`internal/atproto/relay`) serves describeServer, requestCrawl, subscribeRepos, getRepo, getRepoStatus, listRepos, listReposByCollection, getLatestCommit, listHosts, getHostStatus; its crawler dials each PDS's subscribeRepos - detail of the market streams: [market.md](market.md) ## Bluesky Posts As Associations - association kinds `bskyPost` (`app.bsky.feed.post`) and `delvePost` (`town.delve.feed.post`), opt-in per verifier - match: `text` equals the template with `{did}` filled in (default `associate {did}`, trimmed) AND exactly one `app.bsky.richtext.facet#mention` whose `did` is the subject (`internal/policyengine/trust/association.go` `postMatches`) - posts are made from any client and pasted as an at-uri; we never ask for a feed.post scope - detail: [identity.md](identity.md#associations-plan-0036) ## com.fedproxy | NSID | rkey | written by | read by | purpose | |---|---|---|---|---| | `com.fedproxy.rbac` | `node.` (`RBACRKey`) | requester (`internal/market/requester/secrets.go`) | guest control plane (`internal/guestcp/openbao/plane.go` -> `internal/guestidentity/grants.go`, `rbac_reader.go`) | roles `{role_name, definition {iss, aud, sub, policies}}` for the guest secrets and node-join JWT roles | | `com.fedproxy.sshPublicKey` | - | nobody in Go | nobody | create scope in `ComputeSPAOAuthScope` only | ## Scope-Only Collections - NSIDs that appear only as OAuth scopes: `com.publicdomainrelay.temp.auth.allowlist.rbacDid` (create, bidder and desktop sets), `com.fedproxy.sshPublicKey`, `com.publicdomainrelay.temp.market.bidderAssociation` ([identity.md](identity.md#marketbidderassociation)), `rpc:com.publicdomainrelay.temp.requester.associateConfirm` ([identity.md](identity.md#requesterassociateconfirm)) ## OAuth Scopes Per Role - sets: `internal/atproto/oauth/scope.go` (identities) and `internal/app/compose/atprotoscope.go` (the OpenBao atproto plugin of the gateway and ssh gateway) - every set starts with `atproto`; a `repo:?action=...` per collection; `rpc:?aud=...` per method | set | used by | repo (write) | rpc | |---|---|---|---| | `RequesterOAuthScope` | CLI and long-lived requesters (`internal/market/requester/requester.go`), `pkg/embedded` (iOS), requester client metadata (`internal/services/requester/routes.go`), dev requester identities (`internal/devcore`) | create+update: `compute.vm`, `market.rfp`, `market.accept`, `market.event`, `compute.events.vm.delete`, `vm.onNetwork`, `guest.publicKey`, `guest.signalWebRTC`, `guest.secrets`, `guest.agentStart`, `guest.secretsUpdated`, `guest.r2Start`, `guest.r2Release`, `policy.ghalite`, `policy.typescript`; create+update+delete: `com.fedproxy.rbac`, `badgeBlueKeys`, `market.association`, `market.associationRequest`, `computer.socialweb.infra`, `ephemeralInfra`; optional: `sh.tangled.graph.vouch` | `market.submitRfp`, `submitAccept`, `submitBid`, `submitEvent` (`aud=*`); `chat.atirc.mint`, `chat.atirc.revoke` (aud `did:web:irc.fedfork.com%23atirc`, revoke optional) | | `BidderOAuthScope` | bidder identities (`internal/devcore`, bidder client metadata `internal/services/bidder/routes.go`) | create: `auth.allowlist.rbacDid`, `market.bid`, `bids.free`, `market.receipt`, `market.event`, `compute.config.wif.simple`, `compute.vm`, `market.rfp`, `market.accept`, `vm.delete`, `vm.onNetwork`, `vm.terminated`, `vm.hostKeys`, `guest.publicKey`, `guest.signalWebRTC`, `guest.secretsApplied`, `guest.secretsRequested`, `guest.requesterDid`, `guest.r2Released`, `com.fedproxy.rbac`, `market.bidderAssociation`, `policy.ghalite`, `policy.typescript`; create+update: `market.offering`, `market.heartbeat`; create+update+delete: `badgeBlueKeys`, `market.association` | the four `market.submit*` | | `DesktopOAuthScope` | desktop client | create+update: `rbacDid`, offering, bid, bids, receipts, event, `wif.simple`, `com.fedproxy.rbac`, `bidderAssociation`; create: `badgeBlueKeys` | `submitBid`, `submitEvent` | | `ComputeSPAOAuthScope` | compute SPA | create: `compute.vm`, `badgeBlueKeys`, `com.fedproxy.rbac`, `com.fedproxy.sshPublicKey` | `submitRfp`, `submitAccept` | | `DIDKeyAssociatorOAuthScope` | `oauthQr` sign-in at qr.fedfork.com (`internal/stack/componentcore/controllers.go` `QRScopes`) | bidder + requester sets | + `requester.associateConfirm` | | `K8sOAuthScope` | k8s requester | requester set | + `cluster.createCluster`, `listClusters`, `getKubeconfig`, `controlPlaneUp`, `controlPlaneDown`, `deleteCluster` | | `OAuthScope` | the union | all of the above | all of the above | | `GatewayAtprotoScope()` | gateway OpenBao atproto plugin and gateway session owners (`internal/app/gateway`) | compose `requesterScope` (a shorter requester list: no guest.agentStart/r2*, no vouch) + `heartbeat` | `market.submit*` + `cluster.*` | | `SshGatewayAtprotoScope()` | ssh gateway OpenBao plugin and session owners (`internal/app/sshgateway`) | compose `requesterScope` | `market.submit*` + `chat.atirc.mint`, `chat.atirc.revoke` | ### Optional Scopes - `OptionalScopes` = `rpc:chat.atirc.revoke?aud=did:web:irc.fedfork.com%23atirc`, `repo:sh.tangled.graph.vouch?action=create&action=update` - `RequiredScopes` drops them from the identity's check (`internal/marketidentity/atproto/oauth.go`): a session granted without them stays Ready - a missing optional scope -> `status.pendingAction {kind: signInAgain, reason: ScopeUpgrade}`; the feature that needs it falls back (revoke with the token itself) - a new REQUIRED scope = SessionExpired for OAuth identities on rollout. plan the re-sign-in first - granted scopes are read from the token response, never the access token's `scope` claim (the claim is an opaque `ref:` on bsky) - a scope change is mirrored token for token in the other clients (typescript-helpers `lib/oauth-scope/mod.ts`, did-key-associator `generated/oauth-scope.js`, kcp-wasm `scripts/serve-public.ts`) per plan 0036 ## See Also - [README.md](README.md), [identity.md](identity.md), [policy.md](policy.md), [market.md](market.md) - [../SYSTEM.md](../SYSTEM.md#service-auth), [../oauth.md](../oauth.md), [../ATIRC-WORKFLOW.md](../ATIRC-WORKFLOW.md)