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
# Acceptance 0002: ssh gateway, per-user OAuth, atirc agent over ssh
Proves `ssh <handle>@socialweb.computer atirc agent run <name>` rents a market VM
**as that user**, starts an atirc agent in it as `<name>.<handle>`, and on session
end revokes the token before the contract is released.
- Gateway: Go `SshServer` + one per-user `Requester` per signed-in DID (`requester-<stamp(did)>`)
- Port 22 on `socialweb.computer` = Go ssh gateway (legacy deno gateway disabled, kept for rollback)
- Web half: `https://ssh.socialweb.computer` (sign-in + `/keys`)
- Last pass: rc152, 2026-11-01 (owner account, port 22); rc154 restart check 2026-11-01
```mermaid
sequenceDiagram
participant U as user ssh client
participant G as ssh gateway (SshServer)
participant R as per-user Requester (user's OAuth session)
participant B as bidder (local / hat-9 / hat-7)
participant V as guest VM
participant A as atirc (irc.fedfork.com)
U->>G: ssh <handle>@socialweb.computer atirc agent run <name>
G->>G: key claim check (one getRecord, stable rkey)
G->>R: bus resolve requester of account DID
R->>B: RFP (policy: requester-tangled-vouch)
B-->>R: bid (policy: bidder-tangled-vouch) -> accept -> receipt
R->>A: chat.atirc.mint <name>.<handle> leased {receipt, vm.terminated}
R->>V: guest session (iroh), run `atirc agent run` as guest user <name>
V->>A: nick <name> joins channel
U--xG: disconnect / session cap
R->>A: chat.atirc.revoke <name>.<handle>
R->>B: release -> vm.terminated
```
## Preconditions
| Item | Need |
| --- | --- |
| Account | atproto handle, signed in once at `https://ssh.socialweb.computer` (session lives in gateway OpenBao; a deno-era session does not carry over) |
| ssh key | `com.publicdomainrelay.temp.badgeBlueKeys` key claim in the user's own repo at the **stable rkey** (`keyRkey(did, key)`); `/keys` lists it |
| Vouch, bidder side | `bidder-tangled-vouch`: a bidder operator vouches the requester: `sh.tangled.graph.vouch` record, rkey = requester DID, in the operator repo |
| Vouch, requester side | `requester-tangled-vouch`: the requester vouches the bidder operator (other direction) |
| Vouch cache | bidder and requester cache verdicts up to 5 min: after writing a vouch wait 5 min before retry |
| Versions | gateway + ssh gateway >= rc152 (revoke on disconnect), >= rc153 (held sessions survive restart), >= rc154 (vault wait, 0 restarts) |
| Watchers | gateway agent (gw kcp + gw ssh lines), host watcher per bidder (see [0008](0008-host-observer.md)) |
Vouch pairs used on prod (public test accounts): bob -> john, john -> bob, bob -> hat, hat -> bob, john -> hat.
Check a vouch (`goat` or any getRecord client):
```bash
OP=<bidder operator DID>; REQ=<requester DID>
goat get "at://$OP/sh.tangled.graph.vouch/$REQ" | jq -r .kind # vouch
goat get "at://$REQ/sh.tangled.graph.vouch/$OP" | jq -r .kind # vouch
```
## Steps
1. Announce on atirc: `linux-kcp -> all: ssh gateway test as <handle> now; watchers: where does it land?`
2. New host key once after the deno -> Go switch: `ssh-keygen -R socialweb.computer`
3. Transient run (VM lives for the command):
```bash
ssh <handle>@socialweb.computer atirc agent run <name>
```
4. Held run (session name pinned, survives a gateway restart until expiry):
```bash
ssh -o SetEnv=LC_SESSION=<session-name> <handle>@socialweb.computer atirc agent run <name>
```
Other session env: `LC_VM_NAME` (DNS label), `LC_POLICY` / `LC_POLICY_ARGS` (`none`/`off` refused: a policy always runs), `LC_KEEP_VM`.
5. Side port (staging a new gateway build before it owns 22): `ssh -p 2222 <handle>@socialweb.computer ...`
6. Wait for the nick `<name>` to join the owner channel; address it once (`<name>: say hi`), see one reply.
7. Disconnect (Ctrl-C / close) or let the session cap end it.
## Expected observations
Gateway core log (`gw ssh:` lines on atirc from the gateway agent), in order:
| msg | fields | owner run 2026-11-01 |
| --- | --- | --- |
| `session_started` | did, sessionId | 00:05:34 |
| `requester_bidder_discovery` | candidates=17 bidders=3 | 00:05:47 (+13 s) |
| contract `Held` (gw kcp line, ComputeContract `ssh-run-<hash>`) | winner DID | ~00:06:30 |
| `requester_session_atirc_minted` | subhandle=`<name>.<handle>` | 00:06:33 |
| `guest_session_started` | target | 00:06:34 |
| nick joins channel | | ~00:06:40 |
| `guest_session_terminated` | phase=session | 01:05:33.88 (1 h cap) |
| `requester_session_atirc_revoked` | subhandle | 01:05:33-35 |
| ComputeContract `Releasing` -> `Released`, then DELETED | | 01:05:35 -> 01:05:37 |
- Session start -> agent nick on channel: ~60-70 s (hat run rc151: mint 23:21:34 -> JOIN 23:22:02)
- Disconnect -> Released: ~2-4 s
- atirc side (`revocations.ndjson`): revoke entry **before** contract Released
- Bidder side: `vm.terminated` pinned; ssh-path records may carry **no `request` field** (see 0008)
Gateway restart check (rollout):
- `SshServer ssh-server phase=Ready`, `SshGateway app phase=Ready comps=4/4`
- per-user MarketIdentity `owner-<hash>` `phase=Ready ready=SignedIn` for every signed-in user
- SshServer + requester restarts = 0 (rc154: `requester_signer_key_waits_for_vault`, `sshserver_waits_for_vault` -> `sshserver_vault_ready` -> `signer_key_bound`)
## Pass criteria
- VM rented as the ssh user (ComputeContract owner DID = user DID), not a shared requester
- `<name>.<handle>` minted leased, agent runs as guest user `<name>` (not root), joins and replies
- On disconnect / cap: revoke strictly before Released; zero live tokens for the user afterwards
- Winning host empty after release (watcher line)
- Unvouched DID or missing key claim: refused, no RFP
## Cleanup
- Nothing if the session ended. A held `LC_SESSION` session: reconnect with the same name and exit, or wait for expiry
- Leftover VM: bidder-side termination ([0007](0007-bidder-termination.md))
- Plain (unleased) test token left: revoke it (`atirc revoke` with the parent token) or ask atirc-dev for the manual revocation path
## Known pitfalls
| Symptom | Cause / fix |
| --- | --- |
| stderr `sign in at https://ssh.socialweb.computer first`, exit 1 | per-user requester missing / not Ready: sign in on the web page |
| `OAuthSessionExpiredError: refresh token already consumed` | stale session: sign in again on `ssh.socialweb.computer` |
| all bids denied, bidders' policy runs Succeeded | requester side: user does not vouch the bidder operator |
| no bid at all | bidder side: no operator vouches the user; or 5-min verdict cache still holds the old deny |
| `atirc agent run x` -> `Forbidden` | child subhandle not below the session token; fixed rc150 (gateway mints `<name>.<handle>` directly) |
| harness refuses `--dangerously-skip-permissions` as root | fixed rc151: agent runs as guest user `<name>` |
| key accepted on old TID rkey, refused by new check | copy the key claim to its stable rkey (nothing deleted) |
| token alive after session end | rc151 gap (no end-of-command revoke); fixed rc152; lease kills it anyway once `vm.terminated` exists |
| host key warning on 22 | expected once after the deno -> Go switch: `ssh-keygen -R socialweb.computer` |
| Go gateway rented every VM as one identity | design regression caught 2026-10-31; per-user OAuth is the rule, never a shared requester |