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
plan: 0014-atproto-identity-in-openbao
written: 2026-10-12
revised: >
2026-10-12, first version. Written from the user's question of 2026-10-12: "Shouldn't we be persisting oauth
sessions for users who log into k8s.socialweb.computer somewhere like whatever the appropriate scope openbao
is? In fact, would it make sense to develop an openbao plugin for atproto oauth so that we could get a
seamless experience there as far as our architecture goes?" The open question of kubectl without the
socialweb-credential binary is in this plan too (M3-KUBECTL-CLIENT-CERT).
2026-10-12, second revision, with the user's answers: bao signs DPoP proofs (M1-SESSION-CUSTODY stands);
kubectl gets a kubeconfig download plus an XRPC that a user or automation calls to get the same kubeconfig
(M3-KUBECTL-CLIENT-CERT rewritten: pki/issue, the key made by bao and returned once); bidders keep prod-qr
(M2-BIDDER-IDENTITY-BAO dropped); the plan starts right after plan 0009 M3-SSHSIG-AUTH-PLUGIN. The lead
decided the other questions (the user may veto): the cookie, the exec-plugin path deleted once the XRPC
works in prod (new item M3-RETIRE-EXEC-PLUGIN), certificate ttl 8 h default and 24 h max, upstream later.
queued: >
In the current /goal, this plan starts right after plan 0009 M3-SSHSIG-AUTH-PLUGIN (decided by the user
2026-10-12). That item adds OpenBaoServer spec.plugins and the build, pin and catalog registration of a
plugin asset; this plan uses the same machinery for a second plugin. The gateway requester policy of
M2-GATEWAY-SCOPE changes again when plan 0009 M3-REQUESTER-CONTROLLER lands; each item's depends_on governs.
source: >
Checked on main 2026-10-12. The OAuth sessions of the users of k8s.socialweb.computer are ALREADY in OpenBao.
The web app (socialweb-computer-k8s/hono-socialweb-computer-k8s/mod.ts, accountState) keeps them in the
gateway's root vault (OpenBaoServer root-vault, client socialweb-computer-k8s, kv mount socialweb-computer-k8s,
path oauth-sessions/<did>) through socialweb-computer-oauth-session-vault, and writes a MarketIdentity per
signed-in DID (sessionRef = that path). The sessions survived the upgrade of 2026-10-12. They were lost only
on the fresh deploy, which removes all prod state by design. Without --openbao-server the web app falls back
to a file (socialweb-computer-oauth-session-fs, <state>/oauth-sessions.json).
What is not good: OpenBao is only a store. The OAuth client is public (token_endpoint_auth_method none). The
browser runs the whole OAuth flow, holds the DPoP private key, and posts the full session (DPoP private JWK,
access token, refresh token) to the web app's POST /session. The web app process holds the DPoP private key
and the refresh token in memory and does the refresh itself (socialweb-computer-requester-inproc, one
refresher per account to protect the single-use refresh token). Any process that can read the kv path can
copy a session and use it from anywhere, for as long as the refresh token lives.
The bidder has a second atproto OAuth session: the prod-qr MarketIdentity sign-in (session transfer through
qr.fedfork.com, ts/atproto-market/lib/market-identity-atproto), kept in the bidder's own vault (OpenBaoServer
bidder). The user keeps it as it is.
Today kubectl needs cmd/socialweb-credential: the gateway's XRPC com.publicdomainrelay.temp.cluster.getKubeconfig
(internal/xrpcserver, Go; atproto service auth checked by internal/atprotoauth with lxm = the method and aud
in SOCIALWEB_GATEWAY_AUDIENCES) returns a kubeconfig (internal/devicekey, internal/kubeconfig) with an exec
plugin that trades a device credential at the mint URL (/v1/credential, internal/mint) for a short-lived
token. The web app calls that XRPC for the user with a service JWT. The per-cluster OpenBao already has a pki
mount with a client-cert role (internal/clusterprovider/vaultdeclared.go, VaultPKIRoleTenant) and the
cluster's ClientCA (ImportClientCA).
principle:
stated: >
A credential that can act as a user (an atproto DPoP private key, a refresh token, the OAuth client key) is
made in, lives in and ends in the OpenBao of the application that the user signed in to. No process outside
that OpenBao ever holds it. OpenBao is the authority for atproto sign-in, the same way plan 0009 made it the
authority for guest identity (route B). Processes get only short-lived, narrow things from it: a bao token,
an access token with one DPoP proof, a short-lived client certificate.
rules:
- "No path of the plugin returns a DPoP private key, a refresh token or the client private key. There is no export path."
- "Each application signs its users in to its own OpenBaoServer (user decision 2026-10-12: OAuth sessions and every key live in the app's own OpenBao, never kcp Secrets). Two applications never share a session store."
- "TypeScript keeps the atproto market flow. Go owns bao state: the plugin, its config, roles and policies, applied by the openbao controller from kcp objects (the GuestIdentityGrant pattern of plan 0009 M3-RBAC-TO-OPENBAO)."
- "No token, key or session appears in a kcp object, a log line, a DenoPod argument or an atproto record. kcp holds only references and status. A client certificate key made by bao is returned once and stored nowhere."
- "Fail fast: every outbound call of the plugin has a short timeout (5 s per call, 15 s for a whole login callback); a failed refresh marks the session Expired at once and says why in MarketIdentity status."
state_rules:
- "No tests until prod works (user, 2026-10-12). Each item's done_when is a manual live reading in the item's own market network. The integration tests of M4-INTEGRATION-TESTS come after prod works, on the plan 0010 G2-IT-HARNESS."
- "Real names only (AGENTS.md). Commit and push only socialweb-computer-kcp; the lead flips status."
- "Every new TypeScript package follows the ABC layering (lib/common, lib/abc, implementation, hono-factory, CLI)."
- "No migration and no back-compat (user, 2026-10-12): the next fresh deploy starts with the new store. Prod is disposable."
ownership:
repository: socialweb-computer-kcp
go: "cmd/openbao-plugin-auth-atproto (new), internal/openbao and internal/openbaoctl (plugin config, roles, policies), internal/versions/versions.json, api (OpenBaoServer spec, ClusterClientCertificate), internal/xrpcserver and internal/gateway (getKubeconfig), internal/clusterprovider (per-cluster pki role), internal/tlsfront and internal/stack/gatewaykcp (client certificate check at the front)"
typescript: "ts/atproto-market/lib (session custody interface and its OpenBao implementation), socialweb-computer-k8s (sign-in, cookie, kubeconfig download), ts/socialweb-computer-ssh (same sign-in)"
research:
- "atproto OAuth (atproto.com/specs/oauth): PAR is required, PKCE S256 is required, DPoP is required with server nonces, the client_id is an https URL of the client metadata document, a confidential client authenticates with private_key_jwt and publishes its public keys (jwks or jwks_uri). After the token response the client must check that the sub DID resolves to a PDS whose authorization server is the one that issued the token."
- "The authorization server may give a confidential client a much longer session than a public client (a public client is held to about two weeks). A confidential client in OpenBao is the seamless experience the user asks for: fewer sign-ins."
- "OpenBao plugins (go-plugin, the OpenBao plugin SDK): a backend is either an auth or a secrets backend. An auth backend can also have paths that need a token, gated by ACL policies (as auth/token/lookup-self is). The login response sets an entity alias; templated policies can use identity.entity.aliases.<mount accessor>.name."
- "ts/hono-pds already serves PAR, DPoP and private_key_jwt (token_endpoint_auth_methods_supported none and private_key_jwt), so a market network can run the full confidential-client flow against its own PDS DenoPod (user decision 2026-10-12: drive hono-pds's OAuth to complete sign-ins in tests)."
- "A DPoP-bound access token is useless without a fresh proof from the DPoP key: each proof is bound to one method and URL (htm, htu), the access token hash (ath), a time (iat) and a unique jti. An access token that leaks with one proof cannot be used for another request."
- "OpenBao pki never stores the private key of pki/issue; with no_store=false it stores the certificate, which revocation by serial needs. kube-apiserver does not check revocation of client certificates. A front that ends TLS can check it and forward the user with request-header authentication (--requestheader-client-ca-file, X-Remote-User, X-Remote-Group)."
items:
- id: M1-ATPROTO-AUTH-PLUGIN
milestone: M1
title: An OpenBao auth plugin `atproto` - OpenBao does the atproto OAuth sign-in as a confidential client
depends_on: ["0009:M3-SSHSIG-AUTH-PLUGIN"]
decision: >
New Go module cmd/openbao-plugin-auth-atproto on the OpenBao plugin SDK, built and pinned exactly like
cmd/openbao-plugin-auth-sshsig (an asset in internal/versions/versions.json, sha256 registered in the
catalog, re-registered before the server restarts on an upgrade, moves together with the bao asset).
One plugin, type auth, mounted at auth/atproto. It does the whole client side of atproto OAuth: handle
to DID resolution (DNS TXT and https well-known), DID document fetch (PLC directory or did:web), PDS to
authorization server metadata, PAR with PKCE S256, DPoP with a fresh per-session key generated inside
the plugin, private_key_jwt client assertions, the code exchange, and the sub-DID check of the spec.
Paths (details decided by Claude, user may veto): `config` (written by the openbao controller only:
client_id URL, redirect_uris, scope, client_name, PLC directory URL, per-call timeout, network rules);
`client-metadata` (unauthenticated read: the client metadata document with the public keys inline as
jwks); `config/rotate-client-key` (the old public key stays in jwks for 24 h); `role/<name>` (bound_dids
or bound_handles or "*", token_policies, token_ttl, token_max_ttl); `login/start` (unauthenticated:
{role, handle or DID, redirect_uri} -> {authorization_url, state}; the pending state is single use and
ends after 10 minutes); `login` (unauthenticated, the bao login path: {state, code, iss} -> a bao token
whose entity alias name is the DID, metadata handle and pds, policies from the role). The client key is
ES256, made by the plugin at the first `config` write, and never leaves bao storage.
Network rules: https only and public addresses only, except the hosts and the PLC directory URL that the
openbao controller copies into `config` from the application's root object (a market network resolves
its fake PLC and PDS names this way; no fetch patch in Go).
files: ["cmd/openbao-plugin-auth-atproto/", "internal/versions/versions.json", "scripts/build-socialweb-computer.sh"]
done_when: "in a market network, a bao login through auth/atproto against the network's PDS DenoPod returns a bao token whose entity alias is the account's DID; `bao read auth/atproto/client-metadata` shows only public keys; a login with a used or expired state is refused; the catalog sha256 matches versions.json."
risks:
- "The plugin fetches URLs that a user controls (handle, DID document, PDS, authorization server): the network rules and the short timeouts are the guard against SSRF and slow-loris; every refusal names the rule."
- "bao runs as a HostProcess; the plugin process needs outbound https. The HostProcess network rules of the vault must allow it, and only for this plugin's server."
status: done
evidence:
- id: M1-SESSION-CUSTODY
milestone: M1
title: The session stays in bao; bao signs the DPoP proofs
depends_on: [M1-ATPROTO-AUTH-PLUGIN]
decision: >
Decided by the user 2026-10-12: bao signs DPoP proofs. Session custody is part of the same auth plugin
(not a second secrets plugin): the session is made at login in the plugin's own storage, so nothing
crosses mounts and there is one asset to pin. Paths that need a token: `session/<did>` read (did, handle,
pds, scope, expiresAt, status; no secret); `session/<did>/proof` ({htm, htu, nonce?} -> {access_token,
dpop_proof, expires_at}); `session/<did>` delete (revokes the tokens at the authorization server's
revocation endpoint, then removes the session). The plugin refreshes the access token when it has less
than 60 s left, under one lock per DID, and writes the new refresh token before it returns. A failed
refresh marks the session Expired and returns the error at once. It keeps the last DPoP nonce per origin
in memory and takes a newer one from the caller. htu must be under the session's PDS origin; any other URL
is refused.
Why a proof and not a signed (proxied) PDS request: the key never leaves bao in both designs. With a
proof, what leaves bao is a DPoP-bound access token plus one proof bound to one method, URL and time,
which cannot be used for a second request. With a proxy, bao would send every PDS request itself: bodies
and blob uploads through the plugin process, one more hop for every call, and a general http client to
user-chosen hosts inside the vault.
TypeScript: new concept atproto-session-custody in ts/atproto-market/lib: lib/abc/atproto-session-custody
(SessionCustody: start, complete, session, proof, signOut), lib/atproto-session-custody-openbao (over the
existing OpenBaoServer resolver and cert login), and a fetch handler for @atproto/api agents that asks for
a proof per request and retries once on use_dpop_nonce with the nonce from the DPoP-Nonce header. Every
gateway caller that acts for a signed-in DID uses it: the web app and the requester (in process today,
the gateway requester DenoPod after plan 0009 M3-REQUESTER-CONTROLLER).
files: ["cmd/openbao-plugin-auth-atproto/", "ts/atproto-market/lib/abc/atproto-session-custody", "ts/atproto-market/lib/atproto-session-custody-openbao", "deno.json", "deno.lock"]
done_when: "a market network gateway requester posts an RFP with an agent whose only credential is proofs from bao; the requester process memory and its vault reads show no refresh token or DPoP private key; two callers that refresh at the same time give one refresh at the authorization server."
risks: ["One local bao call per PDS request: the done_when run also reads the p95 of a proof call (budget 20 ms; the client keeps one warm TLS connection to bao). Over budget is a bug report, not a reason to move the key out."]
status: done
evidence:
- id: M2-GATEWAY-SCOPE
milestone: M2
title: Where each session lives - the vault of the application the user signed in to
depends_on: [M1-SESSION-CUSTODY]
decision: >
OpenBaoServer spec.plugins (plan 0009) gets the atproto entry with its config, and the spec declares the
roles and the policies; the openbao controller applies them, renders the templated policies after it
enables the mount (it knows the accessor), and keeps them in sync like its mounts.
Scope: (1) the gateway's root vault (OpenBaoServer root-vault) for the users of k8s.socialweb.computer:
role `user` (any DID, policy atproto-self: read session/<own did>, proof for own did, delete own session,
by the templated entity alias). Until plan 0009 M3-REQUESTER-CONTROLLER, the web app's client
(socialweb-computer-k8s) also gets proof on every session of the mount, because its in-process
requester acts for claims when the user is away. After that item, only the gateway requester DenoPod has
that policy and the web app acts only with the user's own bao token. (2) The ssh gateway's own vault
(OpenBaoServer ssh-gateway) for the users of hono-socialweb-computer-ssh, with the same roles.
Not in scope: the MarketIdentity sign-ins of the bidder (prod-qr, decided by the user 2026-10-12) and of
`run request` keep the qr.fedfork.com session transfer and their own vault stores. A tenant cluster's vault
is never a session store and never loads this plugin; it only issues client certificates
(M3-KUBECTL-CLIENT-CERT). The kv paths oauth-sessions/<did> of the web app go.
files: ["api (OpenBaoServer spec.plugins config, atproto roles)", "internal/openbao", "internal/openbaoctl", "internal/appctl/gateway.go", "internal/appctl/sshgateway.go", "deploy/apps/gateway"]
done_when: "on a market network with a gateway: root-vault and ssh-gateway list an auth/atproto mount; no bidder, requester or tenant cluster vault does; `bao policy read` of the web app client shows no path outside auth/atproto session paths and its own kv; no kv path of the web app holds a session."
risks: []
status: done
evidence:
- id: M2-WEB-APP-SIGNIN
milestone: M2
title: The web app signs users in through bao; the browser holds a cookie, not a session
depends_on: [M2-GATEWAY-SCOPE]
decision: >
The client_id is the web app's public origin plus /oauth-client-metadata.json, served by the web app (the
application's front) from bao's `client-metadata` path (cached 60 s). The redirect URI is the web app's
/oauth/callback. Sign-in: the browser posts a handle; the web app calls login/start and redirects the
browser to the authorization URL; the callback forwards {state, code, iss} to bao `login`. Decided by the
lead 2026-10-12 (user may veto): the bao token for that DID goes to the browser as an HttpOnly, Secure,
SameSite=Strict cookie (ttl 1 h, renewed on use up to the role's token_max_ttl, default 30 d). bao is not
reachable from the internet (port 0 behind the edge), so the token is useful only through the web app.
The web app calls bao with that token for the user's own paths and checks Origin on every state-changing
request. It still writes a MarketIdentity per signed-in DID, with sessionRef = {mount: auth/atproto,
path: session/<did>}. Sign-out deletes the session in bao (revocation at the authorization server),
revokes the cookie's token, and sets the MarketIdentity SignedIn=False. Deleted:
socialweb-computer-oauth-session-fs, socialweb-computer-oauth-session-vault, the browser-side OAuth client
in web/, POST /session, the DPoP private JWK and refresh token in OAuthSessionData, the request proofs of
socialweb-computer-k8s-request-proof, and the file fallback of accountState (--openbao-server becomes
required). The same change goes into ts/socialweb-computer-ssh/hono-socialweb-computer-ssh.
files: ["socialweb-computer-k8s/hono-socialweb-computer-k8s/mod.ts", "socialweb-computer-k8s/lib/hono-factory-socialweb-computer-oauth", "socialweb-computer-k8s/lib/socialweb-computer-oauth-session-fs (deleted)", "socialweb-computer-k8s/lib/socialweb-computer-oauth-session-vault (deleted)", "socialweb-computer-k8s/lib/socialweb-computer-k8s-request-proof (deleted)", "socialweb-computer-k8s/lib/socialweb-computer-requester-inproc", "socialweb-computer-k8s/web", "ts/socialweb-computer-ssh/hono-socialweb-computer-ssh"]
done_when: "a user signs in on a market network gateway with scripts/oauth-signin.ts, creates a cluster and gets nodes through the RFP flow; after `dev restart` of the web app DenoPod and after an upgrade of the gateway the same browser cookie still works and claims still post RFPs; the web app's memory, logs and DenoPod args hold no refresh token or DPoP private key."
risks: ["The bao token is a bearer credential in a cookie: HttpOnly and SameSite=Strict, short ttl, revoked on sign-out, and useless outside the web app."]
status: done
evidence:
- id: M3-KUBECTL-CLIENT-CERT
milestone: M3
title: kubectl with a short-lived client certificate - a kubeconfig download and the getKubeconfig XRPC
depends_on: [M2-WEB-APP-SIGNIN]
decision: >
Decided by the user 2026-10-12: a kubeconfig download plus an XRPC that a user or automation calls to get
the same kubeconfig. The XRPC is the existing com.publicdomainrelay.temp.cluster.getKubeconfig of the
gateway (internal/xrpcserver, Go; the cluster methods keep their NSID prefix; renaming them is not part of
this plan). Its caller presents an atproto inter-service JWT for the user's DID with lxm = that method and
aud = the gateway DID (SOCIALWEB_GATEWAY_AUDIENCES), checked by internal/atprotoauth as today, and the
method is reachable at the gateway's public XRPC endpoint through the edge, so automation calls it with
only a PDS session (com.atproto.server.getServiceAuth). The web app's download button calls the same
method for the signed-in user with a service JWT made through a bao proof. One path, two callers.
The response kubeconfig embeds a client key and a short-lived certificate (default ttl 8 h, max 24 h,
decided by the lead 2026-10-12, user may veto; the request's ttlSeconds is capped). The gateway (Go)
checks that the DID owns the Cluster and is not denied (internal/revocation), then calls pki/issue/user
on that cluster's own OpenBaoServer: role `user` (client_flag, CN = the DID, O = the cluster's tenant
group, no_store=false so revocation by serial works; bao never stores the private key). The key is made
by bao, returned once in the XRPC response and stored nowhere: not in kcp, logs or records. The response
is sent with Cache-Control no-store. A CSR path is not required.
New kind ClusterClientCertificate (api/, in the gateway's cluster workspace, ownerRef its Cluster): spec
{cluster, did, serial, notAfter, revoked}; status {revokedAt, lastError}; no key and no certificate body.
The getKubeconfig handler (Go) writes it after the issue, because the key must come back in the same call
and must never pass through kcp; a TypeScript writer would need that. Revocation: revokeIdentity, a
sign-out with "revoke my certificates" (the web app sets revoked through the gateway) or a Cluster delete
sets revoked, and the cluster provider revokes the serial in that cluster's pki.
The front that kubectl reaches (internal/tlsfront in internal/stack/gatewaykcp) asks for a client
certificate, checks it against the cluster's pki CA, refuses a serial that is revoked or expired (from a
watch on ClusterClientCertificate, with the pki CRL as a second source refreshed every 60 s), and forwards
the user to the tenant apiserver with request-header authentication (the front's own client certificate
from that cluster's pki; X-Remote-User = the DID, X-Remote-Group = the tenant group). The pki `user` role
is outside the no_store rule of plan 0013 M3-VAULT-TIDY; pki tidy removes its expired certificates.
files: ["internal/xrpcserver", "internal/gateway", "api/types_clusterclientcertificate.go", "internal/clusterprovider/vaultdeclared.go", "internal/clusterprovider/tenantcredential.go", "internal/clusterstack", "internal/tlsfront", "internal/stack/gatewaykcp", "internal/kubeconfig", "socialweb-computer-k8s/lib/hono-factory-socialweb-computer-k8s", "socialweb-computer-k8s/web"]
done_when: "on a market network gateway, `kubectl --kubeconfig <download> get nodes` works with no exec plugin; a script with only a PDS session calls getKubeconfig with a service JWT and gets a kubeconfig that works the same; a JWT with another lxm, another aud or for a DID that does not own the cluster is refused; after revoke the next kubectl call is refused at the front within 1 s; an expired certificate is refused; `kubectl get clusterclientcertificates` shows every certificate with its serial and state and no key; no log line or kcp object holds the key."
risks: ["A tenant apiserver that is not set up for request-header authentication: the cluster stack (internal/clusterstack) sets --requestheader-* from the cluster's pki at create; a cluster created before this item is recreated (no migration)."]
status: done
evidence:
- id: M3-RETIRE-EXEC-PLUGIN
milestone: M3
title: Delete the exec-plugin kubeconfig path once getKubeconfig with a certificate works in prod
depends_on: [M3-KUBECTL-CLIENT-CERT]
decision: >
Decided by the lead 2026-10-12 (user may veto): the exec-plugin path stays until getKubeconfig returns a
certificate kubeconfig that works on the prod gateway (the lead's live run), then it is deleted in this
plan with no back-compat: cmd/socialweb-credential, internal/mint, internal/devicekey, internal/execplugin,
the /v1/credential mint path of internal/xrpcserver, the credential helper downloads and the
SOCIALWEB_DEVICE_KEY and SOCIALWEB_MINT_URL settings.
files: ["cmd/socialweb-credential (deleted)", "internal/mint (deleted)", "internal/devicekey (deleted)", "internal/execplugin (deleted)", "internal/xrpcserver", "internal/kubeconfig", "internal/stack/gatewaykcp/env.go", "socialweb-computer-k8s/hono-socialweb-computer-k8s/mod.ts", "scripts/build-socialweb-computer.sh"]
done_when: "the release closure has no socialweb-credential binary and no mint path; on the prod gateway a downloaded kubeconfig and an XRPC kubeconfig both work."
risks: []
status: done
evidence:
- id: M4-INTEGRATION-TESTS
milestone: M4
title: Integration tests on the plan 0010 harness
depends_on: [M2-WEB-APP-SIGNIN, M3-KUBECTL-CLIENT-CERT, "0010:G2-IT-HARNESS"]
decision: >
Written only after scale-to-zero k3s on market RFP VMs works in prod (user, 2026-10-12), on the
G2-IT-HARNESS market network with a gateway. Tests: web app sign-in through hono-pds's OAuth; a refresh
after a core restart and after an upgrade (`socialweb-computer upgrade`) with the same cookie and no new
sign-in; sign-out revokes at the PDS and the cookie stops working; kubectl with a downloaded kubeconfig,
with a kubeconfig from getKubeconfig called by a service JWT, and refused after revoke; and a leak check:
no token, key or session in any kcp object (`kubectl get -A -o yaml` of every workspace), any unit
journal or DenoPod stdout/stderr, any DenoPod or HostProcess argument, or any record on the network's PDS
repositories. Decided by Claude, user may veto. The core restart leg is a kill -9 of the gateway's core
(a child application has no devsup of its own, so `dev restart <gateway> core` does not apply).
`socialweb-computer upgrade` needs an installed host, so the upgrade leg is the prod reading
ACC14-SURVIVES (the cookie across rc8 to rc9). Its dev form is the network core restarted on another
binary (`dev restart --binary <path> core`, every unit adopted).
files: ["test/integration/web_app_signin_test.go", "test/integration/web_app_session_test.go", "test/integration/kubeconfig_client_cert_test.go"]
done_when: "the tests pass on the harness and in the 0010 G3-CI-INTEGRATION pipeline."
risks: []
status: done
evidence:
dropped_items:
- id: M2-BIDDER-IDENTITY-BAO
verdict: dropped
why: "Decided by the user 2026-10-12: keep prod-qr for bidders. The bidder keeps the qr.fedfork.com session transfer and its vault store (OpenBaoServer bidder)."
not_doing:
- "Migration of the sessions in kv oauth-sessions/<did>: no back-compat; the next fresh deploy starts with the plugin."
- "A signed (proxied) PDS request path in bao: bao signs DPoP proofs (user, 2026-10-12)."
- "A second plugin of type secrets for custody: the auth plugin holds the session it made."
- "Sessions in a tenant cluster's vault: per-cluster OpenBao issues client certificates only."
- "The bidder's or the `run request` requester's sign-in in bao: prod-qr stays (user, 2026-10-12)."
- "A CSR path for client certificates (user, 2026-10-12: not required)."
- "Moving any atproto market flow into Go. The plugin does OAuth and DID resolution; it reads no market record."
- "A token issuer in TypeScript: OpenBao issues every token (as in plan 0009)."
- "Publishing the plugin as its own repository now: later, after prod (decided by the lead 2026-10-12, user may veto)."
acceptance:
where: hat-8
checks:
- id: ACC14-KEY-CUSTODY
text: "No DPoP private key, refresh token or OAuth client private key of a gateway user exists outside the OpenBao of the application the user signed in to; every caller acts with proofs that bao signs."
status: done
evidence: []
- id: ACC14-SURVIVES
text: "A signed-in user keeps working with no new sign-in across a core restart and a gateway upgrade."
status: done
evidence: []
- id: ACC14-REVOCATION
text: "Sign-out revokes the atproto tokens at the authorization server and the bao token; a revoked client certificate is refused at the front."
status: done
evidence: []
- id: ACC14-KUBECTL
text: "Plain kubectl works with a kubeconfig that holds a short-lived client certificate from the cluster's own OpenBao PKI, from the web app download and from getKubeconfig called with a service JWT."
status: done
evidence: []
- id: ACC14-NO-LEAK
text: "No token, key or session appears in a kcp object, a log, a DenoPod or HostProcess argument or an atproto record."
status: done
evidence: []
questions_for_user: []