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
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
plan: "0031"
title: "Gateway tenant-cluster nodes are managed through one Infra per Cluster; a NodeClaim keeps the join, the grant and the secrets"
status: done
source: >
2026-10-23, the user's request (gateway tenant-cluster nodes are managed through Infra), read
against main at 1dc14655 (plans 0028 and 0029 merged). Today every node of a tenant cluster is one
NodeClaim and one ComputeContract that the NodeClaim controller writes itself. The clusterprovider's
node host writes the NodeClaim into the cluster's home workspace (internal/nodemarket/nodemarket.go:
167-216 Provision, claimLocked 218-237 with the Cluster ownerReference of 239-244) and deletes it to
release the node (release 393-422, which waits for the claim to be gone). The NodeClaim controller
("the door", internal/controllers/nodeclaim/controller.go) lists every ComputeContract of the
contract workspace on each reconcile (Reconcile 192-232), writes the join bundle into the gateway
vault and applies a ComputeContract named nodeclaim-<20 hex of the claim uid> with
spec.nodeClaimRef (ensure 234-274, mapper Contract internal/gateway/nodeclaim/nodeclaim.go:274-327),
writes spec.release and deletes the contract on the claim's deletion (settle 276-312, mapper
Settle 461-496), and releases or deletes contracts whose claim is gone or was recreated (orphan
314-337, gone 339-355, mapper Orphan 440-459). It maps contract events back to claims through
spec.nodeClaimRef (contractClaims 523-538, resync 635-650). The contract workspace is the gateway
root workspace (internal/app/gateway/gateway.go:482 gatewayNodeMarket(o, p.ws, p.r.Workspace);
internal/services/gatewayapi/config.go:88). compose.Adopt and the GC report skip a contract with
spec.nodeClaimRef (internal/app/compose/infra.go:175 and 558).
Plan 0029 built Infra: a deployment object that owns its ComputeContracts in its own workspace
(internal/controllers/infra/contracts.go:349-355 owned by labels, 437-448 create), with the
Staged, Requested, Released states, the release finalizer and the plan 0028 order (controller.go
298-332 finalize), and a ComputeContract spec built only from the publishable contract fields and
spec.secrets (internal/infra/lifecycle.go:98-107 ContractSpec), so a node's nodeJoin, ownerDid and
adopt have no place in it yet. The Infra and NodeClaim controllers run in the same core
(internal/stack/componentcore/controllers.go:145 and 149). The Gateway root's delete pass
(internal/app/gateway/gateway.go:150-194) deletes every account Cluster
(compose.DeleteAccountClusters, internal/app/compose/infra.go:678-716) and then waits while a
ComputeContract still holds a guest (compose.HeldContracts, internal/app/compose/apply.go:408-428).
The MarketRequester root's delete pass deletes its ComputeContracts first and has no step for
Infra or EphemeralInfra (internal/app/marketrequester/marketrequester.go:121-142), plan 0029
R8-ROOT-DELETE-ORDER. Removing spec.record from an Infra deletes its AtprotoRecord child
(internal/controllers/infra/controller.go:221-233, record.go:243-264), whose finalizer deletes the
record from the repo (internal/controllers/atprotorecord/controller.go:204-224), plan 0029
U1-UNBIND-KEEPS-RECORD.
principle: >
A tenant cluster's nodes are one deployment. Each Cluster that has a NodeClaim has one Infra in
the gateway's contract workspace, and each node is one contract entry of it. The Infra controller
is the only writer of a node's ComputeContract and runs its whole lifecycle (Requesting, Deployed,
Releasing, Released, Failed, the release finalizer and the plan 0028 order). The NodeClaim keeps
what only a tenant cluster has: the guest grant, the join secrets, the apiserver ClusterLink
ticket, adoption, and the loss and join graces of the clusterprovider. Deleting a Cluster deletes
its Infra, which releases every node; deleting a root deletes its deployments before it stops the
component that sends the releases.
state_rules:
- Read AGENTS.md first. status is one of todo, doing, done, blocked. The lead flips statuses.
- No Go tests (AGENTS.md, plan 0027). Proof is the build gates and the ACC31 live rows below.
- Fresh deploy (AGENTS.md, Prod deployments). No migration code for NodeClaims or ComputeContracts
written before this plan; a contract named nodeclaim-<hex> left by an older core is not adopted.
- Plan 0030 is rewiring the account identity and credentials code now. This plan edits none of
internal/pds/identity, internal/services/bidder/credentials.go,
internal/market/requester/identity.go, internal/pds/remote or internal/marketidentity.
- This plan owns plan 0029's R8-ROOT-DELETE-ORDER and U1-UNBIND-KEEPS-RECORD and executes them
against their done_when in plan 0029; their statuses stay in plan 0029.
items:
- id: A1-API
title: "Infra carries the node fields and its Cluster; a NodeClaim names its entry; ComputeContract loses nodeClaimRef"
depends_on: []
decision: >
InfraSpec gains two kcp-only fields that are never written to a record, like spec.secrets:
clusterRef (an ObjectRef to the tenant Cluster, kind Cluster, with its home workspace), and
nodes (listType=map keyed by contract), one InfraContractNode per node entry with ownerDid,
adopt, nodeJoin (the ComputeContractNodeJoin type) and nodeClaimRef (the NodeClaim the entry
serves). NodeClaimStatus gains infra (workspace, name, contract, computeContract), written by
the NodeClaim controller: this is how a NodeClaim refers to its Infra entry. It is status, not
spec, because the NodeClaim controller chooses the entry and the clusterprovider writes the
claim's spec. ComputeContractSpec.nodeClaimRef is removed rather than kept: a node's contract
is now named by its ownerReference to the Infra and the labels infra.socialweb.computer/name
and infra.socialweb.computer/contract, and the claim is named by the entry's
spec.nodes[].nodeClaimRef; nothing in the requester reads nodeClaimRef. The constant
InfraClusterLabel = "infra.socialweb.computer/cluster" (the Cluster's name) marks a cluster's
Infra for kubectl. go generate ./api/... writes the schemas; a Revision bump follows if the
generator asks.
files:
- api/v1alpha1/types_infra.go
- api/v1alpha1/types_nodeclaim.go
- api/v1alpha1/types_computecontract.go
- api/v1alpha1/zz_generated.deepcopy.go
- api/v1alpha1/resources.go
- deploy/
done_when: "go generate ./api/... leaves no diff on a second run; go build ./... passes."
status: done
- id: I2-INFRA-NODE-FIELDS
title: "The Infra controller puts a node entry's fields on its ComputeContract"
depends_on: [A1-API]
decision: >
infra.ContractSpec takes the entry's InfraContractNode (nil for an ordinary entry) and sets
ownerDid, adopt and nodeJoin on the ComputeContract spec, beside the publishable fields and
spec.secrets. The Infra controller passes spec.nodes by contract name, as it passes
spec.secrets. The record render and the declaration hash stay description plus contracts, so
clusterRef, nodes and secrets never reach a record. ttlSecondsAfterFinished is not set: the
schema default (691200) equals the NodeClaim mapper's ContractTTLSeconds.
files:
- internal/infra/lifecycle.go
- internal/controllers/infra/contracts.go
done_when: "ACC31-SCALE-UP-TWO passes: each node's ComputeContract carries nodeJoin, ownerDid and the bundle secretsRef."
status: done
- id: N3-CLAIM-ENTRY
title: "The NodeClaim controller composes its Cluster's Infra and writes no ComputeContract"
depends_on: [A1-API, I2-INFRA-NODE-FIELDS]
decision: >
Where. The Infra lives in the contract workspace of the binding (the gateway root workspace),
beside the ComputeContracts it owns and the Requester that runs them; the Infra controller
applies and lists its contracts only in its own workspace, and the tenant's home workspace
must not hold an object that declares contracts on the gateway's account. It is named
nodes-<20 hex of sha256(<home workspace>/<claim spec.clusterRef.name>)> (that name is the
cluster key <account>-<label>, not the Cluster object's name), with spec.clusterRef naming the
Cluster object by the claim's Cluster ownerReference, the label infra.socialweb.computer/cluster=<cluster name> and a description naming the
cluster. It carries no ownerReference: kcp garbage collection is per logical cluster, and an
owner in another workspace reads as a missing owner, so the dependent would be collected at
once. The Cluster owns it through spec.clusterRef and the delete rule of N4-CLUSTER-DELETE.
One writer. The NodeClaim controller is the Infra's composer and the only writer of its spec
(field manager socialweb.computer/nodeclaim): it holds every input of a node's contract (the
join bundle in the gateway vault, the pinned meshd and node agent downloads and their resolved
download base, the node policy, the extra bidder DIDs, the user data base), which the
clusterprovider does not have. The scale decision stays where it is: internal/scale decides,
and the node host internal/nodemarket expresses a scale up by writing a NodeClaim and a scale in
by deleting it, as today. The controller turns that one to one into the entry: a new claim
adds the entry, and a deleting claim sets it to Released. So the scale decision adds a
Requested entry and sets an entry to Released through the claim it already writes, and no
second writer touches the Infra's atomic spec.contracts. Every write reads the Infra, changes
only this claim's entry (contracts, secrets and nodes by entry name) and applies the whole
spec, under one in-process lock, so two claims of one cluster never lose each other's entry.
The entry. Its name is node-<12 hex of sha256(claim uid)>, so a claim recreated under the same
node name gets a new entry while the old one releases. Its ComputeContract is
infra-<infra name>-<entry name> (50 characters), and the join bundle stays at
<web app vault mount>/contracts/<that name>/bundle, so the requester's vault link is unchanged.
Its state follows the claim: deleting, or ended (Failed, Lost, Released), is Released; phase
empty or Pending with no bundle written yet (the download base or the cluster's OpenBao link
ticket is not ready) is Staged; Bidding, Provisioning, Joining, Running and Adopting are
Requested. The controller writes the bundle and the Bidding (or Adopting) status before it moves
the entry to Requested, which keeps today's order: the RFP is sent only once the bundle is in
the vault. Staged is used only for that wait. A warm pool of pre-provisioned nodes is not
built: a Staged entry holds no guest, so it would save only the bundle write; a pool would
need Requested guests that have not joined, and is a later item.
Status. The controller reads the entry's ComputeContract by name (a GET, not a list of the
workspace) and maps it onto the claim with the mapper's Status as today, and writes
status.infra. Contract events reach the claim through an index from the contract name to the
claim, learned on reconcile; Infra events reach the claims of that Infra the same way. The
deletion of a claim (finalizer nodeclaim.socialweb.computer/release) sets its entry to
Released and waits, woken by the contract watch, until the contract is Released, Failed or
gone; then it writes Released (or Failed) on the claim, removes the bundle and lets go. A claim
that was Failed or Lost and whose release still retries lets go at once, as Settle does today;
the Infra keeps the release going. The gone hook drops the claim's entry; the Infra controller
then deletes the finished contract (a dropped entry is released and deleted once finished,
plan 0029 D4). When the last entry is dropped the controller deletes the Infra, so a cluster
at zero nodes has none. On bind, the resync sweeps the cluster Infras of the contract workspace
and drops entries whose claim is gone or has a different uid, which covers a claim deleted
while the core was down.
Removed. From internal/controllers/nodeclaim: the ComputeContract list per reconcile, ensure's
contract apply, settle's spec.release and contract delete, orphan, the contract part of the
gone hook, the nodeClaimRef contract mapping and the resync over contracts. From
internal/gateway/nodeclaim: ContractNamePrefix, ContractName, Contract, Orphan and the release
and delete parts of Settle. From internal/contract/nodeclaim: UIDLabel, ContractTTLSeconds,
Orphaned and Settlement.Release and DeleteContract. Kept: Wants, Bases, LinkNames, Bundle,
BundleRef, Requested, Failed, Status, LostMessage, the bundle write and removal, the ClusterLink
reads, the exposure wake-up, the home index and the join report.
files:
- internal/contract/nodeclaim/nodeclaim.go
- internal/gateway/nodeclaim/nodeclaim.go
- internal/controllers/nodeclaim/controller.go
- internal/controllers/nodeclaim/infra.go
done_when: "ACC31-SCALE-UP-TWO and ACC31-SCALE-IN-ONE pass; no ComputeContract named nodeclaim-* is written."
status: done
- id: N4-CLUSTER-DELETE
title: "Deleting a Cluster deletes its Infra, which releases every node"
depends_on: [N3-CLAIM-ENTRY]
decision: >
The NodeClaim controller watches Cluster objects in the home workspaces and maps a deleting
Cluster to its claims. A claim whose Cluster is deleting or gone deletes the cluster's Infra
and never applies it again; its finalizer then releases every node at once, in the plan 0028
order, while the clusterprovider's teardown still deletes the claims one by one and waits for
each to be gone (nodemarket release). A claim's deletion then finds its contract finished or
gone and lets go. A cluster with no claim has no Infra, so a Cluster deleted at zero nodes has
nothing to release. The resync on bind deletes a cluster Infra whose Cluster is gone.
files:
- internal/controllers/nodeclaim/controller.go
- internal/controllers/nodeclaim/infra.go
done_when: "ACC31-CLUSTER-DELETE passes."
status: done
- id: G5-GATEWAY-ROOT-DELETE
title: "The Gateway root's teardown is the cluster delete path, then the deployments, then the held-contract guard"
depends_on: [N4-CLUSTER-DELETE]
decision: >
compose gains DeleteDeployments(ctx, ws): it deletes every Infra and EphemeralInfra of the
workspace that is not deleting yet and returns how many are left, the same shape as
DeleteAll. The Gateway root's delete pass keeps its order and gains one step: delete the
account Clusters and wait (each Cluster deletes its Infra, N4-CLUSTER-DELETE), then
DeleteDeployments on the root workspace and report Deleting "<n> deployment(s) releasing" while
any is left (an Infra whose Cluster was already gone), then the HeldContracts guard as today,
with a message that no longer names NodeClaims, then the records and the components. So the
plan 0028 M4 teardown is the cluster delete path applied to every Cluster, and the Requester
stops only after every node is released. compose drops its nodeClaimRef checks in adoptable and
ownedByLink: a node's contract has an ownerReference to its Infra, which both already honour.
files:
- internal/app/compose/apply.go
- internal/app/compose/infra.go
- internal/app/gateway/gateway.go
done_when: "ACC31-GATEWAY-ROOT-DELETE passes."
status: done
- id: R8-ROOT-DELETE-ORDER
title: "Plan 0029 R8: a MarketRequester root deletes its deployments before it stops the Requester"
depends_on: [G5-GATEWAY-ROOT-DELETE]
decision: >
The delete pass of internal/app/marketrequester calls compose.DeleteDeployments on the root
workspace first and reports Deleting "<n> deployment(s) releasing" while any is left; then the
plan 0028 M3 step (its ComputeContracts), then the components and the vaults. The Infra
finalizers release their contracts and delete their records while the Requester and the Pds
still run. An InfraCollection is not deleted by this step: its children are Infras and go with
it; the collection goes by garbage collection when the root's workspace goes.
files:
- internal/app/marketrequester/marketrequester.go
done_when: "Plan 0029 R8-ROOT-DELETE-ORDER done_when: ACC31-R8-ROOT-DELETE passes."
status: done
- id: U1-UNBIND-KEEPS-RECORD
title: "Plan 0029 U1: removing spec.record stops the sync and keeps the record"
depends_on: []
decision: >
AtprotoRecordSpec gains deletionPolicy (Delete, the default, or Retain). The AtprotoRecord
finalizer deletes the repo record only under Delete; under Retain it lets go and logs
atprotorecord_retained. The Infra controller's unbind (spec.record removed from a bound Infra or
EphemeralInfra) re-applies its AtprotoRecord child with the child's current spec plus
deletionPolicy Retain (the same field manager, so it stays the one writer), then deletes the
child, its AtprotoRecordWatch and the watch's deliver ComponentLink, and reports Synced False
Unbound once they are gone. status.record keeps the uri and cid of the record left in the repo
with direction Retained (a new InfraSyncDirection), and the unbind step does not run again
while it reads Retained; a status apply of record: null does not clear the field in this kcp. Deleting a bound Infra still deletes the record (Delete). Rebinding
later to the same repo finds the record present and follows the plan 0029 first-sync rule.
files:
- api/v1alpha1/types_atprotorecord.go
- internal/controllers/atprotorecord/controller.go
- internal/controllers/infra/controller.go
- internal/controllers/infra/record.go
done_when: "Plan 0029 U1-UNBIND-KEEPS-RECORD done_when: ACC31-U1-UNBIND passes."
status: done
- id: REC9-RECORD-OPT-IN
title: "A cluster's Infra may publish its record to the cluster owner's repo"
depends_on: [N3-CLAIM-ENTRY]
decision: >
The switch. ClusterSpec gains publishInfra (bool, optional, default false). Default: a
cluster's Infra has no spec.record and nothing is published. There is no gateway-level default.
Its writer is the cluster owner, through a new procedure of the gateway's cluster XRPC API,
com.publicdomainrelay.temp.cluster.setPublishInfra {name, publish}: the account is the verified
service-auth token's iss, never an input field, and the tenant API applies spec.publishInfra
with its field manager socialweb.computer/gateway, as it applies spec.running. Publishing writes
into the owner's own repo, so only the owner asks for it; no other code writes the field. The
web app's OAuth scope gains the rpc scope of the new procedure.
The binding. The NodeClaim controller stays the one writer of the cluster Infra's spec. On each
declare it reads the claim's Cluster (it already reads it for N4) and, while publishInfra is
true and the Cluster is not deleting, writes spec.record {repoRef: the AtprotoRepo
repo-<owner did> (atprotorepo.RepoName of the Cluster's spec.account) in the binding's
workspace (the GatewayAPI's workspace, root:<gateway>:app), sync: Publish, eventSourceRefs: the
binding's infraRecordSources}; while it is false the spec carries no record. The Cluster watch
of N4 maps the flip to the cluster's claims, so the next declare applies it. A cluster at zero
nodes has no Infra (N3) and so no record: the record appears with the first node and goes with
the Infra when the last node goes, by the plan 0029 D3 delete rule (deleting a bound Infra
deletes its record).
Who writes the repo. The Gateway root composes the owner's repo only from credentials it holds
for that DID, and the callee holds them; no session or token is copied into the Infra path.
On the market network profile the Gateway spec names the owner sessions file: for each DID in
it that is not the gateway requester's own, the root composes in root:<gateway>:app a
MarketIdentity owner-<stamp of the did> in mode pdsAccount (the plan 0030 owner of that
account's one session; the password from the file is written into the gateway root vault at
requester/identity/owner-<stamp>/keys and read there by the MarketIdentity controller), a
RemotePDS for the file's service URL with the network CA, a RemoteRepo and an AtprotoRepo
repo-<did> whose credentialsRef is that MarketIdentity, and the links: RepoBackendLink,
compose.RemoteRepoLinks (the RemoteRepo resolves pds.Authenticator from the MarketIdentity
over the bus), the trust links, compose.InfraLinks(<contract workspace>, repo) and the
events.subscribe link from the record watches' EventStreams of the contract workspace to the
network relay EventSource. They are composed whether or not a cluster opts in, so the switch
needs no root pass; nothing is written to the repo until a Cluster opts in. On prod a tenant
owner's session is the web app's: the openbao-plugin-auth-atproto mount holds it at
auth/atproto/session/<did> and alone refreshes it; the core holds no client with the declared
atproto-session-proxy policy that could ask it for a proof. That source is
REC9-PROD-OWNER-SESSION, not built here; until it lands no repo-<did> exists for a prod owner,
the Infra reports Synced False RecordUnavailable and nothing is published.
What is published. InfraRecordBinding gains sync (Both, the default and the plan 0029
two-way rule, or Publish). Under Publish the record carries the description (which names the
cluster only, no workspace path) and, for each entry, name, state, vmName, bidderDids and
denyBidderDids, and the observed status (phase; per contract phase, winner and receipt; no
message). userData, policy, policyArgs, sshAuthorizedKeys and vmReadyTimeoutSec of the entry and
spec.nodes (nodeJoin, ownerDid, adopt, nodeClaimRef), spec.secrets and spec.clusterRef are
never rendered (internal/infra Render already leaves the last three out; Published() projects
the rest away). The declaration hash compares the projection on both sides.
Edit rule: the cluster record is push-only. An owner's edit could not scale the cluster
safely: the scale decision is internal/scale's, from pending pods, and the node host expresses
it as NodeClaims; an entry added on the PDS would be a contract with no nodeJoin, no bundle and
no claim (a guest billed to the gateway that never joins), and an entry removed would release a
node under a running pod. So under Publish the Infra controller never pulls: a record that
differs from the projection (an edit, an invalid record) is overwritten with a compare-and-swap
on the cid it read, and logged infra_record_overwritten; a record deleted elsewhere is written
again and never deletes the Infra. A record failure under Publish never blocks the contract
lifecycle: the contracts are reconciled, Synced False RecordUnavailable is reported, and the
error goes back to the rate-limited backoff.
Off rule: setting publishInfra false removes spec.record, and plan 0029 U1-UNBIND-KEEPS-RECORD
applies: the AtprotoRecord child is retained and deleted, the record stays in the owner's repo
with its last content, status.record.direction is Retained and Synced is False Unbound.
Turning it on again rebinds to the same URI and pushes.
files:
- api/v1alpha1/types_cluster.go
- api/v1alpha1/types_infra.go
- api/v1alpha1/types_component.go
- api/v1alpha1/resources.go
- deploy/
- lexicons/com/publicdomainrelay/temp/cluster/setPublishInfra.json
- internal/gateway/gateway.go
- internal/xrpcserver/xrpcserver.go
- internal/kcpclient/kcpclient.go
- internal/app/compose/atprotoscope.go
- internal/infra/declaration.go
- internal/infra/sync.go
- internal/controllers/infra/controller.go
- internal/controllers/infra/record.go
- internal/contract/nodeclaim/nodeclaim.go
- internal/services/gatewayapi/service.go
- internal/controllers/nodeclaim/controller.go
- internal/controllers/nodeclaim/infra.go
- internal/app/compose/owner.go
- internal/app/gateway/gateway.go
- deploy/market-network/README.md
done_when: "ACC31-REC9-DEFAULT-OFF, ACC31-REC9-PUBLISH, ACC31-REC9-EDIT-OVERWRITTEN, ACC31-REC9-OPT-OUT-KEEPS and ACC31-REC9-SCALE-IN-RECORD pass."
status: done
- id: REC9-PROD-OWNER-SESSION
title: "A prod tenant owner's repo through the web app session the atproto auth plugin holds"
depends_on: [REC9-RECORD-OPT-IN]
decision: >
Not built in this plan. The source of a web-app owner's credentials is the
openbao-plugin-auth-atproto mount (auth/atproto/session/<did>, refreshed only by the plugin
under its per-DID lock, the proof endpoint hands one access token and one DPoP proof per
request). A MarketIdentity mode whose pds.Authenticator asks that proof endpoint through a
vault handle of a client bound to the declared atproto-session-proxy policy, composed per
opted-in owner by the Gateway root as repo-<did>; the web app's OAuth scope gains repo scope on
computer.socialweb.infra, so owners sign in again once. It cannot be proven on a market network
(the network gateway has no public-origin and so no web sign-in), so it is proven on prod by
the lead.
files: []
done_when: "On prod, alice opts a cluster in and getRecord on her repo returns its computer.socialweb.infra record."
status: done
- id: DOCS
title: "Code map and the gateway section"
depends_on: [G5-GATEWAY-ROOT-DELETE]
decision: >
docs/ARCHITECTURE.md section 12 says the NodeClaim controller composes one Infra per Cluster
and writes no ComputeContract; section 15 records this plan's before (a ComputeContract per
NodeClaim, written and released by the NodeClaim controller) and after (the ACC31 rows).
deploy/market-network/README.md's cluster gateway section names the cluster Infra and how to
read it with kubectl.
files:
- docs/ARCHITECTURE.md
- deploy/market-network/README.md
done_when: "Both documents name the cluster Infra, spec.nodes and status.infra."
status: done
- id: ACC31-SCALE-UP-TWO
title: "A tenant cluster scales from 0 to 2 nodes through its Infra"
depends_on: [N3-CLAIM-ENTRY]
done_when: >
On an own market network with the gateway (instance nodes31), two pods that need a node each
make the clusterprovider write two NodeClaims; the cluster's Infra nodes-<hex> in root:gateway
holds two entries that go Requested, then Deployed; each entry's ComputeContract
infra-nodes-<hex>-node-<hex> is owned by the Infra and carries nodeJoin and the bundle
secretsRef; both NodeClaims reach Running with status.infra naming their entry; both tenant
nodes are Ready; no ComputeContract named nodeclaim-* exists.
status: done
- id: ACC31-SCALE-IN-ONE
title: "Scaling in to 1 releases one entry and leaves the other node untouched"
depends_on: [ACC31-SCALE-UP-TWO]
done_when: >
Deleting one pod lets the clusterprovider scale in after its hold: one NodeClaim is deleted, its
entry goes Released, its ComputeContract is Released and then deleted and its entry dropped, its
guest is gone on the bidder; the other entry stays Deployed, its contract, claim and node keep
their uid and stay Running and Ready.
status: done
- id: ACC31-CLUSTER-DELETE
title: "Deleting the Cluster releases its Infra and leaves no guest"
depends_on: [ACC31-SCALE-IN-ONE]
done_when: >
kubectl delete of the tenant Cluster deletes its Infra (Terminating with
infra.socialweb.computer/release, its contracts Released, then gone), the NodeClaims go, the
Cluster goes, and no ComputeVM is left on the bidder.
status: done
- id: ACC31-GATEWAY-ROOT-DELETE
title: "Deleting the Gateway root with a busy tenant cluster is bounded as in plan 0028 M4"
depends_on: [G5-GATEWAY-ROOT-DELETE]
done_when: >
With a tenant cluster holding a node (a pod running on it), kubectl delete of the Gateway root
reports Deleting "<n> Cluster(s) tearing down", the cluster's Infra is released and deleted, no
ComputeContract holds a guest before the Requester component is deleted, the root is gone
within the plan 0028 M4 bound, and no guest is left on the bidder.
status: done
- id: ACC31-R8-ROOT-DELETE
title: "Plan 0029 R8 row: a MarketRequester root deleted with a Deployed Infra"
depends_on: [R8-ROOT-DELETE-ORDER]
done_when: >
A MarketRequester root with one Deployed Infra bound to its repo is deleted; its status reads
Deleting "1 deployment(s) releasing" before "component(s) stopping"; the guest is gone on the
bidder; the computer.socialweb.infra record is gone from the repo; the root is gone.
status: done
- id: ACC31-U1-UNBIND
title: "Plan 0029 U1 row: an unbound Infra keeps running and its record stays"
depends_on: [U1-UNBIND-KEEPS-RECORD]
done_when: >
Removing spec.record from a bound, Deployed Infra deletes its AtprotoRecord child (which had
deletionPolicy Retain), its watch and its deliver link; the Infra stays Deployed with its
guest; getRecord on the repo still returns the record.
status: done
- id: ACC31-REC9-DEFAULT-OFF
title: "A tenant cluster that did not opt in publishes nothing"
depends_on: [REC9-RECORD-OPT-IN]
done_when: >
On an own market network with the gateway (instance rec31), a tenant cluster scaled to one node
has an Infra with no spec.record, and listRecords of computer.socialweb.infra on the owner's
repo returns no record of it; the owner's repo-<did> AtprotoRepo is Ready.
status: done
- id: ACC31-REC9-PUBLISH
title: "Opting in publishes the node entry and its state to the owner's repo, and nothing kcp-only"
depends_on: [ACC31-REC9-DEFAULT-OFF]
done_when: >
setPublishInfra {publish: true} by the owner sets Cluster spec.publishInfra; the Infra gets
spec.record (repo-<owner did>, sync Publish) and reports Synced True; getRecord of
computer.socialweb.infra/<infra name> on the owner's PDS returns the node entry with its name,
state requested, vmName and bidderDids and status phase deployed with the winner, and no
nodeJoin, ownerDid, nodeClaimRef, secretsRef, userData, policy or policyArgs.
status: done
- id: ACC31-REC9-EDIT-OVERWRITTEN
title: "An owner's edit of the cluster record cannot scale the cluster; kcp writes it back"
depends_on: [ACC31-REC9-PUBLISH]
done_when: >
A putRecord straight to the owner's PDS that adds a second entry and marks the first released
leaves the Infra spec, the NodeClaim and the node unchanged; the core logs
infra_record_overwritten and getRecord returns the kcp projection again; a deleteRecord straight
to the PDS is followed by the record written again, and the Infra stays.
status: done
- id: ACC31-REC9-OPT-OUT-KEEPS
title: "Opting out unbinds and keeps the record (plan 0029 U1)"
depends_on: [ACC31-REC9-PUBLISH]
done_when: >
setPublishInfra {publish: false} removes spec.record; the AtprotoRecord child goes; the Infra
reports status.record.direction Retained and Synced False Unbound and stays Deployed; getRecord
still returns the record. Opting in again rebinds and the record is pushed again.
status: done
- id: ACC31-REC9-SCALE-IN-RECORD
title: "Scaling in shows in the record; the last node takes the record with the Infra"
depends_on: [ACC31-REC9-OPT-OUT-KEEPS]
done_when: >
With the cluster opted in at two nodes, scaling the pods to one lets the clusterprovider scale
in: the AtprotoRecord writes the released entry and getRecord then returns the record with only
the remaining entry, still deployed; scaling to zero deletes the Infra with its last entry and
getRecord answers RecordNotFound; no guest is left on the bidder.
status: done