1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
# Quickstart (prod): Infra and compute against prod bidders
- the same Infra / EphemeralInfra objects as [quickstart-local.md](quickstart-local.md), rented from prod bidders with a real
atproto account
- prod means: prod bidders (each runs `bidder-tangled-vouch`), plc.directory, the did:key ingress relay at
xrpc.fedproxy.com, the relay at relay.mini-cloud-0002.chadig.com, irc.fedfork.com for atirc, a real atproto account
- what works today: the browser page (section 3), the CLI per contract (section 4), the agent join (section 5), tenant clusters
through the prod gateway (section 6), a long-lived requester from the CLI that `kubectl apply` works against (section 7,
`run requester`)
- also on main: that requester installed as a systemd unit of its own account (section 7a, `install requester`) or as a
HostProcess of an account's core (section 7b); the atirc agent from a local PDS or a pdsAccount identity is built
(section 5) and not yet proven on prod
- also on main: your own tenant cluster in the requester kcp (section 8, `run requester --with-gateway`, plan
[0039](plans/0039-long-lived-requester.yaml) item C-GATEWAY-COMPOSE), proven on a local network; a node from a remote
prod bidder is the lead's run
- acceptance: [acceptance/0012-infra-quickstart.md](acceptance/0012-infra-quickstart.md) "Prod Step For The Agent Join" and
"Prod Readings"; [acceptance/0013-mixed-prod.md](acceptance/0013-mixed-prod.md) for
[../examples/infra/mixed-prod.yaml](../examples/infra/mixed-prod.yaml) on a bidder you configured; the runbook rules of [acceptance/README.md](acceptance/README.md) apply (announce on atirc first, release
what you rent, no secret on any terminal)
```mermaid
flowchart TD
ACC["your atproto account<br/>(OAuth QR / browser, or app password)"] --> V["vouches: you -> bidder operator,<br/>bidder operator -> you"]
V --> PAGE["browser page shell.socialweb.computer<br/>kubectl terminal, page's own kcp"]
V --> CLI["CLI per contract<br/>run request --hold [--agent]"]
V --> GW["prod gateway XRPC<br/>createCluster / getKubeconfig"]
V --> LL["run requester<br/>long-lived kcp, ready file, kubectl apply"]
PAGE --> HELD["ComputeContract Held on a prod bidder"]
CLI --> HELD
LL --> HELD
GW --> K8S["tenant k3s, nodes rented through the gateway Infra"]
HELD --> AG["agent VM: requester mints at irc.fedfork.com,<br/>guest.agentStart, nick joins"]
```
## 1. Who Runs This
- you, with your own account, from your own host or browser; prod bidders and prod services are shared, so follow the
acceptance rules: one driver per account, announce before a run, release everything you rent
- never set `policy: open`: the default `requester-tangled-vouch` (requester) and `bidder-tangled-vouch` (every prod bidder)
always run
- credentials stay in owner-only files, sourced into the one command that needs them; never print a token, a password or a
sign-in URL
## 2. Vouches On Prod
Both policies read `sh.tangled.graph.vouch` records (rkey = the subject DID, `record.subject` = that DID, any `kind` but
`denounce`). On prod they are records in people's own repos, written with any atproto client (`goat record create`):
| direction | who writes | record |
|---|---|---|
| bidder operator -> you | the bidder operator, in the operator's repo | rkey = your DID |
| you -> bidder operator (or the bidder DID) | you, in your repo | rkey = the operator's DID |
- read a bidder's operator from its MarketIdentity `status.approvedAssociations`, or ask the operator
- the bidder runs the RFP's attached policy as itself before it bids, so without the operator's record you see
`no bids received within 30s` and, on the bidder, `policy rejected ... not vouched`
- verdicts are cached 5 minutes, denials too: write the records first
- check a record: `goat get "at://<operator did>/sh.tangled.graph.vouch/<your did>" | jq -r .kind` prints `vouch`
- when you operate the bidder yourself (`install bidder`, [bidder.md](bidder.md)), you write the operator record from the
operator account; [../examples/infra/vouch-bidder-operator.yaml](../examples/infra/vouch-bidder-operator.yaml) applies in
the bidder kcp of a bidder installed with `--bidder-operator-signin <operator handle>`: the operator signs in once
by QR (scope: `sh.tangled.graph.vouch` writes only) and the record is written to the operator's repo
([bidder.md](bidder.md), Operator sign-in)
## 3. Browser Page: kubectl Terminal
The page `shell.socialweb.computer` runs a kcp and the requester in the browser (comms Firehose, sealed secrets fallback).
Its kubectl terminal applies the same files into the page's own kcp ([acceptance/0009-browser-page.md](acceptance/0009-browser-page.md)).
1. sign in with your account (OAuth)
2. in the terminal, paste the file with a heredoc (the page terminal has no repo checkout, so `-f agent-vm.yaml` finds
no file): `kubectl apply -f - <<EOF` + the contents of `examples/infra/agent-vm.yaml` (or `vm.yaml`) + `EOF`
3. read `kubectl get ephemeralinfras.socialweb.computer -o yaml` until the contract is Deployed
4. the Agent button sends `guest.agentStart`; `ssh <contract>` opens a shell from the page terminal
5. `kubectl patch ... state Released` releases it
- proven on the page: `agent-vm.yaml` and `vm.yaml`, released by `kubectl patch ... state Released`; `mixed.yaml` (with
the DenoSandbox `sbx1`) and release by deleting the object are not yet proven on the page
- page-rc142 and later write the ssh key and the agent secrets bundle for a kubectl-applied contract; on page-rc141 an
`agent` contract failed at once and `ssh <contract>` had no key
- last pass: page-rc142, account hat, EphemeralInfra `qsprod`, Held in 41 s, `hat-<rkey>` joined, revoked, then Released
(acceptance 0012 "Prod Readings")
## 4. CLI Per Contract: run request
`socialweb-computer run request` brings up a kcp of its own for one invocation, composes a MarketRequester root, rents one
contract and keeps the invocation under `--state-dir` (runbooks [0003](acceptance/0003-cli-webrtc-ssh.md),
[0004](acceptance/0004-vm-atirc-agent.md)).
```bash
SWC=socialweb-computer
R=~/.tmp/req; mkdir -p "$R"; chmod 700 "$R"
NAME=qs-vm-$(date +%s)
$SWC run request --state-dir "$R" --name "$NAME" --hold \
--tunnel ssh:22:webrtc --transport webrtc --exec "uname -a; id" \
-- --atproto-oauth-qr > "$R/$NAME.log" 2>&1 &
# sign in from the pendingAction url in the log (QR or a scripted sign-in); never paste it anywhere
$SWC run request ssh "$NAME" --state-dir "$R" --transport webrtc --exec "uname -m"
$SWC run request release "$NAME" --state-dir "$R"
```
- identity: `-- --atproto-oauth-qr` (QR), `-- --atproto-oauth` (browser), or an existing account with an app password
(`-- --atproto-identity-mode pdsAccount --atproto-handle <handle> --atproto-pds-url <pds>`, the password in
`ATPROTO_PASSWORD` in the environment of that one command; `run request` has no `--atproto-password` option)
- `run request list`, `run request resume <name>`, `run request release <name>` manage the invocations it recorded
- one invocation is one contract; there is no `kubectl apply` of an Infra into it (section 7)
## 5. Agent VM: The atirc Join
The mint needs a requester identity irc.fedfork.com resolves and that signs service auth (lxm `chat.atirc.mint`, aud
`did:web:irc.fedfork.com#atirc`). irc.fedfork.com checks the JWT against the DID document from plc.directory, then the
handle: the first of up to 3 `alsoKnownAs` aliases whose DNS TXT `_atproto.<handle>` (else
`https://<handle>/.well-known/atproto-did`) names the DID. An agent token is leased, so irc.fedfork.com also reads the
receipt and the bidder's `vm.terminated` from the requester's PDS: that PDS must be reachable from the internet.
| identity | signs the mint with | log `requester_service_auth` `atircSigning` |
|---|---|---|
| OAuth (`--atproto-oauth-qr`, `--atproto-oauth`, the page, the iOS app) | the OAuth session; its granted scope holds `rpc:chat.atirc.mint` | `oauth` |
| existing account with an app password (`--atproto-identity-mode pdsAccount`) | the owner MarketIdentity (getServiceAuth on that account's PDS) | `owner-identity` |
| local PDS (`localRepo`) | its own Pds component, only when the DID is on `https://plc.directory` and the Pds is public through its ingress (a ComponentLink Requester -> Pds `pds.serviceauth`); otherwise none, with the reason in `atircReason` | `local-pds` or `none` |
- the market callbacks (submitBid, submitEvent, the RFP and accept signatures) are unchanged: this source signs only the
atirc mint
- a `spec.agent` contract whose requester handle is `.local` (the default `localRepo` handle
`<workspace>.requester.local`) or does not resolve to the requester DID is Failed before its RFP, with
`refused: the atirc agent needs a requester handle that resolves to <did>: ...; start the requester with
--atproto-handle <a domain you control> ...`; a plain VM contract of the same requester still runs
- a `spec.agent` contract whose `spec.secretsRef` path holds no bundle gets one from the requester: one entry at
`agent.envFile` (`ATIRC_TOKEN=`, mode 0600), logged `requester_agent_bundle_placeholder`; no bao step is needed (the
bao commands of [quickstart-local.md](quickstart-local.md) still work and win, a bundle that is there is kept). The path
must be under `contracts/` of the requester mount
```bash
NAME=qs-agent-$(date +%s)
$SWC run request --state-dir "$R" --name "$NAME" --hold --agent -- --atproto-oauth-qr > "$R/$NAME.log" 2>&1 &
# sign in, wait for Held and requester_atirc_minted in the log
$SWC run request agent "$NAME" --state-dir "$R" # guest.agentStart
# watch your channel with your own atirc token for the JOIN of <label>-<rkey>; prompt "<label>-<rkey>: uname -m"
$SWC run request release "$NAME" --state-dir "$R"
```
- the requester mints a leased token `<label>-<rfp rkey>.<handle>` after the verified receipt and writes it into the
bundle (`/etc/atirc/atirc.env`, 0600); release revokes it before the bidder writes `vm.terminated`
- last pass: CLI, account alice, `aliceoa-<rkey>` joined and answered `x86_64` over atirc; revoked before vm.delete
## 6. Tenant Kubernetes Clusters: The Prod Gateway
The prod gateway at `k8s.socialweb.computer` owns tenant clusters. With service auth from your account (runbook
[0001](acceptance/0001-auth-modes.md) section 3; `CL`, `W` and the `xrpc` shell function over `goat` come from its
"Variables" and "Service auth for the gateway" blocks, sourced first):
```bash
xrpc procedure createCluster name="$CL"
xrpc query listClusters | jq -r --arg n "$CL" '.clusters[] | select(.name==$n) | .phase' # until Ready
xrpc procedure getKubeconfig name="$CL" ttlSeconds:=28800 | jq -r .kubeconfig > "$W/kubeconfig"
chmod 600 "$W/kubeconfig"
kubectl --kubeconfig "$W/kubeconfig" apply -f examples/infra/k8s-cluster.yaml
```
- a pod that does not fit makes a NodeClaim; the gateway's Infra `nodes-<hex>` rents the node from a prod bidder
- your own tenant cluster without the prod gateway: section 8 (`run requester --with-gateway`)
## 7. A Long-Lived Requester From The CLI: run requester
`socialweb-computer run requester` (plan [0039](plans/0039-long-lived-requester.yaml) item A-RUN-REQUESTER) brings up the
same MarketRequester root as `run request` in a kcp of its own and keeps it up: no contract of its own, every listener on
port 0, state under `<state-dir>/requesters/<name>`. You `kubectl apply` Infra, EphemeralInfra or ComputeContract objects
against it, and its requester rents them from prod bidders under the default policy.
```bash
ST=~/.tmp/lr; mkdir -p "$ST"; chmod 700 "$ST"
$SWC run requester --name me --state-dir "$ST" -- --atproto-oauth-qr > "$ST/me.log" 2>&1 &
# sign in from the pendingAction url in the log (QR or scripts/oauth-signin.ts); never paste it anywhere
R=$ST/requesters/me/ready.json
until [ -s "$R" ]; do sleep 1; done # written atomically once the root is Ready
eval "$($SWC run requester env me --state-dir "$ST")" # KUBECONFIG of the requester workspace, the DID
kubectl get marketrequesters.socialweb.computer app -o jsonpath='{.status.phase} {.status.did}{"\n"}'
sed "s|ssh-ed25519 AAAAREPLACE-WITH-YOUR-PUBLIC-KEY you@laptop|$(cat "$(jq -r .sshPublicKey "$R")")|" \
examples/infra/vm.yaml | kubectl apply -f - # pin a bidder with bidderDids when you need one
kubectl wait infras.socialweb.computer/quickstart-vm --for=jsonpath='{.status.phase}'=Deployed --timeout=3m
$SWC run requester ssh me --state-dir "$ST" --contract infra-quickstart-vm-vm1 --exec 'cat /root/quickstart'
$SWC run requester ssh me --state-dir "$ST" --contract infra-quickstart-vm-vm1 --transport webrtc --exec 'uname -m'
kubectl delete infras.socialweb.computer quickstart-vm # releases the contract first
kill -INT %1 # Ctrl-C: stops the kcp, removes the ready file, keeps the state
```
- identity, after `--` (the MarketRequesterSpec options of `run request`, parsed by the same code): `--atproto-oauth-qr`,
`--atproto-oauth`, or an existing account with an app password (`--atproto-identity-mode pdsAccount --atproto-handle
<handle> --atproto-pds-url <pds>`, the password in `ATPROTO_PASSWORD` in the environment of that one command, which
puts it into the requester's own OpenBao, or later `run requester password <name>` on stdin); without one it is a
local PDS (`localRepo`,
`--identity <name>` before `--` reuses a stored one); comms, ingress, requester ingress and `--secrets-fallback` too. A
contract option (`--vm-name`, `--tunnel`, `--hold`, ...) is refused: contracts are kubectl objects here
- the ready file (`<dir>/ready.json`, and a second copy at `--ready-file <path>`): written to `<path>.tmp` then renamed,
mode 0600, once the root is Ready; rewritten on every restart and when a field changes; removed on a clean stop.
`{schema: 1, name, pid, startedAt, kcpUrl, kubeconfig, workspace, did, handle, identityMode, pdsUrl, ingress {pds,
requester}, sshPublicKey}`: the bound kcp front, the path of a kubeconfig for the requester's workspace, the public
ingress URLs from the Exposure status, the path of the requester's ssh public key; never a token, key or password. The
same JSON (plus mode, network, instance, readyFile, restarted, seconds) is the summary on stdout
- foreground only: it blocks until Ctrl-C (or SIGTERM), then stops its kcp and keeps its state; `--fg` is accepted (the
installed unit form). A second `run requester` on the same name is refused while the first holds `<dir>/owner.lock`
- restart: the same command (or the same command with nothing after `--`) adopts the units and keeps the DID, the vault
and every contract; a Held contract stays Held across the restart and ssh works again (the A-RUN-REQUESTER
reading below; fixed in rc158, proven on prod in acceptance 0013 A). `<dir>/requester.json` records
the first start's network, `--identity` and options; a restart with other options is refused (start a new name)
- `run requester env <name>` prints `KUBECONFIG`, `SOCIALWEB_KCP_HOST`, `SOCIALWEB_KCP_WORKSPACE`, the DID and the ssh public
key path from the ready file (`--json`); `run requester ssh <name> --contract <cc>` is the `run request ssh` code over
any Held contract, with the requester's own key and `StrictHostKeyChecking=yes` against the guest's reported host keys
(`--transport webrtc` needs a webrtc tunnel on the contract, as in `vm.yaml`)
- vouches: as section 2; the requester's own record for the bidder can be the `vouch.yaml` AtprotoRecord applied into
this kcp (its repo is `repo-<did with - for :>`)
- proven (2026-11-01, own market network, `--network`, bidder policy `bidder-tangled-vouch`): ready 11 s, `vm.yaml`
Deployed 20 s after the apply, ssh over iroh and webrtc printed `quickstart-vm-ok`; Ctrl-C and restart: same DID, ready
file rewritten in 1.4 s, contract still Held, ssh again. The standalone path against prod bidders with an OAuth QR
sign-in is the lead's run (acceptance rules)
### Ingress options per identity
Two ingresses, two jobs. Both go after `--` (rows of `deploy/apps/option-map.yaml`, app `market-requester`); the ready
file reports both public URLs (`ingress.pds`, `ingress.requester`).
- PDS ingress (`spec.ingress`, a local PDS only): makes the DID document's `atproto_pds` endpoint, the relay crawl, the
bidders' record reads and irc.fedfork.com's lease reads reach the local PDS
- requester ingress (`spec.requesterIngress`, every identity): submitBid, submitEvent and getSecrets; the agent's
`ATIRC_TOKEN` reaches the guest through getSecrets
| identity | PDS ingress | requester ingress | agent mint |
|---|---|---|---|
| OAuth QR / browser | none (the account's PDS is public) | as below | mints |
| pdsAccount (app password) | none (the account's PDS is public) | as below | mints through the owner MarketIdentity |
| localRepo (own Pds) | `--ingress-xrpc-host xrpc.fedproxy.com` (the did:key ingress relay; the standalone default, public URL `https://<did label>.xrpc.fedproxy.com`); `--ingress-provider opentunnel --ingress-opentunnel-command ... --ingress-opentunnel-route-name ...`; `--ingress-public-host <host>` (with `--ingress-relay-url <your relay>`) for `https://<did label>.<host>` behind your own edge | as below | mints only with the DID on plc.directory (standalone default `--plc-directory-url https://plc.directory`), that PDS ingress Ready, and `--atproto-handle <a domain you control>` whose DNS TXT `_atproto.<handle>` or `https://<handle>/.well-known/atproto-did` names the DID |
Requester ingress, every identity:
| option | effect |
|---|---|
| (none, comms `XRPC` or `Both`) | the app ingress (standalone: xrpc-subscription through xrpc.fedproxy.com) |
| `--requester-ingress-provider webrtc` (`--requester-ingress-webrtc-stun stun:...`) | bids, events and getSecrets over a webrtc datachannel |
| `--requester-ingress-provider xrpc-subscription`, `opentunnel` or `iroh` | the same endpoints through that provider |
| `--comms-mode Firehose` | no inbound endpoint at all: add `--secrets-fallback Sealed`, or a contract with `spec.secretsRef` (every agent contract) fails with "secrets need the requester ingress (getSecrets) or secretsFallback: Sealed" |
- a localRepo requester with a public handle, its PDS behind xrpc.fedproxy.com and the agent:
`$SWC run requester --name me --state-dir "$ST" -- --atproto-handle me.example.org` (the defaults give
plc.directory and xrpc.fedproxy.com); publish `did=<the ready file's did>` as DNS TXT `_atproto.me.example.org`
before the first agent contract; the core log shows `requester_service_auth ... "atircSigning":"local-pds"`
- proven (2026-11-01, own market network, localRepo with the default `.local` handle, bidder policy
`bidder-tangled-vouch`): `atircSigning` `none` ("the DID is on the network PLC"), no `pds.serviceauth` link;
`agent-vm.yaml` with no bao step: placeholder bundle written (`/etc/atirc/atirc.env`, 0600), contract Failed
`refused: ... --atproto-handle ...` with no RFP; `vm.yaml` of the same requester Deployed in 22 s, ssh printed
`quickstart-vm-ok`. A real mint from a local PDS is the lead's prod run
### 7a. Installed: install requester (a systemd user unit of its own account)
`install requester` (plan 0039 B-INSTALL-REQUESTER, decisions D2 and D9) is the one explicit root step, the same steps as
`install bidder`: it creates the account, writes the units, `config.json` and the requester record, enables linger and
starts the unit. The unit runs the same process, `run requester --name %i --fg`, with `host-backend systemd-user` from
`config.json`: its core places the requester's kine, front and kcp slot as user units of that account
(`socialweb-kcp-req-<sha256(name)[:10]>-*`) and its HostProcesses and OpenBao beside them, all in `socialweb.slice`.
```bash
sudo $SWC install requester --name me --dry-run # prints every step; writes nothing
sudo $SWC install requester --name me # account swc-requester, unit socialweb-computer-requester@me
sudo $SWC install requester --account swc-req-t1 --name me -- --atproto-oauth-qr # name the account; options after --
ST=/var/lib/socialweb-computer/swc-requester
sudo -u swc-requester $ST/bin/current/socialweb-computer run requester signin me --state-dir $ST # the sign-in URL on stdout
sudo journalctl _UID=$(id -u swc-requester) _SYSTEMD_USER_UNIT=socialweb-computer-requester@me.service -f # its log, as root
R=/var/lib/socialweb-computer/swc-requester/requesters/me/ready.json
sudo -u swc-requester kubectl --kubeconfig "$(sudo jq -r .kubeconfig "$R")" get marketrequesters.socialweb.computer app
```
- account: `swc-requester` by default; `--account <name>` creates that account when no install record and no user of that
name exist; an existing account needs `--adopt-account` (refused otherwise, as for a bidder). The install record is
`/var/lib/socialweb-computer/install-<account>.json` with role `requester` and the names
- `--name` (repeatable, comma-separated) names each instance; each is the unit `socialweb-computer-requester@<name>.service`
(Type=notify, Slice=socialweb.slice, Restart=always, Delegate=yes) with its state in `<state>/requesters/<name>`. The
unit sends READY=1 only after the ready file is written, so `TimeoutStartSec` is the ready budget plus one hour for a
person to sign in; a start that times out restarts and shows a new sign-in URL. `install` waits for that start
- options after `--` are the MarketRequesterSpec options of `run requester`, parsed by the same code, written once per
name into `<state>/requesters/<name>/requester.json` (the first start's options; a second install keeps the file and
reports it FOUND). With no identity option the identity is `--atproto-oauth-qr` (D9): `run requester signin <name>` (as
the account or root) reads the MarketIdentity `requester` from the requester's own kcp and prints its
`pendingAction.url` on stdout (phase and expiry on stderr, `--json` for the object; nothing when Ready); drive it with
`scripts/oauth-signin.ts --url "$(... run requester signin me ...)"`. `install` prints both commands before it starts
the unit. The unit's log has the same URL, but only root reads it: the account is a system account (uid under 1000),
journald stores its user units' lines in the system journal, and `journalctl --user` as the account opens no file;
as root, `journalctl --user-unit` matches root's own uid, so name `_UID=<uid> _SYSTEMD_USER_UNIT=<unit>`
- an existing account with an app password (`-- --atproto-identity-mode pdsAccount --atproto-handle <h> --atproto-pds-url
<pds>`): the password never goes into `config.json`, a flag, a unit or the requester record. After the unit started, as
the account, `socialweb-computer run requester password me --state-dir <state> < <file>` reads it once from stdin and writes
it into the requester's own OpenBao (`identity/requester/keys`, the SecretRef the MarketIdentity's keysRef names); the
composer, waiting on "its account password", goes on
- `config.json`: `host-backend systemd-user`, `state-dir`, `asset-cache`, `ready-timeout`, `network-timeout`
(and `unit-prefix` when given); never a password or a token
- second install: every step FOUND; `upgrade --binary <new> --sha256 <sum> --state-dir <state>` as the account switches
`bin/current`, runs the new binary's `check-host -role requester` (the role comes from the install record, never read as
a bidder) and restarts every enabled `socialweb-computer-requester@` unit; `rollback` the same way. The kcp units and the
vault keep running across that restart; the core adopts them, so the DID and every Held contract survive
- reboot: linger starts the user manager, and the enabled kcp units and the requester unit come up again
- `uninstall requester --account <a>` (root) disables and stops the units, removes the unit files, linger and the drop-in,
and keeps the state dir (the DID, the vault, the contracts) unless `--purge`
- `check-host -role requester`: the checks every role has (systemd, delegation, linger, cgroup v2, socket path, boot id,
oomd); no kvm, no subids
- proven (2026-11-01, this host as root, throwaway account `swc-req-t1`, localRepo against an own market network's PLC):
`--dry-run` printed 33 steps; the install created the account and was active 25 s after `enable --now`, ready file
and `kubectl get marketrequesters` Ready as the account; with no identity option the identity was oauthQr, AwaitingScan
with a pendingAction URL, and the unit stayed activating (no READY before the ready file); a second install was all
FOUND; `upgrade` as the account read the role from the record, restarted only the requester unit, kept the DID and left
the kcp and OpenBao units adopted (same InvocationID); a restart of `user@<uid>` (the reboot path through linger) was
Ready again in 10 s with the same DID; `run requester password` wrote the vault path; `uninstall` kept the state dir
and the account, `--purge` removed both. `install bidder --dry-run` and `install gateway --dry-run` print the same
steps before and after this change. A real reboot and an OAuth QR install against prod bidders are the lead's runs
### 7b. Same account: a kubectl-applied HostProcess (no root, no new user)
When the account already runs a core (an installed requester, or a dev host instance), a second requester needs no root
and no new account: apply [`examples/infra/requester-hostprocess.yaml`](../examples/infra/requester-hostprocess.yaml) into
that core's kcp. The core places it as one more unit (placement supervisor) in `socialweb.slice`; it runs the same process,
`run requester --name lr2 --state-dir <dir> --fg`, which brings up its own kcp under that unit.
```bash
A=swc-requester; ST=/var/lib/socialweb-computer/$A # the account whose core places it
K=$(sudo jq -r .kubeconfig "$ST/requesters/me/ready.json") # that core's kcp (the installed requester me)
P=socialweb-hp-req-$(printf %s me | sha256sum | cut -c1-10)- # the HostProcess prefix of the installed requester me
sed -e "s|REPLACE-HP-PREFIX|$P|" -e "s|REPLACE-STATE-DIR|$ST/lr2|g" examples/infra/requester-hostprocess.yaml \
| sudo -u $A kubectl --kubeconfig "$K" apply -f -
sudo -u $A kubectl --kubeconfig "$K" get hostprocess requester-lr2 -o jsonpath='{.status.phase}{"\n"}' # Running once the ready file is written
sudo -u $A kubectl --kubeconfig "$K" delete hostprocess requester-lr2 # SIGTERM: stops its kcp and keeps its state dir
```
- every command runs as the account (`sudo -u $A`): the kubeconfig is 0600 to it, and the unit runs as it, so the
state dir must be one it can write (here under its own state dir). As a normal account that runs its own core, drop
`sudo -u $A` and `sudo`, and use a dir of that account for `REPLACE-STATE-DIR`
- the unit must start with the core's HostProcess prefix (`<unit-prefix>hp-<key>-`; an installed requester's key is
`req-<sha256(name)[:10]>`, a dev instance's is its short id: read it from `kubectl get hostprocesses`), or the core
refuses it ("is not under")
- no root prunes it: a root renders and checks only its own set's HostProcesses by name (`compose.SetWorkloads`,
`compose.CheckWorkloads`), adopts only objects labelled with its owner, and `status.gc` only counts an unowned
HostProcess as an orphan; it never deletes one. The core adopts the unit by name and InvocationID across its own restart
- the sign-in URL: `run requester signin lr2 --state-dir <dir>`, or the unit's journal (`journalctl --user -u <unit>` for a
normal account; for a system account such as swc-requester only root reads it, `journalctl _UID=<uid>
_SYSTEMD_USER_UNIT=<unit>`); a password never goes into the argv
- proven (2026-11-01, as test-0001, own market network, localRepo against the network PLC): applied into the core of an
installed-form requester, the HostProcess was Running with its ready file after 18 s, in `socialweb.slice`, and its
kubeconfig read the MarketRequester Ready; a restart of that core left it adopted (same InvocationID) with the core's
kcp, kine and OpenBao units; `kubectl delete` stopped it in under 1 s, removed the ready file and kept its state dir
Still planned in plan 0039:
- its live pass: [../examples/infra/mixed-prod.yaml](../examples/infra/mixed-prod.yaml) (vm1, sbx1, agent1 pinned to one
bidder, no policy field) by acceptance [acceptance/0013-mixed-prod.md](acceptance/0013-mixed-prod.md) (item
ACC39-MIXED-PROD; written, not yet run on prod)
## 8. Your Own Tenant Cluster: run requester --with-gateway
`run requester --with-gateway` (plan 0039 item C-GATEWAY-COMPOSE) composes the gateway into the requester's own kcp. One
identity: the MarketRequester's Requester rents the cluster's nodes under your DID and the default policy; there is no
second sign-in, no gateway PDS, no gateway Requester and no web app.
```bash
$SWC run requester --name me --state-dir "$ST" --with-gateway -- --atproto-oauth-qr > "$ST/me.log" 2>&1 &
until jq -e .gateway "$ST/requesters/me/ready.json" >/dev/null 2>&1; do sleep 1; done
$SWC run requester cluster me --cluster c1 --state-dir "$ST" # account + home workspace and the Cluster
until TK=$($SWC run requester kubeconfig me --cluster c1 --state-dir "$ST" 2>/dev/null); do sleep 2; done # once the Cluster is Ready
kubectl --kubeconfig "$TK" apply -f examples/infra/k8s-cluster.yaml
kubectl --kubeconfig "$TK" wait --for=condition=Ready pod -l app=quickstart-k8s --timeout=5m
kubectl --kubeconfig "$TK" logs deploy/quickstart-k8s # quickstart-k8s-ok
kubectl get infras.socialweb.computer # nodes-<hex>, in the requester workspace
kubectl --kubeconfig "$TK" delete deployment quickstart-k8s # the node scales to zero, its contract Released
```
- what it composes: a Gateway root `gateway` of profile `requester` with `spec.requesterRef` naming the MarketRequester
`app`, in the requester's own workspace. It composes no PlanAccount, Pds, Requester, identity or web app of its own, only
a nodes-only GatewayAPI (`spec.nodesOnly`): the NodeClaim binding and the meshd and node-agent downloads, nothing else
is served. The NodeClaim controller writes the cluster's Infra `nodes-<hex>` into the requester workspace, the
requester runs its ComputeContracts as your DID with `requester-tangled-vouch`, the node join bundles go to the
requester's own vault (`requester/contracts/<contract>/bundle`, served by getSecrets), and the Infra record mirrors to
your repo (`computer.socialweb.infra/nodes-<hex>`, Retain: never deleted)
- the cluster provider runs in the `run requester` process and serves only the clusters of your DID; in a local
market network `--with-gateway` is refused when the network has a gateway child (`dev up --with-gateway`), whose
provider serves every Cluster of the network's kcp
- the cluster's k3s, kine, OpenBao, front and links are
HostProcesses of the requester's core (`kubectl get hostprocesses.socialweb.computer`), with state under
`<instance state>/clusterprovider` (the ready file's `gateway.clusters`)
- a Cluster lives in a home workspace `root:<account>:<cluster>` of your DID: `run requester cluster` makes the account
workspace, its AtprotoIdentity, the home workspace and the Cluster `{account: <did>, running: true}`; delete the
Cluster there to tear it down
- the ready file is written only once the Gateway is Ready too, with `gateway {workspace, name, clusters}` in its first
write, so `run requester cluster` right after `until [ -s ready.json ]` finds it; `requester.json` records
`withGateway`, so a restart keeps the gateway and the clusters
- preflight: cgroup v2, user manager delegation, one subuid/subgid range and setuid newuidmap/newgidmap (the
`check-host -role gateway` fixes); a dev build or a release build (k3s and external-secrets are embedded)
- proven (2026-11-01, own market network, `--network`, bidder policy `bidder-tangled-vouch`, both vouches): Gateway
Ready 3 s after the requester, `run requester cluster` Ready in 17 s with 11 HostProcesses in the requester workspace,
the NodeClaim's Infra `nodes-<hex>` in `root:<requester>` and its contract Held in 21 s (owner DID, attached
`requester-tangled-vouch`, SecretsApplied GetSecrets), the pod logged `quickstart-k8s-ok`, the node scaled to zero and
the contract Released 2.5 min after the Deployment was deleted, the Infra record stayed in the repo; Ctrl-C and
restart with nothing after `--`: the same DID, the gateway Ready 9 s after the start, the Cluster still Ready, k3s
`/readyz` ok, every cluster HostProcess adopted by unit name and InvocationID but the cluster OpenBao (restarted in
place). The first node join of a fresh network waited about 4 min for the bidder's guest OIDC discovery to report its
configured issuer (it reported `https://127.0.0.1:0` until then)
- a node on a remote bidder: the node fetches `socialweb-meshd` and `socialweb-node` from
`<base>/downloads/<sha256>/<name>` and checks the sha256 its contract pins (`spec.nodeJoin.meshd`, `nodeAgent`).
The base is `--node-download-base` (env `SOCIALWEB_NODE_DOWNLOAD_BASE`), by default the public bucket the bidders
already read guest images from, where CI publishes the pinned downloads tree by sha256 (the `publish` job, prefix
`downloads/`, content-addressed: an object is never replaced). With `--network` the default is unused and the node
fetches from the requester's local Exposure, as the network's bidders share its host; a dev build whose `rust/` or
`cmd/socialweb-node` differs from a published commit has pins the bucket does not hold. Proven on a local network
(2026-11-01): with the flag the node contract's `nodeJoin` names the bucket and the pins of the released tree, the
node's bootstrap reached `<bucket>/downloads/<sha256>/socialweb-node-x86_64` (HTTP 404 until CI publishes it), and a
guest fetched a published object from the bucket and matched its manifest sha256. Proven on prod (rc159, acceptance
0013 C): the node of cluster `c1` on hat-7 (a remote bidder) fetched both from the bucket and joined; the pod ran
- the node reaches the k3s API and the cluster OpenBao over iroh: each is a dumbpipe `listen-tcp` HostProcess of the
requester's core whose ticket names the public n0 relay and the host's addresses, so a node on any host dials it
through the relay when no direct path opens (a guest cannot reach its own bidder host's addresses through the fence,
so the same-host proof already takes an internet path); the node's join bundle comes by getSecrets over the
requester ingress, and the cluster OpenBao checks the node's token against the bidder's public OIDC issuer
## Not Supported On Prod Today
| gap | where it is planned |
|---|---|
| an atirc agent from a local PDS identity: built, not yet proven against irc.fedfork.com (a pdsAccount identity is proven on rc161) | plan 0039 D-AGENT-IDENTITY |
| `vouch-bidder-operator.yaml` on an installed bidder whose operator did not set `--bidder-operator-signin`, or before the QR page (qr.fedfork.com) asks for `repo:sh.tangled.graph.vouch` | operator setting; the QR page's scope |
| deno on a prod bidder unless its operator ran `run bidder --compute-kinds vm,deno` or `install bidder --compute-kinds vm,deno`; pin such a bidder with `bidderDids` | operator setting |