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
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
plan: "0026"
title: "ABC control plane: contracts, abstract kinds, a declared bus and watch-only controllers"
status: done
source: >
The 2026-10-22 architecture audit found that the tree has 197k non-test Go
LOC, 203 packages, 57 kinds and about 25 controllers, and that only one
controller (internal/cloudprovider) uses an informer and a workqueue. The
rest poll on 2s, 5s or 60s tickers or run hand-written List+Watch loops
(17 copies). There is no contract layer: OpenBao access has 5 to 9
implementations, PDS access 5, TTL caches about 20, setCondition 14 to 18,
and 89 groups of byte-identical helpers. Bidder and Requester share 85
function names and 18 identical bodies. There are 10 ref types (pdsRef
alone has 4 shapes) and URLs and host paths sit in spec. ComputeVM
status.market has 4 writers and Bid status 2; annotations carry control
signals. appctl (12k LOC) replaces child specs wholesale. The in-process
handler registry and ComponentLink already form a default-deny call bus,
but the core's run context makes every controller the System caller
(internal/stack/componentcore/componentcore.go:317), so controllers bypass
links; the AtprotoRecord controller then writes a local PDS with no token
(docs/plans/0025-defect-register.yaml D01). The deepcopy file is kept by
hand and 11 kinds are missing from the scheme (D22).
The old TypeScript tree had a clean ABC layering
(old tree: CLAUDE.md:123-181): common -> abc -> impl -> factory -> CLI,
one-way imports, and a new transport as a sibling package, never a flag.
The compute-provider concept is the model: one ComputeProvider interface
(old tree: hono-compute-provider/lib/abc/compute-provider/mod.ts:69-88)
with digitalocean, firecracker, kcp and local siblings. This plan applies
that layering to the Go tree and to the CRD surface, removes polling, and
makes every component-to-component and controller-to-component
interaction a declared, capability-scoped link.
principle: >
Every effectful reference carries a capability, every capability is
granted by a declared link, every credential is held by the callee as a
lease, every mutable field has one writer, and every controller is driven
by watches. Spec flows down from the composing root; status flows up from
the owning controller. An abstract kind is a written contract; each
implementation is a concrete kind, a controller and a sibling package.
Polling is a bug.
decisions:
runtime: >
Controllers run on sigs.k8s.io/multicluster-runtime with the kcp
github.com/kcp-dev/multicluster-provider apiexport provider, reading
through the APIExport virtual workspaces. Versions are pinned in go.mod.
The tree wraps it in internal/controllers/runtime so no controller
imports multicluster-runtime directly. If the W0-RUNTIME pilot hits a
blocker in the experimental provider, the fallback is client-go typed
informers and workqueue behind the same internal/controllers/runtime
interface; the fallback is decided once, in W0, and recorded here by
flipping W0-RUNTIME to done with the chosen backend in the commit
message.
kcp: >
kcp is built by this repository from upstream kcp-dev/kcp main at a
pinned commit (first pin 74e9fb9c, 2026-10-22), not taken from a release
binary; kcp-dev/sdk and multicluster-provider are pinned to match.
Decided by the user on 2026-10-22.
codegen: >
deepcopy-gen through k8s.io/code-generator kube_codegen.sh gen_helpers
writes zz_generated.deepcopy.go. internal/apigen additionally writes
zz_generated.register.go per group (AddKnownTypes for every kind in
APIResources, GroupVersionResource vars, resource name consts) and
zz_generated.conditions.go (GetConditions/SetConditions for every kind
whose status has Conditions). No client-gen, lister-gen, informer-gen,
applyconfiguration-gen, kcp cluster-* generators or controller-gen.
internal/apigen and its revision lock stay the schema source.
comment_exception: >
The no-comments rule bans explanation, not function. Functional
comments that a tool reads (//go: directives, generated-code headers,
k8s code generator markers anywhere they are needed) are allowed and
carry no prose. Approved by the user on 2026-10-22.
abstract_kinds: >
A concept with two or more implementations is an abstract kind plus
concrete kinds. Consumers ref only the abstract kind. The abstract kind
has spec.backendRef (an ObjectRef to a concrete kind) and a contract
status that its controller mirrors from the backend. Concrete kinds
carry the label contracts.socialweb.computer/<abstract-resource>:
v1alpha1. This is the Cluster API infrastructureRef pattern.
abstract_kind_set: |
AtprotoRepo -> LocalRepo (backed by the Pds component) | RemoteRepo
(backed by RemotePDS plus a credentialsRef). The abstract kind keeps
the name AtprotoRepo.
Vault -> OpenBaoServer (local) | RemoteOpenBao.
ComputeVM -> FirecrackerVM now; HcsVM and VzVM when plans 0011 and 0012
land.
EventSource -> PdsEventSource (subscribeRepos through an AtprotoRepo) |
RelayEventSource | JetstreamEventSource; EventStream refs an
EventSource.
bus: >
The in-process handler registry becomes internal/bus. Components and
controllers obtain typed contract handles with
bus.Resolve[T](ctx, from, to, capability). The caller identity is the
object being reconciled or the component instance, never a controller
and never System; System stays only for core bootstrap callers listed in
deploy/bus-bootstrap-callers.txt. The callee holds credentials and signs
or authenticates; a handle never exposes a token. A call with no
accepted ComponentLink fails with ErrLinkDenied.
links: >
ComponentLink v2 (exposure.socialweb.computer) names from (ObjectRef or
a label selector), to (ObjectRef), and capabilities (name plus scope).
The producer side owns the link: the app root that composes the
producer writes links for its own children; a cross-app or
cross-workspace link is written in the producer's workspace by the
producer's root, or comes from a kcp permission claim. Status has
Accepted and ResolvedRefs conditions. Consumers list their resolved
links in status.links. Default deny.
capabilities: |
repo.read, repo.write {collections}, repo.subscribe {collections}
pds.account {did}, pds.session {did}
vault.read {mount/path prefix}, vault.write {mount/path prefix},
vault.lease {role}, vault.pki.issue {role}
policy.run {workflows}
compute.provision, compute.release
events.subscribe {collections, dids}, events.deliver {sink}
xrpc.call {nsids}
exposure.publish {hostnames}
refs: >
One ref package, api/common/v1alpha1: ObjectRef {group, kind, name,
workspace}, LocalObjectRef {name}, StrongRef {uri, cid},
SecretRef {vault ObjectRef, mount, path, key}, CredentialsRef
{ObjectRef to a credentials provider object}. All 10 existing ref types
are replaced. No URL, endpoint, host path or file path in spec unless it
is genuine user input; such a field carries the apigen tag
apigen:"userEndpoint". Addresses a consumer needs are published in the
producer's status.address.
writers: >
Every write is server-side apply with field manager
socialweb.computer/<controller>. A spec is written only by its composer
(the root for children, the creating component for intent objects). A
status is written only by the controller of that kind; a field that a
second controller must report becomes its own status subfield with its
own field manager, listed in the kind's Go type as a const. Every child
and intent object has an ownerReference to its composer or creator.
suspend: >
spec.suspend stops participation: no new RFPs, bids, accepts, offerings
or heartbeats, and no new outbound calls that create obligations. It
never stops obligations already taken: release of running VMs,
termination StateSync acks, withdrawal of published offers, credential
renewal for held leases. ApplicationPhase gains Suspended. A root
suspend cascades; a component override can only add suspension. One
writer per suspend field.
no_polling: >
Controllers are driven by watches on their own kind and every kind they
ref. Deadlines use delayed requeue. Errors use the runtime's rate-limited
backoff. A ticker is allowed only for an outbound obligation (market
heartbeat, lease renewal against a system with no watch, relay crawl)
and must be listed in deploy/obligations.txt with its owner and period.
systemd units are followed through D-Bus signals, processes through
wait, PDS and relay through subscribeRepos/firehose/JetStream.
config: >
ConfigFrom and the appctl override type switch are deleted. Services
receive their typed Spec; defaults and validation live in the schema via
new apigen tags (default, minimum, maximum, maxLength, pattern,
listType, listMapKey, enum). Per-kind settings structs are the same
types embedded in the component specs. component.Typed[S] registers a
service from its Spec type.
layout: |
The old TypeScript ABC layers, in Go:
api/<group>/v1alpha1 common: types, contract status, refs
internal/contract/<concept> abc: small Go interfaces and pure types;
imports api and stdlib only
internal/kit/<name> common helpers with no concept; imports
api, contract, kit and third-party only
internal/<concept>/<impl> impl: one sibling package per backend;
the real work; no kcp (testable_backends)
internal/controllers/<kind> adapter: kcp objects to contract calls,
one reconciler per kind on
internal/controllers/runtime
internal/services/<svc> adapter: XRPC and HTTP routes to contract
calls; no role logic of its own
internal/app/<root> factory: one composition controller per
app root; wires impls into the bus with
bus.Provide and picks a backend by the
backendRef kind, never by a flag
cmd/ CLI
Imports point down the list only (adapters sit beside each other).
scripts/check-architecture.sh enforces it.
go_idiom: >
No factory types. A constructor takes its config and its dependencies
as explicit arguments (New(cfg, deps)) and returns a concrete struct;
callers accept the contract interface. Every impl states the contracts
it satisfies at compile time (var _ compute.Provider = (*Provider)(nil)).
One package holds one concept; no util or helpers package outside
internal/kit. kcp is a transport like XRPC or the CLI: an adapter
translates it, the impl never sees it. The component.Deps service
locator goes in W3-CONFIG.
testable_backends: >
Every backend package (internal/<concept>/<impl>, and the host
libraries it drives: vmruntime, hostsvc, fcapi, guestnet, guestdisk,
guestimage) runs without kcp, the bus or a controller runtime, so plan
0027 can test it directly: the firecracker Provider boots a VM from a
contract value with no kcp process. A backend takes and returns
internal/contract types and api value types only; the controller in
internal/controllers/<kind> translates objects to contract values and
writes status. Host effects (systemd units over D-Bus, process start
and wait, the filesystem root, the clock) sit behind small interfaces
in internal/contract/host, injected by the composer, so a test can
fake systemd and still run the real backend. scripts/check-architecture.sh
reports any backend whose transitive deps reach client-go,
controller-runtime, multicluster-runtime, kcp-dev, unstructured,
internal/bus, internal/controllers, internal/app, internal/appctl,
internal/kcp* or internal/stack; W5-GATES makes it strict.
cutover: >
Hard cutover and fresh deploy, per AGENTS.md. No migration, carry-over
or rollback for a change of shape.
target_model:
repo_example: |
AtprotoRecord(spec.repoRef -> AtprotoRepo/alice)
bus.Resolve[atprotorepo.RecordWriter](ctx, from=AtprotoRecord/x,
to=AtprotoRepo/alice, repo.write{collections:[...]})
AtprotoRepo/alice spec.backendRef -> LocalRepo/alice
status: did, head, rev, writable, address, conditions
LocalRepo/alice spec.pdsRef -> Pds/main
writes through the Pds signer handle, no token
RemoteRepo/bob spec.pdsRef -> RemotePDS/bsky,
spec.credentialsRef -> MarketIdentity/bob
writes through a lease.Source from the credentials provider
ComponentLink/record-alice-write (written by the composing root)
from: {selector: owner=<root>, kind: AtprotoRecord}
to: AtprotoRepo/alice
capabilities: [{name: repo.write, scope: [<collections>]}]
go_contracts: |
internal/contract/atprotorepo: Repo, RecordReader, RecordWriter,
Subscriber
internal/contract/pds: Accounts, Sessions, Sync
internal/contract/vault: KV, Leases, PKI
internal/contract/policy: Runner
internal/contract/compute: Provider
internal/contract/events: Source, Sink
internal/contract/market: Participant, Offers, Contracts
bus_api: |
type Capability struct { Name string; Scope []string }
func Provide[T any](b *Bus, gk schema.GroupKind, capability string,
open func(ctx context.Context, to commonv1.ObjectRef,
scope []string) (T, error))
func Resolve[T any](ctx context.Context, b *Bus, from commonv1.ObjectRef,
to commonv1.ObjectRef, c Capability) (T, error)
runtime_api: |
type Reconciler[T runtime.Object] interface {
Reconcile(ctx context.Context, req Request, obj T) (Result, error)
}
func For[T runtime.Object](m *Manager, name string, r Reconciler[T],
opts ...Option) error
Options: Owns[C], Watches[R](mapFunc), RequeueAfter for deadlines,
field manager name, finalizer name.
state_rules:
- Read AGENTS.md first. status is todo, doing, done or blocked; the lead
flips statuses here and in docs/plans/0025-defect-register.yaml. One
writer per directory; tracks that touch the same file are serialized
even in a parallel wave.
- Every item ends with gofmt, go build ./..., go vet ./...,
deploy/check.sh --fast and scripts/check-architecture.sh once it
exists. W0-TESTS deletes every Go test; no item in this plan writes a
Go test of any kind. Proof is the build gates plus live runs. Tests
return in plan 0027 as specification tests written from
docs/spec/integration-catalog.yaml. No push.
- Dev builds are tested before an rc build. Only the lead runs prod and
cuts rc tags; task agents never touch prod.
- Never put a secret, token, password, private key or credential value in
any spec, status, link or plan. Refs only.
- Never use /tmp; use ~/.tmp.
- Regenerate with go generate ./api/...; record schema revisions in
deploy/apiresourceschema-revisions.txt.
- An item that removes a ticker, an annotation signal or a System caller
deletes it; it does not leave a fallback path.
waves:
- id: W0
title: "Contracts, codegen, refs, bus and runtime"
status: done
serial: true
tracks: [W0-TESTS, W0-RULES, W0-CODEGEN, W0-REFS, W0-ABSTRACT, W0-CONTRACTS, W0-BUS, W0-RUNTIME, W0-MOUNTS]
gate: "all kinds registered; refs unified; contract packages pure; bus denies an unlinked call; the pilot controller runs on the runtime across root and a later-created root:<app> workspace with no ticker."
- id: W1
title: "Shared kit"
status: done
parallel: true
tracks: [W1-MODERNIZE, W1-CACHE-LEASE, W1-XRPC, W1-LIFECYCLE]
depends_on: [W0]
gate: "no duplicate helper groups remain; one TTL cache, one lease source, one auth retry, one XRPC call path; no nil-context guards."
- id: W2
title: "Concept implementations"
status: done
parallel: true
tracks: [W2-REPO, W2-PDS, W2-VAULT, W2-EVENTS, W2-COMPUTE, W2-POLICY]
depends_on: [W1]
gate: "each abstract kind resolves to a concrete backend; every consumer reaches it through a bus handle; no credentials outside the callee."
- id: W3
title: "Composition roots"
status: done
parallel: true
tracks: [W3-ROOTS, W3-CONFIG, W3-STATUS]
depends_on: [W2]
gate: "one composition controller per root; SSA single writer; ownerRefs on every child and intent object; links written by roots; ConfigFrom gone."
- id: W4
title: "Services"
status: done
parallel: true
tracks: [W4-MARKET, W4-SERVICES]
depends_on: [W3]
gate: "bidder and requester share one participant core; services create intent objects only through bus handles; no service lists kcp objects."
- id: W5
title: "No polling, no signals, architecture gates"
status: done
serial: true
tracks: [W5-NO-POLL, W5-GATES]
depends_on: [W4]
gate: "scripts/check-architecture.sh passes with an empty ticker allowlist outside deploy/obligations.txt and no System caller outside deploy/bus-bootstrap-callers.txt."
- id: W6
title: "Market lexicons"
status: done
serial: true
tracks: [W6-LEXICONS]
depends_on: [W4]
gate: "generated record types produce byte-identical CIDs for every fixture record."
- id: W7
title: "Live proof and integration"
status: done
serial: true
tracks: [W7-PROOF]
depends_on: [W5, W6]
gate: "rc build deployed to this host, hat-9, hat-7 and prod gateway passes the acceptance matrix."
items:
- id: W0-TESTS
title: "Catalog the integration scenarios, then delete every Go test"
wave: W0
track: tests
parallel_with: []
depends_on: []
status: done
decision: >
First write docs/spec/integration-catalog.yaml: one entry per
integration test in test/integration and per soak lane in test/soak,
naming the behavior it asserts (actors, inputs, observable outcome),
not how the code does it. Then move the release asset check out of
TestEveryEmbeddedAssetExtractsAndVerifies into a release-tagged
socialweb-computer dev verb and point deploy/check.sh at it. Then
delete every *_test.go file, test/integration, test/soak, and every
testdata file that only tests read. Remove the go test steps from
deploy/check.sh, the integration job from .gitlab-ci.yml and the
integration test build from scripts/dev-droplet.sh. Rust tests under
rust/ are out of scope and stay.
files:
- docs/spec/integration-catalog.yaml
- "**/*_test.go"
- test/**
- testdata/**
- internal/assets/**
- cmd/socialweb-computer/**
- deploy/check.sh
- .gitlab-ci.yml
- scripts/dev-droplet.sh
done_when: >
git ls-files '*_test.go' is empty; the catalog names every deleted
integration test and soak lane; deploy/check.sh --fast and --full
pass without go test and still verify the embedded assets.
- id: W0-RULES
title: "Record the codegen marker exception and the architecture rules"
wave: W0
track: rules
parallel_with: []
depends_on: [W0-TESTS]
status: done
decision: >
Add the comment exception from decisions.comment_exception to
AGENTS.md. Replace the AGENTS.md integration test rules with: no Go
tests until plan 0027; tests then are specification tests from
docs/spec/integration-catalog.yaml. Add the layout, no_polling, bus and writers rules from this
plan to AGENTS.md as short rules. Create deploy/obligations.txt and
deploy/bus-bootstrap-callers.txt (empty headers only).
files:
- AGENTS.md
- deploy/obligations.txt
- deploy/bus-bootstrap-callers.txt
done_when: >
AGENTS.md states the exception and the four rules; both lists exist.
- id: W0-CODEGEN
title: "Generate deepcopy, scheme registration, GVRs and condition accessors"
wave: W0
track: codegen
parallel_with: []
depends_on: [W0-RULES]
status: done
closes: [D22]
decision: >
Add api/<group>/v1alpha1/doc.go with the deepcopy-gen package marker
for all five groups. Run deepcopy-gen through kube_codegen.sh
gen_helpers from go generate ./api/.... Extend internal/apigen to
write zz_generated.register.go (AddKnownTypes for every kind and list
in APIResources, GroupVersionResource vars, resource name consts) and
zz_generated.conditions.go. Delete every hand-written
schema.GroupVersionResource literal outside api/ and the hand-kept
deepcopy file. Add a drift check to deploy/check.sh and teach
scripts/regen-after-rebase.sh to rerun the generators.
files:
- api/**/doc.go
- api/**/zz_generated.*.go
- api/**/groupversion_info.go
- internal/apigen/**
- deploy/check.sh
- scripts/regen-after-rebase.sh
- hack/codegen.sh
done_when: >
The scheme registers 57 kinds plus lists; go generate ./api/... is
clean twice in a row; no GVR literal exists outside api/.
- id: W0-REFS
title: "One ref package and no endpoints in spec"
wave: W0
track: refs
parallel_with: []
depends_on: [W0-CODEGEN]
status: done
closes: [D02, D19]
decision: >
Create api/common/v1alpha1 with ObjectRef, LocalObjectRef, StrongRef,
SecretRef and CredentialsRef. Replace ComponentRef, RepoRef,
EventStreamRef, ServiceTargetRef, MarketPresenceBidderRef, VaultRef,
ComputeVMVaultRef, RecordRef, BidRecordRef and PolicyStrongRef, and
every plain-string name ref listed in the audit (cluster, computevm,
network, app, server, account, repo). Remove pdsAccessTokenPath,
marketURL, submitEventUrl, firehoseUrls, relayUrls, pdsUrl and
caBundleRef from spec; consumers read producer status.address or a
ref. Fields that stay as user input carry apigen:"userEndpoint".
Normalize Url/URL/Uri casing to URL/URI. Add the apigen tags for
default, minimum, maximum, maxLength, pattern, listType, listMapKey
and enum.
files:
- api/common/v1alpha1/**
- api/v1alpha1/types_*.go
- api/exposure/v1alpha1/**
- api/events/v1alpha1/**
- api/pds/v1alpha1/**
- api/policy/v1alpha1/**
- internal/apigen/**
done_when: >
api/ has exactly the ref types in api/common/v1alpha1; apigen fails on
a spec field named *URL, *Url, *Path or *File without
apigen:"userEndpoint"; schemas carry the new validation keywords.
- id: W0-ABSTRACT
title: "Abstract kinds and their concrete backends"
wave: W0
track: abstract
parallel_with: []
depends_on: [W0-REFS]
status: done
decision: >
Define the kinds in decisions.abstract_kind_set. AtprotoRepo becomes
abstract with spec.backendRef and contract status (did, head, rev,
writable, address, conditions Ready and BackendBound). Add LocalRepo
(spec.pdsRef to Pds) and RemoteRepo (spec.pdsRef to RemotePDS,
spec.credentialsRef). Add abstract Vault over OpenBaoServer and
RemoteOpenBao. Make ComputeVM abstract over FirecrackerVM; move the
backend-specific spec and status out of ComputeVM. Add EventSource
over PdsEventSource, RelayEventSource and JetstreamEventSource. Each
concrete kind carries the contract label. Write the contract for each
abstract kind in docs/ARCHITECTURE.md as MUST rules (fields, meaning,
who writes them).
files:
- api/v1alpha1/types_atprotorepo.go
- api/v1alpha1/types_localrepo.go
- api/v1alpha1/types_remoterepo.go
- api/v1alpha1/types_vault.go
- api/v1alpha1/types_computevm.go
- api/v1alpha1/types_firecrackervm.go
- api/events/v1alpha1/**
- api/v1alpha1/resources.go
- deploy/*-apiresourceschema.yaml
- deploy/*-apiexport.yaml
- docs/ARCHITECTURE.md
done_when: >
Every abstract kind has a written contract; every concrete kind has
the contract label; exports and bindings include the new kinds.
- id: W0-CONTRACTS
title: "Pure Go contract packages"
wave: W0
track: contracts
parallel_with: []
depends_on: [W0-ABSTRACT]
status: done
decision: >
Create the packages in target_model.go_contracts. They import only
api/** and the standard library. Move pure types out of
internal/component (Service, Key, Endpoint) into
internal/contract/component; move the concrete OpenBao clients out of
internal/component/credentials.go. internal/kcplibs/abc is folded into
internal/contract or deleted. Write scripts/check-architecture.sh with
the import-direction rule from decisions.layout using go list.
files:
- internal/contract/**
- internal/component/**
- internal/kcplibs/abc/**
- scripts/check-architecture.sh
done_when: >
scripts/check-architecture.sh passes for internal/contract and api;
internal/component no longer imports kcpserver, kcpstore,
openbaosession, networkdial or os.
- id: W0-BUS
title: "Typed bus, ComponentLink v2 and per-object caller identity"
wave: W0
track: bus
parallel_with: []
depends_on: [W0-CONTRACTS]
status: done
decision: >
Move internal/component/handlers.go and caller.go into internal/bus.
Add Provide/Resolve from target_model.bus_api with capability and
scope checks; HTTP and stream dialing stay for real XRPC and edge
surfaces only. ComponentLink v2 per decisions.links with selector
support, Accepted and ResolvedRefs conditions, and status.links on
consumers. The core run context no longer carries System; each
reconcile carries the reconciled object as caller. System is allowed
only for callers listed in deploy/bus-bootstrap-callers.txt. Deny
counts are exported as a metric and a log line with from, to and
capability.
files:
- internal/bus/**
- internal/component/handlers.go
- internal/component/caller.go
- api/exposure/v1alpha1/types_componentlink.go
- internal/exposure/links.go
- internal/stack/componentcore/componentcore.go
- deploy/bus-bootstrap-callers.txt
done_when: >
A dev core denies an unlinked typed call and an unlinked HTTP call,
allows them after a link is applied, and reports both in
ComponentLink status; componentcore.go:317 no longer sets System.
- id: W0-RUNTIME
title: "Watch-driven controller runtime on multicluster-runtime"
wave: W0
track: runtime
parallel_with: []
depends_on: [W0-CODEGEN, W0-BUS]
status: done
decision: >
Add sigs.k8s.io/multicluster-runtime and
github.com/kcp-dev/multicluster-provider (apiexport provider), pinned.
Serve the provider exports through APIExport virtual workspaces;
add permission claims with identityHash where an export references
another export's kinds. Build internal/controllers/runtime per
target_model.runtime_api: typed reconcilers, Owns/Watches mapping,
SSA status with a field manager, finalizers, rate-limited backoff and
RequeueAfter. Port the RemotePDS controller as the pilot. If the
provider blocks the pilot, implement the same interface on client-go
typed informers and workqueue and record the choice in the commit.
files:
- go.mod
- go.sum
- internal/controllers/runtime/**
- internal/controllers/remotepds/**
- internal/remotepds/**
- deploy/*-apiexport.yaml
- deploy/*-binding.yaml
- internal/stack/componentcore/controllers.go
done_when: >
The pilot reconciles RemotePDS in root and in a root:<app> workspace
created after the core started, retries a failing reconcile with
backoff, writes status by SSA, and contains no ticker.
- id: W0-MOUNTS
title: "Tenant cluster requests go around the kcp mount proxy"
wave: W0
track: mounts
parallel_with: [W0-ABSTRACT]
depends_on: [W0-RUNTIME]
status: done
closes: [D25]
decision: >
kcp main authenticates mount callers itself and verifies the mount
target CA, so tenant k3s bearer requests cannot pass through the kcp
mount. Tenant cluster API requests (/clusters/<home>:clusters/...)
are routed by our own front (internal/clientcertfront) directly to
the cluster's front, forwarding Authorization unchanged and verifying
the cluster server CA. Tenant liveness and shut checks probe the
tenant front directly; a 502 is never read as shut. The revoker
reaches tenants the same way. The kcp mount stays for kcp-side
discovery only.
files:
- internal/clientcertfront/**
- internal/materialiser/**
- internal/clusterprovider/registry.go
- internal/kcpclient/kcpclient.go
- scripts/deploy.sh
- docs/ARCHITECTURE.md
done_when: >
No tenant request depends on kcp mount forwarding; shut() never
treats a 502 as shut.
- id: W1-MODERNIZE
title: "go fix, stdlib idioms and shared helpers"
wave: W1
track: modernize
parallel_with: [W1-CACHE-LEASE, W1-XRPC, W1-LIFECYCLE]
depends_on: [W0-RUNTIME]
status: done
decision: >
Run go fix ./.... Replace option default blocks with cmp.Or or a
Positive helper. Move the 89 byte-identical helper groups into shared
packages (internal/kit/hostport, internal/kit/servetls,
internal/kit/httpjson, internal/kit/policyrun, internal/kit/conds).
Delete every local setCondition, writeJSON, hostnameOnly, sleep,
containsString, mergeDeps, fileSHA256 and serverTLS copy. Rename
colliding packages (two kcpstore, two gatewayapi).
files:
- internal/kit/**
- "every file that holds a deleted helper copy (serialized per directory with other W1 tracks)"
done_when: >
A duplicate-function scan finds no byte-identical group of two or more
across packages; go fix -diff is empty.
- id: W1-CACHE-LEASE
title: "One TTL cache, one lease source, one auth retry"
wave: W1
track: cache-lease
parallel_with: [W1-MODERNIZE, W1-XRPC, W1-LIFECYCLE]
depends_on: [W0-RUNTIME]
status: done
closes: [D14]
decision: >
internal/kit/ttlcache.Cache[K,V] with GetOrLoad (single-flight,
per-entry TTL). internal/kit/lease.Source[T] (Get, Invalidate, renew at
2/3 of lifetime, concurrent-safe). internal/kit/authretry.Do[T]
(retry once on 401 or expired 403 after Invalidate). Replace
kcplibs/common/expiringmap, policyengine/cache.Scope and the about 20
hand-written caches; replace every for attempt < 2 auth loop and every
hand-written refresh-on-expiry path.
files:
- internal/kit/ttlcache/**
- internal/kit/lease/**
- internal/kit/authretry/**
- internal/kcplibs/common/expiringmap/**
- internal/policyengine/cache/**
done_when: >
No map plus mutex plus expiry cache remains outside internal/kit; the
OpenBao token cache dedupes concurrent logins.
- id: W1-XRPC
title: "One XRPC and JSON call path"
wave: W1
track: xrpc
parallel_with: [W1-MODERNIZE, W1-CACHE-LEASE, W1-LIFECYCLE]
depends_on: [W0-RUNTIME]
status: done
decision: >
internal/atproto/xrpc: Query[Out], Procedure[In,Out] with an Auth hook
(Bearer, DPoP, service auth) built on lease.Source, a response size
limit and typed errors. internal/kit/httpjson.Do[Out]. Collapse the
three getServiceAuth copies and the five vault do helpers onto them.
One externalHTTPClient and one TLS client builder.
files:
- internal/atproto/xrpc/**
- internal/kit/httpjson/**
- internal/kit/httpclient/**
done_when: >
getServiceAuth exists once; no hand-built XRPC request remains in
internal/services.
- id: W1-LIFECYCLE
title: "Goroutine ownership, contexts and globals"
wave: W1
track: lifecycle
parallel_with: [W1-MODERNIZE, W1-CACHE-LEASE, W1-XRPC]
depends_on: [W0-RUNTIME]
status: done
closes: [D15, D16, D17]
decision: >
Every goroutine is started by an errgroup or a supervisor that is
joined on stop. Remove every nil-context guard; shutdown uses the
caller's context with WithoutCancel plus a deadline. Remove mutable
package globals (appPdsPublicURLs, the appctl sync.Map, the jwt
ES256 swap); state lives on a struct passed by the composer.
files:
- internal/kit/supervise/**
- "files listed under D15, D16 and D17"
done_when: >
No "ctx == nil" guard remains; every go statement is inside an
errgroup or supervisor; no init() mutates another package.
- id: W2-REPO
title: "AtprotoRepo over LocalRepo and RemoteRepo; records through the bus"
wave: W2
track: repo
parallel_with: [W2-PDS, W2-VAULT, W2-EVENTS, W2-COMPUTE, W2-POLICY]
depends_on: [W1-CACHE-LEASE, W1-XRPC]
status: done
closes: [D01]
decision: >
Controllers for AtprotoRepo (binds the backend, mirrors contract
status), LocalRepo (Provides atprotorepo.Repo through the Pds signer
handle) and RemoteRepo (Provides atprotorepo.Repo through RemotePDS
and a lease from its credentialsRef). AtprotoRecord and
AtprotoRecordWatch controllers resolve RecordWriter and Subscriber
through the bus with the record as caller; they hold no token, DPoP
key or session. The requester and bidder stop writing records through
their own LocalRepo handles and use the same contract.
files:
- internal/contract/atprotorepo/**
- internal/atprotorepo/local/**
- internal/atprotorepo/remote/**
- internal/controllers/atprotorepo/**
- internal/controllers/localrepo/**
- internal/controllers/remoterepo/**
- internal/controllers/atprotorecord/**
- internal/atproto/recordcontroller/**
done_when: >
On the prod gateway the association AtprotoRecord is Ready and its
CID matches the PDS record; an AtprotoRecord without a link is denied
with a named condition.
- id: W2-PDS
title: "One PDS contract over Pds and RemotePDS"
wave: W2
track: pds
parallel_with: [W2-REPO, W2-VAULT, W2-EVENTS, W2-COMPUTE, W2-POLICY]
depends_on: [W1-CACHE-LEASE, W1-XRPC]
status: done
decision: >
internal/pds/local Provides the pds contracts from the in-process Pds
component; internal/pds/remote from RemotePDS. Delete bidder
RecordStore/PDSStore/PDSOptions, requester RecordStore/PDSStore/
LocalRepo, guestidentity DirectoryPDS, appctl pdsclient and the three
PDSResolver interfaces; their callers use bus handles.
files:
- internal/contract/pds/**
- internal/pds/**
- internal/atproto/pdsclient/**
- internal/services/bidder/records.go
- internal/services/requester/repo.go
- internal/guestidentity/pds.go
- internal/appctl/pdsclient.go
done_when: >
Exactly one PDS client implementation exists; no service constructs a
PDS client.
- id: W2-VAULT
title: "One OpenBao client behind the Vault contract"
wave: W2
track: vault
parallel_with: [W2-REPO, W2-PDS, W2-EVENTS, W2-COMPUTE, W2-POLICY]
depends_on: [W1-CACHE-LEASE, W1-XRPC]
status: done
closes: [D13]
decision: >
internal/vault/openbao implements contract/vault on
github.com/openbao/openbao/api/v2 with lease.Source tokens. The Vault
controller binds OpenBaoServer or RemoteOpenBao. Delete
internal/openbao, kcplibs/impl/openbaoclient, credentials/
openbaosession, vaultaccess, the raw X-Vault-Token callers and the
per-package vault login caches. CA pools and certificates are leases
that reload on rotation, including the dispatchertls upstream pool
and the exposure and PDS client verification pools.
files:
- internal/contract/vault/**
- internal/vault/**
- internal/controllers/vault/**
- internal/openbao/**
- internal/kcplibs/impl/openbaoclient/**
- internal/credentials/**
- internal/vaultaccess/**
- internal/vaultcert/**
- internal/tlsreload/**
done_when: >
One OpenBao client package exists; no X-Vault-Token header is set
outside it; a rotated CA is used without a restart.
- id: W2-EVENTS
title: "EventSource backends and sinks through the bus"
wave: W2
track: events
parallel_with: [W2-REPO, W2-PDS, W2-VAULT, W2-COMPUTE, W2-POLICY]
depends_on: [W1-CACHE-LEASE, W1-XRPC]
status: done
closes: [D03]
decision: >
Controllers for EventSource and its three backends. PdsEventSource
subscribes through an AtprotoRepo Subscriber handle. Relay and
Jetstream sources ref a Relay component or a user endpoint.
EventStream delivers to sinks through events.deliver links. Market
comms reference EventSources instead of firehoseUrls and relayUrls.
files:
- internal/contract/events/**
- internal/eventstream/**
- internal/controllers/eventsource/**
- internal/controllers/eventstream/**
done_when: >
A bidder restart keeps its event sources; no firehoseUrls or
relayUrls field exists.
- id: W2-COMPUTE
title: "ComputeVM over FirecrackerVM; release as spec, not annotation"
wave: W2
track: compute
parallel_with: [W2-REPO, W2-PDS, W2-VAULT, W2-EVENTS, W2-POLICY]
depends_on: [W1-CACHE-LEASE, W1-XRPC]
status: done
closes: [D08, D09]
decision: >
contract/compute.Provider with internal/compute/firecracker as the
first sibling. ComputeVM controller binds the backend and mirrors
contract status. Release is spec.release on ComputeVM written by its
creator; accept is a spec field on Bid. status.market splits into
subfields with one field manager each; Bid status has one writer.
Host process state follows systemd D-Bus signals and process wait.
files:
- internal/contract/compute/**
- internal/compute/**
- internal/computevm/**
- internal/controllers/computevm/**
- internal/controllers/firecrackervm/**
- internal/vmruntime/**
- internal/hostprocess/**
- internal/policyaction/computevm.go
done_when: >
No release or accept annotation exists; each ComputeVM status field
has one field manager; no ticker in computevm or hostprocess.
- id: W2-POLICY
title: "Policy runs through the bus and the runtime"
wave: W2
track: policy
parallel_with: [W2-REPO, W2-PDS, W2-VAULT, W2-EVENTS, W2-COMPUTE]
depends_on: [W1-CACHE-LEASE, W1-XRPC]
status: done
decision: >
contract/policy.Runner Provided by the PolicyEngine component.
PolicyEngine, PolicyWorkflow, PolicyWorkflowRun and PolicyTrigger
controllers move to the runtime; waiters watch PolicyWorkflowRun
status instead of polling. Bidder and requester policy helpers move
to one internal/policyengine/runs package.
files:
- internal/contract/policy/**
- internal/policyengine/controller/**
- internal/controllers/policy*/**
done_when: >
No 2s policy loop exists; a run completes and its waiter wakes on the
status event.
- id: W3-ROOTS
title: "One composition controller per app root"
wave: W3
track: roots
parallel_with: [W3-CONFIG, W3-STATUS]
depends_on: [W2-REPO, W2-PDS, W2-VAULT, W2-EVENTS, W2-COMPUTE, W2-POLICY]
status: done
closes: [D04, D05, D06, D07, D10, D12, D21]
decision: >
Split internal/appctl into internal/app/{marketbidder,marketrequester,
gateway,sshgateway,marketnetwork} plus internal/app/compose (shared
SSA child apply, ownerRefs, link writing, status summary). Each root
applies child specs with its own field manager, writes the
ComponentLinks its children need, and owns root status by SSA;
component-owned root status subfields have their own field managers.
ownerReferences on every child and on StateSync, MarketPresence, Bid
and ComputeContract. Suspend per decisions.suspend, with
ApplicationPhase Suspended and a single writer. Requester operator
associations are composed state on the root, not a manual patch, so a
gateway restart keeps them.
files:
- internal/app/**
- internal/appctl/**
- api/v1alpha1/types_application.go
done_when: >
internal/appctl is gone; a direct child edit is reverted by SSA with a
conflict event; a paused bidder still releases its running VM; a
gateway restart keeps the requester association.
- id: W3-CONFIG
title: "Typed specs instead of ConfigFrom and overrides"
wave: W3
track: config
parallel_with: [W3-ROOTS, W3-STATUS]
depends_on: [W2-REPO, W2-PDS, W2-VAULT, W2-EVENTS, W2-COMPUTE, W2-POLICY]
status: done
decision: >
Delete every ConfigFrom, every service Config struct that copies a
spec, the overrides.go type switch and per-kind validation. Services
take their Spec plus bus handles. Defaults and limits are schema
keywords. component.Typed[S] registers each service. The Deps service
locator is removed.
files:
- internal/services/*/config.go
- internal/component/contract.go
- internal/component/runtime.go
- internal/app/compose/overrides.go
done_when: >
No ConfigFrom exists; no service Config has more than the fields its
Spec lacks (runtime handles only).
- id: W3-STATUS
title: "Status objects on the runtime; dead controllers removed"
wave: W3
track: status
parallel_with: [W3-ROOTS, W3-CONFIG]
depends_on: [W2-REPO, W2-PDS, W2-VAULT, W2-EVENTS, W2-COMPUTE, W2-POLICY]
status: done
closes: [D11, D20]
decision: >
Move statusstate, marketpresence, leasecontroller, hostlifecycle,
childapplication, exposure, identitycontroller, ttlctl, postgresctl,
openbaoctl and remoteopenbao onto the runtime. Delete
internal/statusstate/repo.go. Delete the componentcore poll-and-watch
runtime.
files:
- internal/statusstate/**
- internal/marketpresence/**
- internal/leasecontroller/**
- internal/hostlifecycle/**
- internal/childapplication/**
- internal/exposure/**
- internal/identitycontroller/**
- internal/ttlctl/**
- internal/postgresctl/**
- internal/openbaoctl/**
- internal/remoteopenbao/**
- internal/stack/componentcore/controllers.go
done_when: >
Every controller is registered through internal/controllers/runtime;
componentcore has no List+Watch loop.
- id: W4-MARKET
title: "One market participant core under Bidder and Requester"
wave: W4
track: market
parallel_with: [W4-SERVICES]
depends_on: [W3-ROOTS, W3-CONFIG, W3-STATUS]
status: done
decision: >
The core of each service moves out of internal/services: role logic
lives in impl packages (internal/market/participant,
internal/market/bidder, internal/market/requester and the like) that
obey testable_backends, and internal/services/<svc> keeps only the
XRPC/HTTP adapter and the component lifecycle.
internal/market/participant holds the shared market loop pieces:
credential sessions, record encode/decode, comms validation, policy
run handling, StateSync outbox, service-auth. Bidder and Requester
keep only their role logic. Services create Bid, StateSync and
ComputeContract through intent handles on the bus; they never list or
watch kcp objects; their inputs arrive through runtime watches.
files:
- internal/market/**
- internal/services/bidder/**
- internal/services/requester/**
done_when: >
No function body is shared between bidder and requester; neither
service imports a kcp client.
- id: W4-SERVICES
title: "Remaining services on bus handles"
wave: W4
track: services
parallel_with: [W4-MARKET]
depends_on: [W3-ROOTS, W3-CONFIG, W3-STATUS]
status: done
closes: [D18]
decision: >
Same core and adapter split as W4-MARKET for every service below.
gatewayapi (NodeClaim manager on the runtime), guestcontrolplane,
sshserver, ingressproxy, plc, relay, dispatchertls and pds take typed
specs and bus handles. bidder-dids, adopt-joined-node,
initializer-nudge and restart-at signals become spec fields or
conditions.
files:
- internal/services/gatewayapi/**
- internal/gatewayhttp/**
- internal/services/guestcontrolplane/**
- internal/services/sshserver/**
- internal/services/ingressproxy/**
- internal/services/plc/**
- internal/services/relay/**
- internal/services/dispatchertls/**
- internal/services/pds/**
- internal/nodemarket/**
- internal/kcpclient/**
done_when: >
No annotation is read as a control signal; no service has a ticker
over kcp objects.
- id: W5-NO-POLL
title: "Remove every remaining poll"
wave: W5
track: no-poll
parallel_with: []
depends_on: [W4-MARKET, W4-SERVICES]
status: done
decision: >
Includes the clientcertfront Cluster gate recheck (a 2s cache and
recheck from W0-MOUNTS), which becomes a watch on Cluster.
Includes the clientcertfront revoked-list refresh (a 2s background
refresh served stale up to 10m), which becomes a watch.
Delete every remaining ticker or time.After loop that reads state.
List the remaining outbound obligations in deploy/obligations.txt.
clusterprovider, providermain and cloudprovider move to the runtime.
files:
- internal/clusterprovider/**
- internal/providermain/**
- internal/cloudprovider/**
- deploy/obligations.txt
done_when: >
Every ticker in the tree is listed in deploy/obligations.txt.
- id: W5-GATES
title: "Architecture gates"
wave: W5
track: gates
parallel_with: []
depends_on: [W5-NO-POLL]
status: done
decision: >
scripts/check-architecture.sh enforces: import direction with kit as its own layer (strict); backend packages free of kcp, bus and controller runtime (decisions.testable_backends, strict); every impl package carries a compile-time assertion against an internal/contract interface; no ticker
outside deploy/obligations.txt; no System caller outside
deploy/bus-bootstrap-callers.txt; no dynamic client or unstructured
outside internal/controllers/runtime and internal/apigen; no GVR
literal outside api; no annotation constant read as a signal; no
endpoint field in spec without apigen:"userEndpoint". deploy/check.sh
--fast runs it. socialweb-computer dev links prints the resolved link
graph of a workspace.
files:
- scripts/check-architecture.sh
- deploy/check.sh
- cmd/socialweb-computer/**
done_when: >
The gate passes on main and fails on a seeded violation of each rule.
- id: W6-LEXICONS
title: "Market lexicons and generated record types"
wave: W6
track: lexicons
parallel_with: []
depends_on: [W4-MARKET]
status: done
decision: >
Write lexicon JSON for every market and compute NSID in
internal/atproto/market/nsid.go. Keep one embedded lexicon copy. A
custom go:generate tool writes plain JSON record structs validated
through the indigo lexicon catalog; no cbor-gen. Replace the Parse*
and Build* map[string]any code in internal/atproto/market/records.go.
files:
- lexicons/**
- internal/xrpcserver/lexicons/**
- internal/atproto/market/**
- internal/lexgen/**
done_when: >
Every fixture record taken from the live PDSes re-encodes to the same
CID; records.go has no map[string]any.
- id: W7-PROOF
title: "Dev, rc and live proof"
wave: W7
track: proof
parallel_with: []
depends_on: [W5-GATES, W6-LEXICONS]
status: done
decision: >
Fresh deploy from a dev build on this host first, then an rc build to
this host, hat-9, hat-7 and the prod gateway. Repeat
docs/acceptance/0001-auth-modes.md end to end, updated for the new
kinds. Add the acceptance rows of this plan to
docs/spec/integration-catalog.yaml so plan 0027 writes them as
specification tests. No Go tests are written here.
files:
- docs/acceptance/0001-auth-modes.md
- docs/spec/integration-catalog.yaml
done_when: >
Every acceptance row passes on the rc; the catalog holds the old
scenarios plus this plan's rows; the lead flips the closed defects in
docs/plans/0025-defect-register.yaml; no push.
acceptance_matrix:
rule: >
Every row names a live host. A row without one is blocked, never
skipped.
rows:
- id: A1
check: "Tenant k3s cluster scales from zero to N and back to zero through the prod gateway with the hat-7 oauthQr bidder; firehose and relay events observed live."
hosts: [prod gateway, hat-7]
- id: A2
check: "Pod anti-affinity places one VM per host; pausing each winner moves the next VM to the last unpaused bidder; paused winners still release their VMs at scale to zero."
hosts: [this host, hat-9, hat-7, prod gateway]
- id: A3
check: "Gateway AtprotoRecord association is Ready and its CID matches the PDS record; removing its ComponentLink makes it fail with LinkDenied."
hosts: [prod gateway]
- id: A4
check: "A bidder restart keeps its event sources and the root stays Ready."
hosts: [hat-7, hat-9, this host]
- id: A5
check: "A gateway restart keeps the requester association; requester bids are not denied."
hosts: [prod gateway]
- id: A6
check: "A component created in a root:<app> workspace after core start is reconciled within one watch event; no reconcile waits for a resync."
hosts: [this host]
- id: A7
check: "socialweb-computer dev links prints the full link graph; the bus deny counter stays zero across A1 to A6."
hosts: [this host, prod gateway]
- id: A8
check: "A rotated OpenBao CA and leaf certificate are used without a restart."
hosts: [this host]
- id: A9
check: "Tenant clusters work around the kcp mount on kcp main: a ServiceAccount-token kubeconfig and a client-certificate kubeconfig both get and watch through <public host>/clusters/root:<acct>:<home>:clusters; a revoked certificate gets 401 and a revoked DID bearer 403; caddy validate accepts the @tenant matcher; a stopped or deleted cluster answers 503 within one gate change and cuts open watches; shut and reap follow the Cluster's own state, never a 502; a Ready cluster with its apiserver down is not reaped; revocation deletes the did- ServiceAccount through the direct path and defers a stopped one; kcp mount-proxy logs show no tenant traffic."
hosts: [this host, prod gateway]
- id: A10
check: "W1 wire changes hold against the live counterparties: every XRPC call sends Accept application/json; a bidder whose service-auth re-mint fails returns the first 401/403 without an unsigned retry; custody DPoP calls omit an empty DPoP header and fail when custody returns no access token; requester market and vaultaccess reject an oversized 2xx body; requestCrawl bodies are accepted by the relay; an expired ingress nonce is refused; OpenBao logins are shared by concurrent callers and renew before expiry."
hosts: [this host, hat-7, prod gateway]
risks: |
multicluster-runtime and the kcp multicluster-provider are marked
experimental. The W0 pilot decides the backend before any fan-out; the
runtime wrapper keeps the choice out of controller code.
Serving through APIExport virtual workspaces changes how the core reaches
its own kinds. Permission claims and identity hashes must be correct or
controllers see nothing. CVE-2025-29922 requires kcp 0.26.3 or 0.27.0 or
later. The pinned kcp, built from kcp-dev/kcp main at 74e9fb9c (after
v0.33.0), is past every published kcp advisory (the latest,
GHSA-c8w2-fgvx-vhv4 / CVE-2026-61682, is fixed in 0.32.2) and carries the
mount identity-header fix d0ecac6 that no release has yet.
Removing the System caller can deny calls the composing roots do not yet
link. W0-BUS logs every deny with from, to and capability; W3-ROOTS must
write the links before W5 turns the gate on.
Abstract kinds add objects and a bind step per concept. Status mirroring
must not lag the backend by more than one watch event.
Hard cutover: every host is redeployed fresh. Existing PDS repos, DIDs and
OpenBao data are outside kcp and survive; kcp objects are recreated.
Lexicon-generated types can change record bytes and therefore CIDs and
signatures. W6 gates on byte-identical CIDs before any switch.
The fan-out is wide. One writer per directory and the file lists above
are the conflict guard; a track that needs a file outside its list asks
the lead first.
do_not_do:
- "No Go tests of any kind in this plan; no restoring deleted tests."
- "No polling over kcp or component state; no resync-driven retries."
- "No System caller outside deploy/bus-bootstrap-callers.txt."
- "No token, session, DPoP key or credential handed to a caller; the callee authenticates."
- "No URL, endpoint or file path in spec without apigen:\"userEndpoint\"."
- "No annotation as a control signal."
- "No second writer on a field; no read-modify-write Update on status."
- "No implementation selected by a flag; a new backend is a concrete kind and a sibling package."
- "No client-gen, listers, informer-gen, applyconfiguration-gen, kcp cluster-* generators or controller-gen."
- "No external message broker; the bus is in-process and the network bus is atproto."
- "No Result or Option types."
- "No migration code; deploy fresh."
- "No secrets in kcp, links, status or plans; no push."