SYSTEM: socialweb-computer, the whole system
- one page map of every component, repo, host and flow
- deep dives: KCP.md (kcp, workspaces, API catalog), protocol/README.md (every lexicon, its use and its step in the sequence), ARCHITECTURE.md (decisions, invariants; open by heading)
- rules for agents: ../AGENTS.md
- no history here. what was done when: git log,
docs/plans/*.yaml
What It Is
socialweb-computer is a compute market on atproto. A requester posts a signed RFP record to its own
atproto repo; bidders (hosts that run firecracker VMs) see it on the firehose, bid with records in their
own repos, the requester accepts one bid, and the winner boots a VM from cloud-init user_data. The
requester reaches the guest over iroh (plus opt-in opentunnel https and webrtc). On top of the market sit
products: ssh into a fresh VM as yourself (ssh <handle>@socialweb.computer), tenant Kubernetes clusters
that scale nodes from zero by renting market VMs, a browser page and an iPhone app that run a requester
in process, and atirc agents that join a chat channel from inside a VM. Every application is a kcp of its
own (or a workspace of one shared kcp in a market network); state is kcp objects; secrets live only in
the application's own OpenBao.
Repos
| repo | upstream | role | where it runs |
| socialweb-computer-kcp | https://github.com/publicdomainrelay/socialweb-computer-kcp | the monorepo: Go core socialweb-computer, guest agent, vmexec, apigen, lexicons, manifests, policy actions, socialweb-meshd (rust) | gateway host, every bidder, CLI users, guests (guest tools), wasm page and iOS app (as a library) |
| kcp fork | https://github.com/johnandersen777/kcp | kcp built for js/wasm with memory storage; builds the browser page (socialweb-wasm/, page-rcN tags) | shell.socialweb.computer (browser) |
| kcp upstream | https://github.com/kcp-dev/kcp | native kcp asset, unpatched, built from main and pinned in internal/versions/versions.json | gateway, bidders, CLI requesters, market networks |
| indigo | https://github.com/bluesky-social/indigo | atproto Go libs (repo, MST, xrpc, identity), Go module dependency; local checkout for reading | linked into socialweb-computer |
| atirc | git@tangled.org:did:plc:vtad4w7ujsvz7t6o6iuvmhjq | atproto IRC: channels, subhandle tokens (mint, revoke, lease), agents, web chat, send/recv pipe | irc.fedfork.com server; atirc CLI in guests and on operator hosts |
| AT-IRC iOS app | local only (atirc-client-ios) | iPhone chat client; embeds pkg/embedded (in-process kcp + requester) to launch VM agents | iPhone |
| did-key-associator | local only (frozen TS, copy also in monorepo history) | qr.fedfork.com: OAuth QR session-transfer page for headless identities (oauthQr) | qr.fedfork.com |
| atproto-reverse-proxy | https://github.com/publicdomainrelay/atproto-reverse-proxy | fedproxy.com ingress: legacy deno relay, ssh relay, caddy; the Go ingress relay from this monorepo runs as a drop-in | fedproxy.com |
| typescript-helpers | https://github.com/publicdomainrelay/typescript-helpers | frozen TS helpers (event streams, abc, cli-args-env); reference only | nowhere live |
Hosts And Roles
| host | role | runs | public names |
| socialweb.computer | gateway host | Gateway app (kcp 1): GatewayAPI web app, cluster provider, tenant k3s control planes, k8s requester; SshGateway app (kcp 2): Go ssh server on :22, per-user Requesters; caddy 443; host sshd on 1997 | k8s.socialweb.computer, kcp.socialweb.computer, ssh.socialweb.computer |
| hat-8 | "local" bidder + operator/lead host | MarketBidder (account swc-bidder), dev market networks, builds, deploy scripts | none (requestCrawl to relay) |
| hat-7 | bidder | MarketBidder (bob, oauthQr identity) | none |
| hat-9 | bidder | MarketBidder | none |
| irc.fedfork.com | atirc | atirc server, atirc-iroh.service, /chat web client | irc.fedfork.com, did:web:irc.fedfork.com#atirc |
| fedproxy.com | did:key xrpc ingress | Go internal ingress-relay (caddy drop-in 30-go.conf of xrpc-relay-server.service), ssh relay :2222 | xrpc.fedproxy.com, *.fedproxy.com |
| relay.mini-cloud-0002.chadig.com | atproto relay | Go internal atproto-relay (127.0.0.1:2584 behind caddy) | firehose source, bidders requestCrawl here |
| qr.fedfork.com | OAuth QR | did-key-associator backend | qr.fedfork.com |
| shell.socialweb.computer | static page | wasm kcp + requester in the browser tab | shell.socialweb.computer |
| iPhone | client | AT-IRC app, embedded requester | none |
- every listener behind the edge binds port 0 and reports its address into kcp status
- only stable public entry points keep fixed ports: caddy 443, gateway kcp front 127.0.0.1:6443, GatewayAPI web-app front 127.0.0.1:8788, ssh 22
- prod installs run from the operator host over ssh. CI never deploys
System Context
flowchart LR
subgraph USERS["Users"]
SSHC["ssh client"]
CLI["socialweb-computer run request"]
PAGE["browser page shell.socialweb.computer"]
IOS["AT-IRC iOS app"]
KUBE["kubectl / XRPC k8s client"]
end
subgraph GWH["socialweb.computer"]
GW["Gateway app: GatewayAPI, cluster provider, tenant k3s"]
SG["SshGateway app: SshServer :22, per-user Requesters"]
BAO["OpenBao: OAuth sessions, keys"]
end
subgraph BID["Bidders: hat-8, hat-7, hat-9"]
MB["MarketBidder: Bidder, GuestControlPlane, Pds, PolicyEngine"]
VM["firecracker guests"]
end
subgraph AT["atproto network"]
PDS["PDSes: user repos"]
RELAY["relay.mini-cloud-0002.chadig.com"]
JS["Jetstream us-east, us-west"]
PLC["plc.directory"]
ING["xrpc.fedproxy.com ingress"]
QR["qr.fedfork.com"]
end
ATIRC["irc.fedfork.com atirc"]
SSHC -->|"ssh handle@"| SG
KUBE -->|"service auth XRPC"| GW
CLI -->|"records + submitRfp"| PDS
PAGE -->|"records"| PDS
IOS -->|"records"| PDS
SG --> BAO
GW --> BAO
SG -->|"rfp, accept records"| PDS
GW -->|"rfp, accept records"| PDS
MB -->|"bid, receipt, events"| PDS
PDS --> RELAY
RELAY -->|"firehose"| MB
RELAY -->|"firehose"| SG
JS -->|"jetstream"| MB
ING <-->|"submitBid, submitEvent"| MB
ING <-->|"did:key callbacks"| SG
MB --> VM
SG -->|"ssh over iroh"| VM
VM -->|"atirc agent"| ATIRC
SG -->|"mint, revoke"| ATIRC
IOS -->|"chat"| ATIRC
MB -->|"oauthQr sign-in"| QR
PDS --- PLC
Applications
- one root object per application, in its own kcp (prod) or its own workspace
root:<app> (market network)
- the core (
socialweb-computer process) runs the root's composition controller (internal/app/<root>), every controller, the component runtime and the HostProcess supervisor
- components run in process; third-party binaries (kcp, kine, OpenBao, dumbpipe, firecracker via vmexec, deno for policy) are HostProcess units
| root kind | CLI | composes (main children) | role |
| MarketBidder | run bidder, install bidder | Bidder, GuestControlPlane, Pds (localRepo mode) or RemotePDS (OAuth), PolicyEngine, OpenBaoServer, IngressProxy, MarketIdentity, EventStreams, ComputeVM/FirecrackerVM (per contract) | sells VMs |
| MarketRequester | run request | Requester, PolicyEngine, OpenBaoServer, Pds (localRepo) or OAuth identity, IngressProxy, MarketIdentity, EventStreams, ComputeContract | rents VMs; one fresh kcp per invocation, state dir persists |
| Gateway | serve gateway kcp, install gateway | GatewayAPI, Requester, PolicyEngine, OpenBaoServer, cluster provider, MarketIdentity (alice requester), Infra per cluster | hosted tenant k8s clusters |
| SshGateway | run gateway ssh | SshServer, per-DID Requesters, PolicyEngine, OpenBaoServer ssh-gateway, iroh dialer HostProcess, session-owner MarketIdentities, Exposures | ssh as yourself into a fresh VM |
| MarketNetwork | dev up --profile market-network | Plc, Relay, Pds alice/bob, IngressProxy dispatcher, DispatcherTLS, PolicyEngine, OpenBaoServer network (PKI), child roots (bidders, gateway, requesters, ssh gateways) | dev/CI/soak: a whole local market in one kcp |
Components (in-process kinds)
| component | package | does |
| Bidder | internal/services/bidder, internal/market/bidder | reads RFPs, runs policy, bids (bids.free), takes accepts, writes receipts, creates ComputeVMs, publishes vm events |
| GuestControlPlane | internal/services/guestcontrolplane | guest-facing endpoints at http://192.0.2.1:<port>: OpenBao allow-list proxy (workload identity), POST /v1/events, GET /v1/downloads/<sha256>/<name>, compute.guest.subscribeEvents |
| Requester | internal/services/requester, internal/market/requester | runs each ComputeContract: RFP, bid window, accept, receipt, network, secrets, atirc mint/revoke, release |
| Pds | internal/services/pds, internal/pds/local | a local PDS for an identity without a real account (localRepo) |
| Plc, Relay | internal/services/plc, internal/services/relay | network-local PLC and relay (market network) |
| IngressProxy | internal/services/ingressproxy | did:key ingress to the app's XRPC (xrpc-subscription, iroh or opentunnel provider); default host xrpc.fedproxy.com |
| DispatcherTLS | internal/services/dispatchertls | TLS front of the market network dispatcher |
| GatewayAPI | internal/services/gatewayapi, internal/gatewayhttp | k8s.socialweb.computer: /oauth/*, /session, /api/*, /xrpc/computer.socialweb.k8s.*, /downloads/* |
| SshServer | internal/services/sshserver | ssh on :22, web sign-in at /, /keys, session owners, per-session contracts |
| PolicyEngine | internal/policyengine | gha-lite workflows in embedded deno; bundled actions bundled-actions/tangy/ |
Market Protocol
- lexicons:
lexicons/ (NSID prefix com.publicdomainrelay.temp.)
- every lexicon, field by field, with its sequence step: protocol/README.md
- applications talk ONLY over this protocol. a shared kcp is never a data channel
Records
| record | written by | in repo of | purpose |
compute.vm | requester | requester | the VM asked for (size, role) |
market.rfp | requester | requester | signed request for proposals; strongRefs vm payload + policy; callbacks submitBid/submitEvent |
market.offering, market.heartbeat | bidder | bidder | what the bidder sells; liveness. offering at rkey self |
market.bid + market.bids.free / bids.x402 | bidder | bidder | bid envelope + payload. rkey market.BidRKey (one bid per RFP) |
market.accept | requester | requester | the chosen bid (no payload record) |
market.receipt | bidder | bidder | bidder took the accept; payload = the RFP compute.vm ref |
market.event + compute.events.vm.*, compute.events.guest.* | both | sender | lifecycle: vm.hostKeys, vm.onNetwork, vm.delete, vm.terminated, guest.* |
market.association, market.associationRequest | requester | requester / guest requester | contract-scoped association (guest requester R2) |
badgeBlueKeys | operator / user | operator / user | key claims: bidder association (bidder_associate), requester association, ssh key claims |
sh.tangled.graph.vouch | operators, users | voucher | trust edge read by tangled-vouch policies |
computer.socialweb.infra, computer.socialweb.ephemeralInfra | the owner's kcp (infra controller: CLI, gateway, page, iOS) | owner | the record of an Infra / EphemeralInfra object (two-way sync); per-account index at rkey self; never deleted (protocol/infra.md) |
XRPC
| method | served by | caller |
market.submitRfp | bidder ingress | requester (push path); 409 RFPClosed after the window |
market.submitBid | requester ingress | bidder |
market.submitAccept | bidder ingress | requester |
market.submitEvent | both ingresses | either side |
compute.secrets.getSecrets/<contract> | requester ingress | guest setup-secrets.service (workload identity token) |
compute.guest.subscribeEvents | GuestControlPlane | guest agent (WebSocket, cursor, 25 s keepalive) |
computer.socialweb.k8s.* | GatewayAPI | cluster owners (service auth, aud did:web:k8s.socialweb.computer) |
Comms modes
spec.comms.mode: XRPC (ingress only), Firehose (no inbound endpoint), Both (requester default)
- event sources (Firehose/Both): network relay or
relay.mini-cloud-0002.chadig.com, Jetstream v2 us-east/us-west
- server-side filter:
wantedCollections, wantedDids; client filter always runs too
- requester stream carries
market.bid, market.receipt, market.event; bidder stream market.rfp, market.accept, market.event
- payload records are read through the strongRef (getRecord), never taken as stream input
- dedup: one sink per role drops a
uri|cid seen in the last 15 s
- addressee index: requester keeps only records about its own ComputeContracts; bidder only RFPs, accepts of its bids, events of its receipts. rest counted as
market_foreign_dropped
Selection
- bid window: 10 min from RFP
createdAt (market.RFPBidWindow)
- bidder bids once per RFP (Bid object name = rkey)
- requester picks, recorded in
status.chosen.reason:
firstFree: first policy-allowed free bid ends the window (the normal case; fastest bidder wins)
bestWithinWindow: lowest cost after the window
policy: best within window, confirmed by the RFP's attached policy
resumed: restarted runner picked up a prior choice
status.bids keeps at most 20 {did, uri, offer, cost, verdict, arrivedAt}
Policies
- a market policy ALWAYS runs, both sides, both lanes (scope + full), every decision. no skip paths
- defaults:
bidder-tangled-vouch (bidder), requester-tangled-vouch (requester). open only when named, never prod
- tangled-vouch: counterparty must be vouched (
sh.tangled.graph.vouch) by the operator; both directions needed for a deal
- bidder full lane = own workflow + every policy the RFP attaches; allow only if all allow
- caching lives inside the engine run (
cache-key over workflow, mode, perspective, DIDs, records, args). never across parties, never a skip
- missing engine, evaluator or non-boolean
allow = deny
Market sequence
sequenceDiagram
autonumber
participant R as Requester
participant RP as requester PDS
participant FH as relay / jetstream
participant B as Bidder
participant BP as bidder PDS
participant VM as guest VM
R->>RP: putRecord compute.vm + market.rfp
RP->>FH: commit
FH->>B: market.rfp
B->>RP: getRecord rfp payload + policy
B->>B: bidder-tangled-vouch (scope + full)
B->>BP: market.bid + bids.free
B->>R: submitBid (ingress)
R->>R: requester-tangled-vouch, firstFree
R->>RP: market.accept
R->>B: submitAccept
B->>BP: market.receipt
B->>VM: ComputeVM to FirecrackerVM, boot user_data
VM->>B: POST /v1/events vm.onNetwork (iroh ticket, host keys)
B->>BP: market.event vm.onNetwork
B->>R: submitEvent
R->>R: ComputeContract Held
R->>VM: ssh / tunnels over iroh
R->>RP: market.event vm.delete
R->>B: submitEvent vm.delete
B->>VM: stop, remove
B->>BP: market.event vm.terminated
R->>R: ComputeContract Released
ComputeContract phases
Pending -> Requesting -> Held -> Releasing -> Released; or Failed
status.contract: strongRefs rfp/bid/accept/receipt, winner, vmAddress (iroh), hostKeys, networks, release, termination, timestamps
- every write lands before the flow moves on; a restarted runner resumes from the recorded stage
- release:
spec.releaseRequestedAt (SSA, release manager), outcome in status.release.requestedAt; retried 6x, 1 s doubling to 30 s
- a release whose vm.delete keeps failing settles Released once the bidder's vm.terminated appears
Guests
Boot
- the RFP flow is the ONLY way a guest comes up. no hand-started VM, no agent install outside cloud-init
user_data built by BuildUserData + module registry (internal/atproto/cloudinit)
- requester modules carry config only; bidder adds
setup-guest-binaries.service (internal/guestseed)
- binaries come from the bidder's GuestControlPlane
GET /v1/downloads/<sha256>/<name>, sha256 checked, installed to /usr/local/bin. never GitHub/GHCR from a guest
- downloads = release asset
socialweb-guest-downloads: opentunnel, wootty, atirc, socialweb-guest-agent
- opt-in
spec.guestTools: [socialweb-computer]: bidder serves its own binary (linux/amd64 only)
- guest image:
firecracker-x86_64, pinned by sha256 in internal/versions/versions.json, from guest-images/<target>/<sha256> in the public bucket
flowchart TD
A["Bidder: accept taken, receipt written"] --> B["ComputeVM created, owned by Bid"]
B --> C["FirecrackerVM: vmruntime Prepare"]
C --> D["socialweb-vmexec loads nft table swcguest"]
D --> E{"fence loaded?"}
E -->|"no"| X["VM does not start"]
E -->|"yes"| F["tap + pasta, firecracker boots"]
F --> G["cloud-init runs user_data"]
G --> H["setup-guest-binaries.service: fetch by sha256 from 192.0.2.1"]
G --> I["iroh module: endpoint up"]
I --> J["POST /v1/events vm.onNetwork: iroh ticket, host keys"]
J --> K["Bidder signs + publishes market.event"]
K --> L["Requester: contract Held"]
G --> M["setup-secrets.service: getSecrets, 90 s"]
M -->|"fail + Sealed fallback"| N["guest agent: guest.secretsRequested, sealed bundle"]
M --> O["secrets.ready"]
N --> O
O --> P["units After= secrets: wootty, atirc-agent env"]
Transports (three, never add one)
| transport | default | how |
| iroh | required, every guest | requester ssh + status.contract.vmAddress; contract Held on iroh address + host keys |
| opentunnel | opt-in per spec.tunnels[] | https URL per tag to guest 127.0.0.1:<port>; vm.onNetwork kind https; optional wootty shell |
| webrtc | opt-in per spec.tunnels[] | guest agent bridges datachannel tag to 127.0.0.1:<port>; STUN stun.cloudflare.com:3478, no TURN; run request proxy, ssh --transport webrtc |
- signaling sealed: age-x25519
guest.publicKey per tag, offer/answer sealed. no key, SDP, ICE or IP in any record, object or log
- browser shell: ssh over webrtc first, wootty fallback
Fence
- nf_tables bridge table
swcguest loaded before the tap exists (internal/guestnet)
- allowed from guest: TCP to
192.0.2.1 only on ComputeVM spec.hostPorts; DNS to pasta gateway; ICMPv6 RD/ND
- dropped: host's own addresses, guest link, RFC1918, CGNAT, loopback, link-local, multicast, ULA; every ethertype but IPv4/IPv6/ARP
lanUDP (default on): UDP 1024+ to LAN (not 1900/5353/5355) for same-LAN webrtc
hostUDP (default only with policy bidder-only-me): UDP to host's own addresses
- forwarded webrtc UDP port per ComputeVM from
webrtcUDPPorts (default 31000-31999)
- TCP to LAN or to host addresses: never
Secrets
spec.secretsRef: bundle [{path, value, mode}] in the requester's OpenBao
- primary: getSecrets on requester ingress, auth = guest workload identity token (bidder OpenBao identity engine via GuestControlPlane proxy, GuestIdentityGrant)
- fallback
secretsFallback: Sealed (browser page): only on demand, after guest.secretsRequested; sealed to the guest's key as ContractEvent secrets-<contract>
- never send a sealed bundle unasked
- condition
SecretsApplied reason GetSecrets or SealedFallback
- guest events: guest POSTs to GuestControlPlane, bidder signs + publishes; bidder never opens a sealed body
Identity And Auth
Where identity lives
- OAuth sessions, repo keys, relay keypairs, OIDC signing key, requester key: ONLY in the app's own OpenBao
- kcp objects hold references (SecretRef), never values
- the callee holds credentials; a bus handle never exposes a token
- atproto OAuth sessions held by the OpenBao atproto auth plugin (
auth/atproto session/<did>); refresh in the plugin
- plugin keeps a session on transient refresh errors (
invalid_client*, use_dpop_nonce, 429, 5xx, unreachable); expires on invalid_grant etc.
MarketIdentity modes
| mode | repo lives in | sign-in | used by |
localRepo | app's own Pds component | none (account created at install) | default bidder, dev requester |
pdsAccount | a PDS account (password in vault) | app password | market network accounts |
oauthQr | user's PDS | QR session-transfer at qr.fedfork.com (status.pendingAction.url, 5 min) | headless bidder with a real account (hat-7) |
oauthBrowser | user's PDS | authorize URL in status.pendingAction | CLI requester, browser page |
oauthDelegated | user's PDS | session handed in by another app | delegated flows |
oauthSessionProxy | user's PDS | reads a session another component stored in the vault | ssh gateway session owners, gateway cluster owners |
- scope upgrade: optional new scope (e.g.
rpc:chat.atirc.revoke) -> pendingAction {kind: signInAgain, reason: ScopeUpgrade}, identity stays Ready
- a required new 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
- sign-ins are scripted (
scripts/oauth-signin.ts, headless Chrome). no sign-in link handed to a person
Associations
- bidder/requester DID tied to operator: one
badgeBlueKeys record in the operator repo (challenge = operator DID, keyId = app DID, service bidder_associate / requester)
AtprotoRecordWatch/association verifies; MarketIdentity status.associations reports. no manual patch
- written by
scripts/associate-bidder.ts
- contract-scoped association (R2):
market.association at rkey requester_associate-<sha256(subject)[:32]>-<accept rkey>
Service auth
- atproto service auth JWT (
getServiceAuth, lxm + aud) for every cross-service call:
- k8s API: aud
did:web:k8s.socialweb.computer, lxm computer.socialweb.k8s.*
- atirc: aud
did:web:irc.fedfork.com#atirc, lxm chat.atirc.mint / chat.atirc.revoke
- did:key ingress (IngressProxy) carries submitBid/submitEvent callbacks for apps without a public host
ssh key claims
badgeBlueKeys at a stable keyRkey in the user's own repo, written via ssh.socialweb.computer /keys
- ssh authorizer: one getRecord per offered key. no listing
SSH Gateway
ssh <handle>@socialweb.computer <cmd> -> a VM rented AS that user, from that user's repo
- sign in once at https://ssh.socialweb.computer (OAuth, session in OpenBao
ssh-gateway)
- admission: policy
bidder-tangled-vouch scope lane, self = spec.ssh.operatorDid. refused = signed out, "this account was not admitted"
- admitted -> session owner MarketIdentity
owner-<stamp(did)> (oauthSessionProxy) -> Requester requester-<stamp(did)>
- cap
spec.ssh.maxAccounts (default 64)
- per-DID requester has
spec.contractOwnerDid: runs only that DID's ComputeContracts
- every session = a ComputeContract: transient
ssh-run-<hash> (TTL 1 h, released at command end unless LC_KEEP_VM) or held LC_SESSION ssh-<hash> (idle 5 min)
- disconnect (client close, 3 missed 15 s keepalives) -> cancel command -> revoke atirc token -> release contract
- restart: first sync releases stale
ssh-run- contracts, adopts held sessions still alive
sequenceDiagram
autonumber
participant U as ssh client
participant W as ssh.socialweb.computer
participant V as OpenBao ssh-gateway
participant S as SshServer :22
participant P as user PDS
participant R as Requester for DID
participant B as Bidders
participant VM as guest
participant A as atirc
U->>W: OAuth sign-in (once)
W->>W: policy bidder-tangled-vouch (operator)
W->>V: store session at auth/atproto session/DID
W->>R: EnsureSessionOwner, compose Requester
U->>S: ssh HANDLE@socialweb.computer CMD
S->>P: getRecord badgeBlueKeys at keyRkey
P-->>S: key bound to DID
S->>R: session as ComputeContract
R->>V: session via vault (oauthSessionProxy)
R->>P: compute.vm + market.rfp
B-->>R: bids
R->>R: requester-tangled-vouch, firstFree
R->>B: accept
B->>VM: boot, vm.onNetwork
R->>A: mint child token (if atirc cmd), leased
S->>VM: ssh hop over iroh dialer, run as agent user
U--xS: close / drop
S->>A: revoke token
S->>R: release contract
B->>VM: terminate, vm.terminated
atirc Integration
- atirc = atproto IRC at irc.fedfork.com; identities are subhandles
<sub>.<handle>
- tokens:
chat.atirc.mint with explicit exp (max 24 h), chat.atirc.revoke; revoking a subhandle revokes everything below it
- lease: a token minted for a contract is leased on the receipt + the bidder's
vm.terminated at-uri. dies at vm.terminated even if revoke missed
In a contract (spec.agent)
- requester mints after the receipt is verified, writes
ATIRC_TOKEN into the bundle at /etc/atirc/atirc.env
- user_data installs
atirc-agent.service, never starts it; atirc-agent-env.path try-restarts on env change
guest.agentStart (requester event) -> guest agent starts the unit
run request --agent, run request agent <name>
In an ssh session
- command starting
atirc (or LC_ATIRC=1) -> agent module in user_data; requester mints child ssh-<rfp rkey>.<handle>
- token reaches the command only via ssh env
LC_ATIRC_TOKEN -> process env. never argv, never a file
atirc agent run <sub> -> agent joins as <sub>.<handle>; mint failure fails the command
- runs as the agent user (named by
atirc nick, wheel, NOPASSWD), not root (claude refuses root)
Revoke before release
- order: revoke leased token BEFORE vm.delete, while the lease is alive; 401 "lease not alive" counts as revoked
- every mint recorded under
atircMinted in the bundle secret; background revoke with backoff (cap 5 min) until revoked or exp
status.atircRevokes: Done / Pending / Abandoned (24 h)
run request release waits up to 15 min (RequestRevokeWait) for revokes, exits non-zero on timeout and keeps state
Guest requester (R2)
spec.association {fromGuest}: guest runs its own requester (run request --guest-requester) as the agent user
- requester mints the R2 child token
r2-<accept rkey>.<handle> into /etc/socialweb-computer/r2-atirc.env
- R2 mints its own agents below that subhandle (
spec.atircParentRef)
- release cascades:
guest.r2Release -> guest requester releases its contracts -> guest.r2Released
Clients
atirc sub / atirc pub (agents, operators), atirc send / recv pipe (chat.atirc.pipe)
- web chat
/chat, AT-IRC iOS app (also launches VM agents through its embedded requester)
Tenant Kubernetes Clusters (Gateway)
- the one traditional kcp use: tenant k3s
--disable-agent control planes behind kcp
- per cluster: k3s over kine over its own postgres, cluster OpenBao (identity bundle), External Secrets pair, two ClusterLinks (dumbpipe iroh in front of apiserver and vault)
- blue/green control-plane slots a/b behind a per-cluster front (
controlplane.json)
- scale from zero: a pod -> NodeClaim -> one entry of the cluster's Infra
nodes-<hex> -> ComputeContract infra-nodes-<hex>-node-<hex> -> RFP flow -> VM boots as k3s node, joins over iroh
- delete the pod -> claim deleted -> contract released -> VM gone
- node network:
--flannel-backend=none, bridge CNI, socialweb-meshd (rust) cross-node, CoreDNS per node
- nodes published to the owner's repo:
computer.socialweb.infra/nodes-<hex> (no secrets, no user data)
- API:
computer.socialweb.k8s.kubectlApplyServerSide, getJob, subscribeClusters with service auth
flowchart LR
POD["pod applied"] --> NC["NodeClaim (cluster home workspace)"]
NC --> INF["Infra nodes-hex entry"]
INF --> CC["ComputeContract"]
CC --> RQ["gateway Requester: RFP flow"]
RQ --> BD["Bidder: VM with k3s node user_data"]
BD --> JOIN["node joins via ClusterLink iroh"]
JOIN --> RUN["pod runs"]
INF --> REC["owner repo: computer.socialweb.infra"]
Records Policy
- never delete user records. no sweep, retention or GC of people's repo records
- release deletes only the app's own contract-scoped records it wrote (e.g. its association)
- record growth pain = fix the read, never the data
- no
listRecords on a collection that grows without bound; one getRecord at a deterministic rkey
- stable rkeys:
- bid:
market.BidRKey per RFP
- offering:
self
- infra index:
computer.socialweb.infra/self
- association:
requester_associate-<sha256>-<accept rkey>
- ssh key claim:
keyRkey
listRecords remains only as a legacy fallback (e.g. a ComputeVM accepted before stable rkeys)
- progress fields write at most once a minute (no churn commits)
Deploy And Release
| artifact | how built | where installed |
| release binary | scripts/build-socialweb-computer.sh (clean checkout, every pinned asset, -tags "release embed_deno", CGO_ENABLED=0) -> ~/.tmp/socialweb-computer-build/release/<commit>/ + SHA256SUMS | gateway, bidders |
| dev binary | --dev --publish /var/tmp/<work> | own market network only |
| guest image | CI or scripts/build-guest-image.sh, content-addressed in the bucket | bidders fetch + verify |
| guest downloads | scripts/build-guest-downloads.sh | embedded, served by GuestControlPlane |
| browser page | kcp fork scripts/browser-dev/build-page.sh, tag page-rcN | shell.socialweb.computer |
- release candidates:
vX-rcN git tags on main (local tags); one short worktree per change
- gateway:
scripts/deploy.sh (ship, upgrade --binary --sha256, install gateway again, caddy sites). host socialweb.computer, ssh port 1997, account socialweb
- bidder: stage binary,
upgrade --binary <path> --sha256 <sha> as swc-bidder, then install bidder again
- upgrade: switches
bin/current, keeps bin/previous, core restarts and adopts every unit by name + InvocationID. VMs, state, OpenBao, DID, association survive
- rollback:
socialweb-computer rollback swaps the two binary slots
- fresh install keeps nothing: new DID, new association
- ingress relay:
scripts/deploy-xrpc-relay.sh; atproto relay: scripts/deploy-atproto-relay.sh (both health-check + auto rollback). never run atproto-reverse-proxy/scripts/deploy.sh (prints a token)
- before a rollout restarts bidders: tell testers, wait for a gap
- a new required OAuth scope forces re-sign-in of OAuth identities: line it up first
Burned schema revisions
- APIResourceSchema spec is immutable in kcp; a type change needs a Revision bump in
api/*/v1alpha1/resources.go
deploy/apiresourceschema-revisions.txt locks name -> sha256 forever
- a revision once applied to prod with one spec is burned: reusing its number with another spec (e.g. a reverted branch's build) makes kcp refuse the schema -> core crash loop
- fix: roll back, bump to a fresh revision, roll forward. details: KCP.md
Code Map
| path | holds |
cmd/socialweb-computer | the one binary: run, install, upgrade, rollback, dev, units, serve, internal ... |
cmd/socialweb-guest-agent, cmd/socialweb-vmexec, cmd/socialweb-node | guest agent, VM exec + fence, node tools |
cmd/socialweb-apigen | schema/export generator |
api/ | kcp API types (see KCP.md) |
internal/app/<root> | five composition roots + compose |
internal/controllers/<kind> | one reconciler per kind |
internal/services/<svc> | component XRPC/HTTP adapters |
internal/market/{bidder,requester,participant} | market logic |
internal/atproto/{cloudinit,ingress,relay,market} | user_data, ingress relay, relay, market records |
internal/bus, internal/component | contract handles + ComponentLinks; component runtime |
internal/stack/{componentcore,gatewaykcp,gatewayssh,bidder,request} | core wiring per profile |
internal/kcpserver, internal/kcpdatastore, internal/apiserverfront | kcp slots, kine, fronts |
internal/policyengine | policy engine + bundled actions |
internal/guestnet, internal/guestseed, internal/guestagent | fence, guest binaries, guest agent |
pkg/embedded | in-process requester for other modules (iOS) |
deploy/ | generated schemas/exports, sets, app manifest sets, market network |
lexicons/ | atproto lexicons |
rust/ | socialweb-meshd |
See Also