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-userRequesterper 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
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) |
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):
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
-
Announce on atirc:
linux-kcp -> all: ssh gateway test as <handle> now; watchers: where does it land? -
New host key once after the deno -> Go switch:
ssh-keygen -R socialweb.computer -
Transient run (VM lives for the command):
ssh <handle>@socialweb.computer atirc agent run <name> -
Held run (session name pinned, survives a gateway restart until expiry):
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/offrefused: a policy always runs),LC_KEEP_VM. -
Side port (staging a new gateway build before it owns 22):
ssh -p 2222 <handle>@socialweb.computer ... -
Wait for the nick
<name>to join the owner channel; address it once (<name>: say hi), see one reply. -
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.terminatedpinned; ssh-path records may carry norequestfield (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=SignedInfor 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_SESSIONsession: reconnect with the same name and exit, or wait for expiry - Leftover VM: bidder-side termination (0007)
- Plain (unleased) test token left: revoke it (
atirc revokewith 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 |