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
plan: 0008-embedded-deno-and-typescript-denopods
written: 2026-10-07
revised: >
2026-10-07, with the user's answers to the plan's questions (decisions below marked 'decided by the user
2026-10-07'). 2026-10-11, folded from docs/research/2026-10-11-vm-lifecycle/ (user-decisions.md,
plan-0006-0007-linux-exploration.md, linux-proofs.md); decisions from user-decisions.md are marked
'decided by the user 2026-10-11': Linux only with cross-OS seams (the forked DenoPod runner arrives
build-tagged; Windows and macOS runners are plans 0011 and 0012); deno processes are plain children of the
core USER unit and die with it (control-group); explicit deno permissions in the service template, no -A;
kcp on either route (0006 M0-KCP-SPIKE), so dependencies name 0006:M2-KCP-PORT0 (was M1-KCP-PORT0) and
M2-DENOKCP-INPROC no longer depends on 0006:M1-KCP-INPROC; kill -9-only restart tests widened to
`systemctl --user restart` and an upgrade; market network units get their own prefix. 2026-10-12, from
user-decisions.md (the kcp line of 2026-10-12) and 0006's M0-KCP-SPIKE: the kcp route is out of process
(blue/green slot units of the unpatched release kcp behind our own front; 0006 M2-KCP-SLOTS), so
M2-DENOKCP-FORK depends on 0006:M0-KCP-SPIKE and 0006:M0-XOS-GATE instead of 0006:M1-K8S-FORK (closed,
route not chosen) and builds on k8s.io v0.36.4; every kcp client config is the front URL with the
shard-admin context; M3-DOOR-DENOPOD's test no longer reads a state dir the container wrote (fresh
deploy, decided by the user 2026-10-12). The dev/test run mode (dev-run-mode.md) is folded separately.
2026-10-12 (third fold), the user's decisions: the dev run mode is kcp-native now (0006 state rule;
HostProcess objects on systemd-user or proc, a bootstrap layer below kcp, one kcp per dev instance), so
M2-DENOKCP-INPROC proves "deno dies with the core" on both backends and M2-DENOKCP-FORK's runner sets the
devsup attribution env; M4-MARKET-NETWORK-PORT drops the fixed market network ports 2583-2589 and the one-network-per-host lock and
becomes `dev up --profile market-network`; new M1-MARKET-NETWORK-PARALLEL makes today's market network run N per host EARLY, because live
tests collide on fixed ports until it lands; every org repo works on main and is pushed (state rule,
ownership). No item renamed.
2026-10-12 (fourth fold), the user's paradigm correction (0006 `principle`): new `principle` block, a
market-network-per-workstream state rule; M1-MARKET-NETWORK-PARALLEL names the runner's port-file read-back (port-audit.md
blocker 1) and the fixed-port ratchet (0006 M1-NO-FIXED-PORTS); M3-DOOR-DENOPOD's 8788 marked an edge
exception; M4-RUN-REQUEST restated as an application instance. No item added or renamed.
2026-10-12 (fifth fold), from docs/research/2026-10-11-vm-lifecycle/multi-server-to-kcp.md and the user's
answers to its section 7 (user-decisions.md 2026-10-12; plan 0006 principle.decomposition), which override
the research where they differ. New: M1-SSH-GATEWAY-IROH (EARLY: the ssh gateway reaches guests over iroh
like request-vm-ssh) then M1-STRIP-LEGACY-TRANSPORTS (every non-iroh guest transport removed across
atproto-market, hono-compute-provider and socialweb-computer-ssh, the guest JSR registry with them; the
ssh gateway's Live market CI green throughout), M2-DENOPOD-LISTENERS, M2-MARKET-NETWORK,
M3-DOOR-STATE-VAULT. Changed: M1-TS-CLOSURE (the market network's qr entry), M1-MARKET-NETWORK-PARALLEL (decision text only:
port read-back from the port file, never log lines; per-run state for the bidder; the DID from the market network
record; today's shared-kcp market network recorded as transitional), M2-SERVICE-TEMPLATE, M4-RUN-GATEWAY-SSH (after
the iroh port and the strip), M4-RUN-REQUEST (reworked: a fresh kcp per invocation with a durable state
dir, `release|resume <name>` restart that kcp; the research's persistent requester app is NOT taken),
M4-MARKET-NETWORK-PORT (the strict market network: a MarketNetwork application whose children each run their own kcp; the
research's nested-workspace market network is rejected). No item renamed or removed; no status or evidence changed.
queued: >
OPEN. The plan 0005 gate is satisfied: plan 0005 is complete with all 40 items done and its acceptance
checks ACC-ZERO, ACC-MESH and ACC-NOSSH done with evidence (verified 2026-10-12). Only each item's own
depends_on (within this plan and on plans 0006 and 0007) now governs when it starts.
source: >
Split out of plan 0006 (same user request of 2026-10-07): Go and TypeScript are both first class in
socialweb-computer. The binary carries a pinned deno and the TypeScript codebase; every long-running
TypeScript service runs as a DenoPod reconciled by a deno-kcp provider that this repo now owns (the
user decided to fork and vendor deno-kcp); kcp records which services run, and the core binary and its
deno processes can be upgraded while VMs keep running.
principle:
stated: >
Decided by the user 2026-10-12, the ORGANIZING PRINCIPLE of plans 0006-0008 (full text and rules: plan
0006 `principle`): kcp is a core part of APPLICATIONS; the virtual Kubernetes clusters project is the one
traditional use.
in_this_plan:
- "DenoPods are how an application's TypeScript components become objects of that application's own kcp. An application instance (the gateway with its door, a bidder handle, a requester `run request`, a market network) = its own kcp + a manifest set (DenoPod values over M2-SERVICE-TEMPLATE) + a profile."
- "Modes are profiles over one manifest set, rendered by 0006 M2-TEMPLATE: a bidder in OAuth QR mode against a real account has no PDS pod; a bidder in the market network has its own PDS pod."
- "Applications talk to each other ONLY over the atproto market protocol, never by sharing a kcp: a DenoPod never reads another application instance's objects, and the servicenames table a pod resolves through holds only its own instance's services."
- "Every DenoPod listener behind the edge binds port 0; the runner reads the pod's port file and records status.address; peers resolve by service name through kcp (0006 principle.port0_rule; port-audit.md ranked blocker 1). Only edge entry points keep known addresses (the door's 8788 behind caddy)."
- "The market network (deploy/market-network) is the seed of this model and the vehicle for parallel workstreams: M1-MARKET-NETWORK-PARALLEL and M4-MARKET-NETWORK-PORT are what every workstream tests in."
- "How today's multi-server deno processes split into DenoPods per application is folded (fifth fold 2026-10-12; 0006 principle.decomposition): one root object per application (0006 M2-APP-CONTROLLER); STRICT kcp per application, inside a market network too (the market network is a MarketNetwork application whose children each run their own kcp, M4-MARKET-NETWORK-PORT); every listener reports into DenoPod.status.listeners (M2-DENOPOD-LISTENERS); network values only through the composer and one fetch adapter (M2-MARKET-NETWORK); the requester is a fresh kcp per invocation with a durable state dir (M4-RUN-REQUEST); secrets only in each application's OpenBao (M3-DOOR-STATE-VAULT for the door); iroh is the only guest transport (M1-SSH-GATEWAY-IROH, then M1-STRIP-LEGACY-TRANSPORTS)."
state_rules:
- TypeScript lives in this repository (decided by the user 2026-10-12): the closure's ten repositories were vendored with their history under ts/<repository>/ and the originals are frozen; a path this plan names under ts/ is changed here, never in the original.
- Item status (todo, doing, done, blocked) lives here; the git history is the evidence. No evidence lists are added (user, 2026-10-12).
- status is one of todo, doing, done, blocked.
- An item is done when its done_when holds on main; the commits say how.
- Open items are mirrored in NEXT.yaml (P8-*).
- A decision marked "decided by Claude, user may veto" was not confirmed by the user; one marked
"decided by the user 2026-10-07", "decided by the user 2026-10-11" or "decided by the user 2026-10-12"
was.
- >
RFP flow is the spine (org-root CLAUDE.md). The market network's guests come up only through `run bidder`
and the requester's RFP, bid, accept and cloud-init user_data; no item boots a guest any other way.
- >
Linux only (decided by the user 2026-10-11). OS-specific process code (Setpgid, Pdeathsig) lives in
GOOS-tagged files; darwin and windows stubs return ErrUnsupported until plans 0012 and 0011, and
0006:M0-XOS-GATE keeps both building and vetting.
- An item names its dependencies in depends_on; an item may not start while one of them is not done.
Cross-plan dependencies are written <plan>:<item id>.
- >
Dev run mode is kcp-native now (decided by the user 2026-10-12; 0006 state rule): every test harness
and every market network passes host-backend=proc and runs inside its own dev instance (0006 M1-DEV-INSTANCE); a
production-shaped run passes systemd-user. DenoPods stay children of the core on both backends.
- >
Market network per workstream (decided by the user 2026-10-12; 0006 state rule). Each workstream's live tests run
in the task's own market network: a git worktree of this repository (on its own branch) brings up
its own isolated atproto network (fake PLC, relay, PDSes,
ingress proxy, bidders, requesters) through M1-MARKET-NETWORK-PARALLEL now and `dev up --profile market-network` later. No
workstream test uses a real account or the shared real bidder; bobvmbuilder + alice are a final soak
only.
- >
Every org repo works on `main` and is pushed to GitHub as work lands (decided by the user 2026-10-12).
ownership:
owned: >
All code in socialweb-computer-kcp, the prod deployment, the vendored deno-kcp market network, the
atproto-market requester libraries, and (decided by the user 2026-10-07) the forked copies of
deno-kcp and kcp-libs this plan brings into the repo. hono-compute-provider and atproto-market may
be changed by commit on main, pushed to GitHub as work lands (decided by the user 2026-10-12; replaces
"never pushed").
not_owned: >
The market protocol records and lexicons; the core architecture (nodes only ever born through
requester -> RFP -> bid -> accept -> cloud-init user_data).
research:
sshai_policy_engine:
commit: "publicdomainrelay/sshai a00ab8c9ff721e22f95855b9164b8d031e4cf295 (main, 2026-06-10)"
findings:
- "src/policy_engine/common/deno_embed_linux_amd64.go:1-7: //go:build embed_deno && linux && amd64, //go:embed deno_embedded/deno-linux-amd64.zip; one file per platform; deno_noembed.go returns nil under !embed_deno"
- "common/deno_manager.go:20 pins v2.1.4; :51-90 embedded -> PATH -> GitHub download; :95-135 extracts to <cache>/deno-embedded/deno; :112-115 reuses an extracted file with no version or hash check"
- "The TypeScript is NOT in an embed.FS in sshai: ts/ is a separate deno program; bundled actions are copied by the Dockerfile to /bundled-actions (Dockerfile:71-73, workflow.go:659-663); Go spawns deno run per step (workflow.go:893)"
ts_embedding_precedent: "kcp-libs f6911d2 impl/assets/dns.go:8-11 go:embeds dnsshim.ts and dnsprobe.ts, materialised by the deno-kcp provider as the KCP_SHIM preload"
live_findings_2026_10_07:
- "commit 13349d6: on hat-8 a bidder restarted onto new hono-compute-provider code kept writing the old provider's bid_config token path and booting the old node image until its DENO_DIR v8_code_cache_v2 was moved aside (2026-10-07T22:22Z); the launcher now runs deno with --no-code-cache (deploy/bidder/socialweb-computer-bidder-run). A shared, long-lived DENO_DIR can run stale code after an upgrade."
- "the same launcher runs the bidder with --node-modules-dir=none, so the bidder runs from the global DENO_DIR without a node_modules tree"
deno_kcp:
commit: "deno-kcp 25d10f92 (local), market network vendored at f5b7c10c; kcp-libs f6911d2"
findings:
- "cmd/deno-kcp-provider/main.go:17 imports internal/provider (not importable from another module); :75-109 flags incl. pod-timeout 5m and token-ttl 1h"
- "go.mod:85 replace kcp-libs => ../kcp-libs; deploy/denokcp/kcp-libs-no-default-issuer.patch is carried here today"
- "kcp-libs impl/execrunner/supervisor.go:141 Setpgid without Pdeathsig, :220-229 state.json, :249-268 and :394 recover a run by pid and start ticks, :156 and :322 stop is SIGKILL of the group; pod.go:61-62 maps only Timeout 0 to 5m and :101-105 enforces only > 0; pod.go:184-196 injects KCP_TOKEN, KCP_SERVER, KCP_WORKSPACE"
- "the market network's DenoPods name host paths: 70-bidder.yaml:14-15 SERVICE_ENTRY/SERVICE_CWD under /home/johnandersen777/src/publicdomainrelay-kcp, rewritten by apply.sh:171; :35-92 the supervisor script every service repeats; :78-79 prepends --preload KCP_SHIM"
ts_closure_measured:
- "deno info --json over the door, hono-bidder and the ssh door on 2026-10-07: 389 local files, 1842618 bytes, across atproto-market, deno-worker-sandbox, did-key-ingress-proxy, hono-compute-provider, hono-jsr, hono-pds, policy-engine, typescript-helpers, socialweb-computer-kcp, socialweb-computer-ssh; 234-237 remote modules and 211-261 npm packages per entry; atproto-market/node_modules 143 MiB on disk"
- "deno 2.9.3 (Dockerfile:142, hat-8 host); `deno run --help`: --preload takes a list of files"
multi_server_to_kcp_2026_10_12:
file: "docs/research/2026-10-11-vm-lifecycle/multi-server-to-kcp.md (sections 2.2-2.9, 4.4-4.10)"
findings_that_change_items:
- "F2 (-> 0007 M1-MARKET-IDENTITY, M4-RUN-REQUEST): interactive stdin/stdout steps cannot run in a pod - the market network's DenoPods have stdin null (70-bidder.yaml:85) and pass --skip-qr; request-vm-ssh's association prompt is on stdin (:430)."
- "F5 (-> M2-DENOPOD-LISTENERS, M2-MARKET-NETWORK, M1-MARKET-NETWORK-PARALLEL): peers are found by fixed ports (2583-2589), stdout scraping (accept.sh:463-467 greps `localPDS plc` for the bidder DID; tests read the `bidder_ready` line, hono-bidder/mod.ts:686-691), four fetch patches (hono-bidder :74-95, request-vm-ssh :48-60, market-requester :82-95, test/fetch-interceptor.ts) and a shim that reads OTHER workspaces on a miss (market network README.md:128-130)."
- "F6 (-> M2-SERVICE-TEMPLATE): per-user state under $HOME is shared by every instance (port-audit B11); a DenoPod must not pass HOME through."
- "F7 (-> M4-RUN-REQUEST): request-vm-ssh in QR mode never begins its ephemeral repo's serve (:365-367), so the QR requester has no repo component."
- "F8 (-> M3-DOOR-STATE-VAULT): the door keeps NodeClaims in kcp, but its OAuth sessions (oauth-sessions.json, door mod.ts:50), requester key (requester-key.hex, :72) and per-claim secrets keys (node-market :383) are a file, a hex file and process memory."
- "Today's market network puts four applications in ONE kcp (workspaces root:global, root:relay, root:alice, root:bob; market network README.md:7-22), which the user's strict rule (2026-10-12) forbids; M4-MARKET-NETWORK-PORT retires it, M1-MARKET-NETWORK-PARALLEL only parallelises it."
items:
- id: M1-DENO
milestone: M1
title: A pinned deno inside the binary; a DENO_DIR that can never hold another version's code
depends_on: ["0006:M1-ASSETS"]
decision: >
deno 2.9.3 per platform is an internal/assets entry. internal/denorun builds every deno argv. From
the live finding (13349d6): every run passes --no-code-cache, AND each DENO_DIR is keyed by the
sha256 of the TypeScript it serves (<state>/deno/<closure sha256>/ under the account's state dir,
0006:M1-INSTALL), so an upgrade starts from an empty code cache by construction and the old directory
is removed once no DenoPod names it and it is not the current or previous binary's closure (the same
rule as 0006:M1-ASSETS' runtime GC).
Production runs add --cached-only and --frozen (no jsr.io or npm at runtime; lockfile drift is a
start failure) and DENO_NO_UPDATE_CHECK=1. Decided by Claude, user may veto.
files: ["internal/assets (deno)", "internal/denorun/", "internal/versions/versions.json"]
test_first: >
internal/denorun TestAnUpgradedClosureNeverRunsTheOldCodeCache (run a fixture entry, swap its source
for a new closure sha256, run again: the new behaviour, even with the old DENO_DIR left on disk) and
TestAProductionRunNeverReachesTheNetworkForItsModules (under unshare -rn). Red first.
done_when: "both green; `version` prints deno 2.9.3 and its sha256."
risks: ["A cache missing one module fails at first import; M1-TS-CLOSURE runs every entry with --cached-only at build time."]
status: done
evidence:
- id: M1-TS-SOURCE
milestone: M1
title: Where the TypeScript comes from - the embedded archive in production, the org root otherwise
depends_on: [M1-DENO]
decision: >
Decided by the user 2026-10-07: production builds run the TypeScript from the embed.FS archive;
non-production builds take the org-root path from an env var with an auto-detected fallback that
walks up ../ until the org root is found. Details decided by Claude, user may veto: the variable is
SOCIALWEB_COMPUTER_ORG_ROOT (an option in internal/clicfg); the walk starts at the working
directory, then at the executable's directory, and stops at the first directory holding
ts/typescript-helpers/, ts/atproto-market/ and socialweb-computer-kcp/ as real directories; a symlinked
sibling is refused by name, because the live market test proved a symlinked sibling is not a
directory to Deno.readDir and the guest's tunnel then 404s. Non-production runs allow network
module fetches into a DENO_DIR keyed by the org root path. Production is the build without the
`dev` tag; it has no org-root option at all.
files: ["internal/tsbundle/source.go (Source interface: Embedded, OrgRoot)", "internal/clicfg/options.json"]
test_first: >
internal/tsbundle TestTheOrgRootIsFoundByWalkingUpAndASymlinkedSiblingIsRefused and
TestAProductionBuildHasNoOrgRootOption; red first.
done_when: "tests green; a dev build run from a worktree three levels deep finds the org root."
risks: ["A worktree's own sibling imports (../../) do not resolve from a worktree (plan 0005 evidence); the walk finds the org root above the worktree, which is the checkout the user runs."]
status: done
evidence:
- id: M1-TS-CLOSURE
milestone: M1
title: The production TypeScript closure and its module cache as one deterministic embedded archive
depends_on: [M1-TS-SOURCE]
decision: >
cmd/socialweb-tsbundle (build tool, not shipped) reads deploy/ts-entries.yaml (the door and its
undici-preload.ts, hono-bidder, hono-socialweb-computer-ssh, the request CLI of M4-RUN-REQUEST, the
market network's hono-plc, hono-atproto-relay, hono-did-key-ingress-proxy and hono-pds, and
did-key-associator's hono-backend for the market network's qr DenoPod, 0006 M2-APP-CONTROLLER), stages each
needed sibling repo in org-root layout, fills one DENO_DIR with `deno install --frozen
--node-modules-dir=none --entrypoint` per entry (the flag the bidder launcher already runs with,
13349d6, so no node_modules symlink tree is shipped), runs each entry once with --cached-only, and
writes a tar (sorted, mtime SOURCE_DATE_EPOCH, uid/gid 0) compressed with zstd plus manifest.json
(every repo's git commit, every file's sha256, every entry). A dirty sibling repo is refused. The
archive and manifest are files in an embed.FS. An entry that cannot run without node_modules is
recorded by name with the reason, not silently given one.
files: ["cmd/socialweb-tsbundle/", "deploy/ts-entries.yaml", "internal/tsbundle/ (embed, extract to <state>/ts/<sha256>/, read-only)", "deploy/check.sh"]
test_first: >
TestTwoBundlesOfTheSameCommitsAreByteIdentical, TestAnExtractedTreeWhoseFileChangedIsRefused,
TestADirtySiblingRepoIsRefusedForARelease; red first.
done_when: "archive built; size recorded (feeds 0006:M0-SIZE-BUDGET); every entry starts offline from the extracted tree."
risks: ["The door today runs with node_modules (nodeModulesDir auto); its suite is run under --node-modules-dir=none before the switch."]
status: done
evidence:
- id: M1-MARKET-NETWORK-PARALLEL
milestone: M1
title: "EARLY: today's market network runs N per host - no fixed ports, no one-network-per-host check - so live tests in parallel worktrees stop colliding"
depends_on: ["0006:M1-PORT-LEASE-INSTANCE"]
decision: >
Decided by the user 2026-10-12: live tests collide on fixed ports until the dev-instance / market-network-port
items land, so those come early; M4-MARKET-NETWORK-PORT drops the fixed ports 2583-2589 and the one-network-per-host lock.
Decided by Claude, user may veto: this item does that for TODAY's market network (internal/marketnetwork driving the
vendored deno-kcp market network, accept.sh and apply.sh) without waiting for M4-MARKET-NETWORK-PORT's long dependency
chain. Every fixed market network port - the services' 2583-2589 (internal/marketnetwork/live_test.go:18; the vendored
manifests; accept.sh), the deno-kcp kcp 6443 and kine 23791 (deploy/denokcp/start-kcp.sh:5-6, already
env-overridable), and every place that names them (live_m3_test.go:38-41, :96, :582-599) - is read from
a per-run market network record whose ports are leased from the per-user lease file keyed by instance
(0006:M1-PORT-LEASE-INSTANCE; instance = the dev instance id once 0006:M1-DEV-INSTANCE lands, the
worktree-derived id before) or bound at 0 and read back from the port file the service writes on
onListen (never a log line); the market network's *.localhost names stay, resolved per market network. Port 0 is the rule, leases the exception
(0006 principle.port0_rule, user 2026-10-12): the first change is port-audit.md ranked blocker 1 (B2) -
the deno-kcp runner reads each pod's <runDir>/port-bound-to (PORT_FILE set per run, B3), records it as
the DenoPod's status.address, and servicenames builds the table from status.address, so peers resolve
by service name through the market network's kcp and never by a configured port. Every literal this item removes
deletes its line from 0006 M1-NO-FIXED-PORTS' ratchet. This item is what makes 'market network per workstream'
(state rule) possible: each task tree's live tests run in its own market network. The livegate "port held by another process"
refusal (live_test.go:115) is replaced by the lease. Market network units and processes carry the run id
(socialweb-market-network-<runid>-...), so two market networks never touch each other's processes. Guests still come up only
through the market network's requester (runComputeContract -> RFP -> bid -> accept -> cloud-init) against the
market network's own bidder; the market network adds no provisioning path.
Fifth fold 2026-10-12 (multi-server-to-kcp.md 6, decided by Claude, user may veto; status and evidence
untouched): the port file this item reads back is the precursor of M2-DENOPOD-LISTENERS' listener
report (tcp, unix and relay ingress URLs), which replaces it once the owned runner exists; the market network
bidder's key, repo KV and session live in its per-run dir, never $HOME defaults (port-audit B11); and
accept.sh reads the bidder DID from the market network record (written from the bidder's report), not by grepping
its stdout (accept.sh:463-467). Today's market network keeps its four applications in ONE kcp and its shim reads
other workspaces (README.md:128-130), which the user's strict rule of 2026-10-12 forbids; this item
only parallelises that market network and adds no new cross-application coupling, and M4-MARKET-NETWORK-PORT retires it.
files: ["internal/marketnetwork/ (market network record, leases, live tests read ports from it)", "deploy/denokcp/start-kcp.sh (ports from the record)", "deploy/market-network/ (manifests templated on ports; accept.sh, apply.sh read the record)", "deploy/denokcp/vendor.json (adapted files recorded)"]
test_first: >
internal/marketnetwork TestNoNetworkPortIsALiteral (scans the market network's Go, shell and manifests for 2583-2589, 6443
and 23791 outside the record; red on today's tree), TestTwoNetworkRecordsLeaseDisjointPorts; behind
livegate on hat-8: TestTwoNetworksOnOneHostEachRentAGuestThroughTheRFPFlow (two worktrees' market networks at once,
each guest born via runComputeContract; both pass; no ssh to a guest). Red first.
done_when: "the live run of two market networks passes on hat-8 with its readings (ports, run ids, guest names, UTC); M1-MARKET-NETWORK's accept checks still pass on one market network; 0006 M1-NO-FIXED-PORTS' ratchet holds no market network entry."
risks: ["Two market networks double the guests on hat-8 (memory, /dev/kvm); the run records host memory before and after and refuses a third market network when free memory is below the market network's guest budget."]
status: done
evidence:
- id: M1-SSH-GATEWAY-IROH
milestone: M1
title: "EARLY: the ssh gateway (socialweb-computer-ssh) reaches guests over iroh like request-vm-ssh, so nothing asks for the legacy tunnel transport"
depends_on: []
decision: >
Decided by the user 2026-10-12: strip every non-iroh guest transport as legacy, but FIRST, in a small
early item, port the ssh gateway, because it is the one caller that still asks for one and its Live
market CI depends on it: socialweb-computer-ssh lib/socialweb-computer-requester-inproc/mod.ts:268 passes
userData {transport: "tunnel"} to runComputeContract, and its session provider (:220-228) polls
readiness and runs the session over the guest's websocket tunnel (pollGuestReady,
runSessionOverTunnel). Decided by Claude, user may veto: it asks for the default iroh transport and its
session provider reaches the guest the way request-vm-ssh does on iroh (the dumbpipe ProxyCommand and
the guest's iroh ticket from the contract), reusing atproto-market's iroh session code rather than a
copy (a shared helper moves into atproto-market if it is CLI-local today). Nothing else in the gateway
changes; its kcp-native move stays M4-RUN-GATEWAY-SSH, after this work. The Live market workflow
(socialweb-computer-ssh .github/workflows/live.yml, `deno task test:live`) stays green throughout.
Guests still come up only through runComputeContract (RFP spine); the gateway adds no transport.
files: ["socialweb-computer-k8s/lib/socialweb-computer-requester-inproc/mod.ts", "ts/socialweb-computer-ssh/test/ (iroh session provider)", "ts/atproto-market/lib (iroh session helper, if shared)"]
test_first: >
socialweb-computer-ssh unit test 'the requester asks for the iroh transport and its session provider
dials through the iroh ProxyCommand, never a websocket tunnel' (red on today's tunnel transport); then
the Live market workflow green on the iroh path.
done_when: "unit test green; a Live market workflow run green after the change (run URL and UTC recorded); no `tunnel` transport requested anywhere in socialweb-computer-ssh; pushed on main."
risks: ["The ProxyCommand needs dumbpipe on the gateway host; request-vm-ssh fetches a pinned dumbpipe today (request-vm-ssh/mod.ts:107-117) and the same pinned fetch is reused."]
status: done
evidence:
- "merged to main 2026-10-12: atproto-market b78adc1 (dumbpipeConnectArgv shared by sshProxyCommandFor), socialweb-computer-ssh fcbe82c (tests) + d561845 (requester-inproc asks iroh; bridge spawns `dumbpipe connect <ticket>` from the iroh:// onNetwork address, refuses any non-iroh address; vm.terminated ends the poll/session and kills dumbpipe)"
- "tests red then green: atproto-market transport_address_test.ts 6/6; socialweb-computer-ssh requester_iroh_test.ts 7/7 (red: 3 type errors + transport tunnel), unit suite 84/84; re-run on merged main 7/7"
- "live (local docker, deno task test:live) 2026-10-12T03:22:57Z-03:23:20Z PASS 1/1: guest addressed by its iroh ticket, gateway's own iroh dial ran the session, dispatcher saw 0 xrpc.tunnel requests, no guest container left; the same test on the old tunnel code failed 'the guest must be addressed by its iroh ticket'"
- "left: the GitHub Live market workflow run on main (CI deferred by the user until the kcp-native move is done)"
- id: M1-STRIP-LEGACY-TRANSPORTS
milestone: M1
title: "Strip every non-iroh guest transport - tunnel, fedproxy-ssh, fedproxy-web, wootty, websocat ProxyCommand, relay-fqdn guest addresses, the guest JSR registry - across atproto-market, hono-compute-provider and socialweb-computer-ssh"
depends_on: [M1-SSH-GATEWAY-IROH]
decision: >
Decided by the user 2026-10-12: strip all non-iroh guest transports as legacy, only after
M1-SSH-GATEWAY-IROH, with tests that no non-iroh transport remains, and the ssh gateway's Live market CI
green throughout. Removed (deleted, not flagged off; decided by Claude, user may veto): the
cloud-init-common tunnel, fedproxy-ssh, fedproxy-web and wootty modules (atproto-market
lib/common/cloud-init-common/mod.ts:1227-1230 and their context fields; iroh, secrets and k3s stay); the
websocat ProxyCommand paths (requester-xrpc sshTunnelArgs' websocat form and request-vm-ssh's websocat
download, :107-117); relay-fqdn guest addresses (a guest addressed through the relay FQDN instead of its
iroh ticket); the guest JSR registry (hono-compute-provider compute-provider-firecracker :574-590, a TCP
0.0.0.0:0 listener per provider, injectJsrUrl and the JSR_URL / hono-jsr context fields). Callers cut
to iroh: request-vm-ssh (`--user-data-transport` is removed with the reason legacy-transport, the one
removal 0006 M2-APP-CONTROLLER's option map accepts), lib/compute-contract-gateway-xrpc,
compute-contract-full-flow, socialweb-computer-ssh. Tests that exercise a legacy transport
(cloud_init_snapshot_test, bidder_ssh_relay_test, bidder_cross_platform_integration_test,
bidder_prod_integration_test, hono-compute-provider preinstall_manifest_test) are cut to iroh or deleted,
each with its reason in the commit message. Guests still come up only through cloud-init user_data from
the RFP flow; iroh is installed by the iroh UserDataModule.
files: ["ts/atproto-market/lib/common/cloud-init-common/mod.ts", "ts/atproto-market/lib/requester-xrpc/mod.ts", "atproto-market/lib/compute-contract-gateway-xrpc/mod.ts", "ts/atproto-market/request-vm-ssh/ (mod.ts, cli-args-env.ts)", "atproto-market/compute-contract-full-flow/", "ts/atproto-market/test/no_legacy_guest_transport_test.ts", "ts/hono-compute-provider/lib/compute-provider-firecracker/mod.ts", "ts/hono-compute-provider/test/", "ts/socialweb-computer-ssh/test/"]
test_first: >
atproto-market test/no_legacy_guest_transport_test.ts (listUserDataModules() names no tunnel,
fedproxy-ssh, fedproxy-web or wootty module; no source under lib/, request-vm-ssh/ or
compute-contract-full-flow/ names websocat, fedproxy-client, wootty or JSR_URL); hono-compute-provider
'no guest JSR registry is served and no user_data names JSR_URL'; socialweb-computer-ssh 'no transport
other than iroh is requested'. Red on today's tree.
done_when: "the three scan tests green; every suite of the three repos green; the ssh gateway's Live market workflow green after the change (run URL, UTC); one market network run (guest via runComputeContract) reaches an ssh session over iroh; pushed on main in all three repos."
risks: ["Plan 0012's research lists the legacy modules and forwards the JSR registry per VM (0012 lines on cloud-init-common and the guest-facing services); its M0-REVISIT re-reads them (NEXT.yaml open_notes)."]
status: done
evidence:
- id: M2-DENOKCP-FORK
milestone: M2
title: Fork deno-kcp and kcp-libs into this repo and own them
depends_on: ["0006:M0-KCP-SPIKE", "0006:M0-XOS-GATE"]
decision: >
Decided by the user 2026-10-07: fork/vendor the deno-kcp provider into this repo and own the copy.
Decided by Claude, user may veto: deno-kcp's api, internal/provider, internal/denopod, denojob,
denorun and the rest it needs, and the kcp-libs packages they import, are copied into
internal/denokcp/... and internal/kcplibs/... with import paths rewritten, the carried
kcp-libs-no-default-issuer.patch applied as an ordinary commit, and deploy/denokcp/vendor.json
recording the upstream commits they started from. They build with the rest of the module on today's
k8s.io v0.36.4: the kcp route is out of process (decided by the user 2026-10-12, 0006 M0-KCP-SPIKE), so
0006:M1-K8S-FORK is closed as not chosen and this item needs only the route record and the cross-OS
gate (0006:M0-XOS-GATE) its build-tagged runner must keep green (M0-KCP-SPIKE also measured deno-kcp
25d10f92 and kcp-libs f6911d2 green on the fork, so either module graph builds). The CRDs (DenoPod,
OpenBao, ...) keep their group deno.computer so existing manifests and the market network apply unchanged.
Upstream is no longer followed automatically; a later upstream change is pulled by a recorded
cherry-pick. Cross-OS seam (decided by the user 2026-10-11): kcp-libs impl/execrunner's process
attributes (supervisor.go:141 Setpgid, untagged today, which breaks GOOS=windows) move into
runner_unix.go / runner_windows.go behind a small interface; the windows file returns ErrUnsupported
until 0011 M1-DENOPOD-RUNNER-WINDOWS, and darwin keeps the unix file until 0012
M1-DENOPOD-RUNNER-DARWIN replaces what it needs, so 0006:M0-XOS-GATE stays green when the copy lands.
Dev run mode (decided by the user 2026-10-12; detail decided by Claude, user may veto, dev-run-mode.md
5): runner_unix.go also sets SWC_UNIT=<the core's unit name> in every DenoPod process's environment,
so devsup (0006 M2-HOST-BACKEND-PROC) attributes a deno process that left the core's process group to
the core unit and kills it with the core.
files: ["internal/denokcp/", "internal/kcplibs/ (execrunner runner_unix.go, runner_windows.go)", "deploy/denokcp/vendor.json", "deploy/denokcp/build-provider.sh (deleted once the in-process provider passes M2-DENOKCP-INPROC)"]
test_first: >
The forked packages' own tests, moved with them and green; plus internal/denokcp
TestTheForkRecordsTheUpstreamCommitsItStartedFrom, and check.sh's cross-OS build+vet step green with
the copy in the tree. Red until the copy exists.
done_when: "go test ./internal/denokcp/... ./internal/kcplibs/... green; vendor record check green; GOOS=darwin and GOOS=windows build and vet green."
risks: ["deno-kcp's third_party/openbao and baoembed (an openbao module-path trick) are not copied; the provider talks to a running bao over its API, which is what socialweb-computer runs."]
status: done
evidence:
- id: M2-DENOKCP-INPROC
milestone: M2
title: The owned deno-kcp provider running inside socialweb-computer, upgradable without touching VMs
depends_on: [M2-DENOKCP-FORK, M1-DENO, "0006:M2-KCP-PORT0", "0006:M2-HOST-BACKEND-PROC"]
decision: >
Both backends (decided by the user 2026-10-12: dev run mode kcp-native now; details decided by Claude,
user may veto): "deno processes die with the core" is proved on systemd-user (control-group kill) and
on proc, where execrunner's children sit in their own process groups with no Pdeathsig
(supervisor.go:141) and devsup's subreaper attribution by SWC_UNIT (M2-DENOKCP-FORK) kills them with the
core unit. DenoPod stays its own kind (TypeScript services that die with the core); the HostProcess
kind (0006 M2-HOSTPROCESS-KIND) generalises its reconciler shape for host processes that outlive the
core, and a DenoPod is never placed as a HostProcess.
internal/stack/denoruntime runs the provider in-process with: deno-bin the extracted deno, runs-dir
<state>/denokcp/runs, the runner timeout NEGATIVE (disabled; services are long-running and a
DenoPod that wants a limit sets activeDeadlineSeconds), and the kcp client config internal/kcpserver
hands back (the kcp front URL with the shard-admin context, 0006 M2-KCP-SLOTS; never the per-start
kcp-admin token, so a kcp slot roll does not invalidate it). Upgrade semantics (decided by the user 2026-10-07: the core binary and
its deno processes are upgradable while VMs keep running; decided by the user 2026-10-11: the core runs
as a user unit of a lingering account): deno processes are plain children in the core unit's cgroup
and die with it on any stop, restart or crash of the core (default KillMode=control-group; linux-proofs
E1-neg: plain children die on restart, kill -9 and stop); on start the provider finds every DenoPod
whose status.runID names no live process and restartPolicy Always starts it again from the new closure.
Nothing that holds a VM is a deno process (each VM is its own transient service, plan 0007
M2-VM-RUNTIME), so restarting every deno process never touches a VM.
files: ["internal/stack/denoruntime/"]
test_first: >
Behind livegate: TestADenoPodAppliedToTheKcpRunsOnTheEmbeddedDeno and
TestAnUpgradeRestartsEveryRecordedDenoPodOnTheNewClosureExactlyOnce (run the stack as a user unit
under a test prefix; three cases: `systemctl --user restart` of the unit, `kill -9` of its main
process, and `upgrade` to a build with a different closure sha256: every DenoPod Running on the new
code, one deno process each, no deleted DenoPod returns, no deno process of the old unit left); on proc
in a dev instance: TestAKilledCoreLeavesNoDenoProcessOnTheProcBackend (kill -9 of the core unit's main
process and `dev restart core`). Red first.
done_when: "both green on hat-8 on systemd-user and on proc."
risks: []
status: done
evidence:
- id: M2-DENOPOD-LISTENERS
milestone: M2
title: Every DenoPod listener binds port 0 and its bound tcp, unix and relay addresses land in DenoPod status; peers resolve from status
depends_on: [M2-DENOKCP-FORK]
decision: >
Fifth fold 2026-10-12 (multi-server-to-kcp.md 4.8, gaps G5 and G11; decided by Claude, user may veto,
within 0006 principle.port0_rule): createServe (typescript-helpers lib/serve/mod.ts:63-179, the one place
a "report my listeners" hook belongs) gains a listener report written on onListen and after each relay
connects: <runDir>/listeners.json {name: url} (tcp URL, unix path, relay ingressUrl), superseding the
single port-bound-to file M1-MARKET-NETWORK-PARALLEL reads. The owned runner reads it on observe into
DenoPod.status.listeners[] (the DenoPod status has Outputs and Ready but no address today,
deno-kcp/api/v1alpha1/types_denopod.go:48-68); servicenames.AdvertisedAddress prefers it (port-audit
B2), and the DNS table is re-read on a miss (KCP_DNS_TABLE_FILE) from the pod's OWN application's
status only. No service manifest names a port; the pod script sets the report path in the run dir and
never passes HOME. Readiness, a DID or a port is never read from stdout: the `bidder_ready` line
(hono-bidder/mod.ts:686-691) and accept.sh's DID grep (:463-467) are replaced by status (the bidder's
did is a root-status field, 0006 M2-APP-CONTROLLER).
files: ["ts/typescript-helpers/lib/serve/mod.ts", "internal/denokcp (runner, status.listeners)", "internal/kcplibs/factory/servicenames"]
test_first: >
typescript-helpers serve test: a port-0 tcp serve with one relay writes both URLs to the report; Go:
TestADenoPodOnPortZeroIsResolvedByItsReportedAddress, TestNoServiceManifestNamesAPort,
TestTheDnsTableHoldsOnlyTheOwnApplicationsServices. Red first.
done_when: "green; the market network's services run on port 0 and every probe and peer lookup uses status; pushed on main in typescript-helpers."
risks: []
status: done
evidence:
- id: M2-MARKET-NETWORK
milestone: M2
title: One MarketNetwork value and one fetch adapter replace the four fetch patches; cross-application values only through the composer
depends_on: []
decision: >
Fifth fold 2026-10-12 (multi-server-to-kcp.md 4.8, gap G6, finding F5; decided by the user 2026-10-12:
cross-app discovery only through values the composer copies onto a child's root object; decided by
Claude, user may veto: the packages). typescript-helpers lib/common/market-network-common (the value:
plcDirectoryUrl, ingressProxyHost, relayUrls, firehoseUrls, qrOrigin, guestTlsUrl, caBundleRef, the
*.localhost rewrite) and lib/market-network-fetch (the one fetch adapter) replace the four patches -
hono-bidder/mod.ts:74-95, request-vm-ssh/mod.ts:48-60, the market network's market-requester/mod.ts:82-95 and
atproto-market test/fetch-interceptor.ts. Under kcp the service template renders the value from the
application's root object (copied there by its composer, 0006 M2-APP-CONTROLLER; for a production app
by its profile), never from another application's objects; a value is never discovered by reading
another workspace or kcp. The Go MarketNetwork kind's value fields (0006 M2-APP-CONTROLLER) mirror the
type field for field (apigen TS target, drift-checked).
files: ["ts/typescript-helpers/lib/common/market-network-common/", "ts/typescript-helpers/lib/market-network-fetch/", "ts/atproto-market/hono-bidder/mod.ts, request-vm-ssh/mod.ts, test/fetch-interceptor.ts", "deploy/market-network/market-requester/mod.ts"]
test_first: "one table test: https://x.relay.localhost and https://plc.directory/<did> resolve identically through the adapter for every call site's inputs; a grep test that no fetch patch is left outside the adapter. Red until the adapter exists."
done_when: "no fetch patch left outside the adapter (grep test green); pushed on main in typescript-helpers and atproto-market."
risks: []
status: done
evidence:
- id: M2-SERVICE-TEMPLATE
milestone: M2
title: One service DenoPod template instead of a supervisor script per manifest
depends_on: [M1-TS-CLOSURE, M2-DENOKCP-INPROC, "0006:M2-TEMPLATE", M2-DENOPOD-LISTENERS, M2-MARKET-NETWORK]
decision: >
Fifth fold 2026-10-12 (multi-server-to-kcp.md 6; decided by Claude, user may veto): the template's
values come from application profiles (0006 M2-APP-CONTROLLER renders deploy/apps/<app>/<profile>.values.yaml
through it); it renders a component spec (0007 M3-BIDDER-COMPOSER), the MarketNetwork value
(M2-MARKET-NETWORK) and the listener report path (M2-DENOPOD-LISTENERS); no port, no HOME passthrough
(finding F6), and stdin is never read (interactive steps are MarketIdentity status, 0007
M1-MARKET-IDENTITY). A firecracker-provider market network bidder names --allow-run (docker, its runner
containers) with that reason until 0007 M5-RUN-BIDDER makes compute-provider-kcp its provider.
deploy/denopods/service.yaml renders the supervisor script the market network repeats (70-bidder.yaml:35-92)
from typed fields: entry and cwd relative to the TypeScript source root (M1-TS-SOURCE; never an
absolute host path), args, deno flags (always --no-code-cache), env, permissions, probe,
restartPolicy Always. Preloads: KCP_SHIM first, then the service's own (the door's undici-preload.ts).
The market network's 20-70 manifests become values for this template; vendored originals stay records.
Permissions (decided by the user 2026-10-11: explicit deno permissions, no -A, no --allow-run, no
/dev/kvm, no D-Bus socket path, enforced in the DenoPod template and tested): `permissions` is a
required typed list per service; the template refuses -A/--allow-all; --allow-run and --allow-ffi
render only when the values file names them with a reason, and the bidder's values (0007
M5-RUN-BIDDER) name neither; read/write are scoped to the service's state dir and the TypeScript tree;
every service gets --deny-read and --deny-write for /dev/kvm and $XDG_RUNTIME_DIR, because deno runs as
the same uid as the user manager and its permission sandbox is the only boundary to the bus there.
files: ["deploy/denopods/service.yaml", "deploy/sets.yaml", "internal/manifests"]
test_first: >
TestEveryServiceDenoPodNamesItsEntryRelativeToTheSourceRoot, TestTheTemplateRefusesAllowAll,
TestEveryRenderedServiceDeniesDevKvmAndTheRuntimeDir, TestAllowRunRendersOnlyWithAReason and, behind
livegate, TestTwoPreloadsBothRunBeforeTheEntry; red first.
done_when: "the market network's services start from the template on hat-8 with the probes of M1-MARKET-NETWORK run 9 passing."
risks: []
status: done
evidence:
- id: M3-DOOR-DENOPOD
milestone: M3
title: The door runs as a DenoPod
depends_on: [M2-SERVICE-TEMPLATE, "0006:M3-SERVE-GATEWAY-KCP"]
decision: >
`serve gateway kcp` applies a door DenoPod in root:socialweb-door (type denoruntime) with the env the
entrypoint sets today (container-entrypoint.sh:305-312), STATE_DIR on the persistent state dir, a
fixed PORT 8788 behind caddy (the gateway application's front, an edge exception to 0006
principle.port0_rule listed in deploy/edge-ports.yaml; every other gateway DenoPod binds 0), and the door's explicit permission list (M2-SERVICE-TEMPLATE; any
--allow-run it needs is named with its reason). A door restart (upgrade) must not cost a node; what the
door must re-populate after one is plan 0007 M6-GATEWAY-REPOPULATE.
files: ["internal/stack/gatewaykcp", "deploy/denopods/door.values.yaml", "deploy/sets.yaml"]
test_first: >
Behind livegate, on a FRESH state dir (no container-era state is ever opened; fresh deploy, decided by
the user 2026-10-12): TestTheDoorDenoPodServesApiConfigAndKeepsItsClaimsAcrossACoreRestart (claims the
door DenoPod wrote before `systemctl --user restart` of the gateway unit are listed by the restarted
door DenoPod, unchanged); red while the door is a child process.
done_when: "on hat-8 the door DenoPod serves /api/config on a fresh state dir and lists, after a core restart, the claims it wrote before it."
risks: ["The door's per-process secrets server for guests in flight (plan 0005 M3-NODE-LOSS) is made durable by plan 0007 M6-CLAIM-RESUME (split from M6-GATEWAY-REPOPULATE 2026-10-12); until then a door restart still fails Bidding/Provisioning claims."]
status: done
evidence:
- id: M3-DOOR-STATE-VAULT
milestone: M3
title: The door's OAuth sessions and requester key live in the gateway's root vault; each door account is a MarketIdentity
depends_on: [M3-DOOR-DENOPOD, "0006:M2-OPENBAO-SERVER", "0007:M1-MARKET-IDENTITY"]
decision: >
Fifth fold 2026-10-12 (multi-server-to-kcp.md 4.5, finding F8, gap G8; decided by the user 2026-10-12:
OAuth sessions and every key live in the application's own OpenBao, never in kcp Secrets): the door's
oauth-sessions.json (socialweb-computer-k8s hono-socialweb-computer-k8s/mod.ts:50) and
requester-key.hex (:72) move into the gateway's root vault behind the existing session-store and key
ports (sibling vault impl packages, ABC rules); each door account is a MarketIdentity in mode
oauthDelegated whose status the door UI renders (0007 M1-MARKET-IDENTITY); per-claim secrets keys stay
0007 M6-CLAIM-RESUME (also the root vault). The door state dir then holds no secret. The node fulfiller
(the NodeClaim reconciler, socialweb-computer-k8s-node-market) stays inside the door DenoPod for now;
splitting it into its own DenoPod is revisited after 0007 M6-CLAIM-RESUME, if a door restart still
interrupts fulfilment (decided by Claude, user may veto; research question 8).
files: ["socialweb-computer-k8s/lib (session store and key store over OpenBao, sibling packages)", "socialweb-computer-k8s/hono-socialweb-computer-k8s/mod.ts", "deploy/apps/gateway/*.values.yaml"]
test_first: "socialweb-computer-k8s test: a recreated door DenoPod with an EMPTY state dir serves an existing account session and the same requester DID; a scan finds no secret in the state dir or in any kcp object the door writes. Red first."
done_when: "green; the door state dir holds no secret; pushed on main."
risks: []
status: done
evidence:
- id: M4-RUN-GATEWAY-SSH
milestone: M4
title: socialweb-computer run gateway ssh
depends_on: [M2-SERVICE-TEMPLATE, "0006:M2-KCP-PORT0", M1-SSH-GATEWAY-IROH, M1-STRIP-LEGACY-TRANSPORTS, "0006:M2-APP-CONTROLLER"]
decision: >
kcp on port 0 (0006 slot shape behind a port-0 front, 0006:M2-KCP-PORT0) under <state-dir>/kcp, the owned deno-kcp provider, one DenoPod running
hono-socialweb-computer-ssh/mod.ts with its cli-args-env options passed through; its json-file-store
state under <state-dir>/ssh-door so a DenoPod recreate keeps it. Decided by Claude, user may veto.
Fifth fold 2026-10-12 (decided by the user: the ssh gateway's broader kcp-native move stays after the
bidder/requester/gateway-kcp work): it lands after M1-SSH-GATEWAY-IROH and M1-STRIP-LEGACY-TRANSPORTS
(so it reaches guests over iroh only) and follows the same application model as the others - its own
kcp, a root object rendered by 0006 M2-APP-CONTROLLER, its options as root-object fields, its keys and
sessions in its own OpenBao rather than the json-file-store (to be decided when it starts).
files: ["internal/stack/gatewayssh", "deploy/denopods/ssh-door.values.yaml"]
test_first: "TestRunGatewaySshServesTheSshDoorFromTheEmbeddedTree (live, hat-8); red first."
done_when: "the ssh door answers; its deno unit suite passes against the extracted tree."
risks: []
status: done
evidence:
- id: M4-RUN-REQUEST
milestone: M4
title: "socialweb-computer run request - a fresh kcp per invocation whose state dir persists; contracts as ComputeContract objects; `release|resume <name>` restart that same kcp"
depends_on: [M1-TS-CLOSURE, "0006:M2-KCP-PORT0", "0006:M2-APPLY", M2-SERVICE-TEMPLATE, M1-STRIP-LEGACY-TRANSPORTS, "0006:M2-APP-CONTROLLER", "0006:M2-BOOTSTRAP-LAYER", "0006:M2-OPENBAO-SERVER", "0007:M1-MARKET-IDENTITY"]
decision: >
Reworked in the fifth fold 2026-10-12 by the user's answers to multi-server-to-kcp.md section 7
(user-decisions.md 2026-10-12), which REJECT the research's persistent requester application (its
6.M4-RUN-REQUEST delta is not taken). Principle (user 2026-10-12, 0006 `principle`): a `run request` is
an application instance - its own kcp, the requester manifest set, a profile - and meets bidders only
over the atproto market protocol. Decided by the user 2026-10-12: the requester gets a FRESH kcp per
invocation (the process is ephemeral), and that invocation's state dir - its kcp root (one slot on
kine-sqlite, the requester row of 0006 M2-BOOTSTRAP-LAYER's table, placed by the CLI under
state/<invocation>) and its vault (an OpenBaoServer whose storage is state/<invocation>/bao) - PERSISTS
(durable state): `run request release <name>` and `run request resume <name>` restart that same kcp
from state/<invocation> to finish the contract. kcp only (decided by the user 2026-10-12): there is no
standalone request-vm-ssh; every requester run goes through socialweb-computer and kcp. Every
request-vm-ssh option keeps working as a MarketRequester field or a profile (0006 M2-APP-CONTROLLER's
option map; --hold, --keep-vm, --skip-ssh, --exec, --secrets, --policy, --policy-args, --user-data,
firehose and sign-in options included); --user-data-transport is removed with the reason
legacy-transport (M1-STRIP-LEGACY-TRANSPORTS). Decided by the user 2026-10-07: run request keeps its
state in kcp objects. Kind ComputeContract (socialweb.computer/v1alpha1, export computecontracts,
apigen) with spec {vm request, bidder allow/deny lists, policy refs, keepUntil} and status.contract =
the same ContractState fields NodeClaim.status.contract carries (plan 0005 M2-CODEGEN), so the requester
core writes one shape for both. Decided by Claude, user may veto: the invocation applies a
MarketRequester root object (0006 M2-APP-CONTROLLER) whose children are a MarketIdentity (0007
M1-MARKET-IDENTITY: oauthQr - no repo component, finding F7 -, oauthBrowser, pdsAccount, or localRepo =
the ephemeral-PDS mode the user keeps, with its association approved through status), the requester
DenoPod - a thin hono-compute-request entry in atproto-market (CLAUDE.md CLI layer; request-vm-ssh's
mod.ts is reduced to it and exits 2 naming `socialweb-computer run request` outside a DenoPod) over
compute-request-xrpc requestCompute, awaitComputeNetwork and releaseCompute, every handler awaiting a
status write (the M2-CLAIM-RECEIPT pattern) - and the ComputeContract. Keys and sessions (requester
key, OAuth session and DPoP key, per-contract secrets-server keys) live only in the invocation's vault
(the layout of 0007 M4-BIDDER-VAULT; whichever of the two lands first builds the OpenBao key and
session store packages and the other reuses them), never in a kcp object, so a restarted kcp re-serves
a contract's secrets URL. `--ssh` keeps the session in the foreground CLI process: it waits for status.contract
onNetwork and hostKeys (0007 M4-HOST-KEYS) and runs ssh over the iroh ProxyCommand with
StrictHostKeyChecking=yes. Subcommands: run request [--wait onNetwork] [--ssh], run request list (reads
each state/<invocation>/summary.json written from root status at exit, starting no kcp), run request
release <name>, run request resume <name> (both restart the invocation's kcp and the requester DenoPod
resumes from the ComputeContract, including one accepted before the stop). In a market network, `run
request --market-network <instance>` asks the market network's MarketNetwork to add this invocation as a child application
(0006 M2-APP-CONTROLLER), so the network values are copied onto its root object and its state dir sits
under the market network's state; the CLI then talks to the invocation's kcp exactly as outside a market network. The
org-root alice-request.sh path and the identity-prompt-tty impl are deleted with this item. Everything
goes through the RFP flow.
files: ["api/v1alpha1/computecontract_types.go", "atproto-market/hono-compute-request/", "ts/atproto-market/request-vm-ssh/ (reduced to the DenoPod entry)", "ts/atproto-market/lib (a ComputeContract store over kcp REST, beside the door's nodeclaim-kcp; requester key and session stores over OpenBao)", "internal/stack/request (per-invocation bootstrap, state/<invocation>, summary.json)", "internal/bootstrap (requester role row)", "deploy/apps/requester/*.values.yaml", "deploy/ts-entries.yaml"]
test_first: >
atproto-market test/compute_request_cli_test.ts: against fakes, the entry writes status.contract at
onRfpSubmitted, onAccepted and onReceipt, and `release` from a fresh process submits exactly one
vm.delete from the object; 'request-vm-ssh outside a DenoPod exits 2 naming socialweb-computer run
request'. Go: TestEachRunRequestInvocationGetsItsOwnKcp (two invocations: distinct kcp roots and ports),
TestReleaseRestartsTheInvocationsKcpFromItsStateDir (the kcp stopped after onNetwork; release starts it
again from state/<invocation>, the ComputeContract and its vault keys read back, one vm.delete),
TestRunRequestListStartsNoKcp, TestNoRequesterKeyOrSessionIsInAKcpObject. Red first.
done_when: "tests green; on the market network `run request --market-network` reaches onNetwork and an ssh session over iroh, `run request list` shows it with no kcp running, `release <name>` restarts the invocation's kcp, gets vm.delete 200 and the guest is gone (UTC readings, cold kcp seconds per invocation recorded)."
risks: ["A fresh kcp per invocation costs 6-9 s cold start and ~0.4-0.5 GB while it runs (kcp-spike.md); the user chose it over a persistent requester app (2026-10-12); the reading is recorded so the cost is visible."]
status: done
evidence:
- id: M4-MARKET-NETWORK-PORT
milestone: M4
title: The market network driven by socialweb-computer instead of accept.sh and apply.sh
depends_on: [M2-SERVICE-TEMPLATE, M1-MARKET-NETWORK-PARALLEL, "0006:M2-APPLY", "0006:M2-KCP-PORT0", "0006:M1-DEV-INSTANCE", "0006:M2-BOOTSTRAP-LAYER", "0006:M2-HOSTPROCESS-KIND", "0006:M2-APP-CONTROLLER", M2-DENOPOD-LISTENERS, M2-MARKET-NETWORK, M4-RUN-REQUEST, "0007:M1-MARKET-IDENTITY", "0007:M3-BIDDER-ACCOUNT-PDS", "0007:M3-BIDDER-COMPOSER"]
decision: >
Fifth fold 2026-10-12 (decided by the user: STRICT kcp per application, inside a market network too; cross-app
discovery only through composer-copied values; the shim resolves only within its own application;
overrides the research's nested-workspace market network): `market-network up` = `dev up --profile market-network`, whose dev instance IS a MarketNetwork
application (0006 M2-APP-CONTROLLER, M1-DEV-INSTANCE): its own kcp runs the shared atproto services
(plc, relay, dispatcher, dispatcher-tls, qr, the verifier) as DenoPods, and it composes N MarketBidder
children (each with its own kcp and its own PDS DenoPod, MarketIdentity mode pdsAccount, 0007
M3-BIDDER-ACCOUNT-PDS; firecracker provider until 0007 M5-RUN-BIDDER) and requester invocations (`run
request --market-network`, M4-RUN-REQUEST), plus a Gateway child for m3-shaped runs. Each child learns the network
only from the values copied onto its root object; no bidder or requester reads the network's kcp, and
the shim never resolves another application's name (today's cross-workspace lookup, README.md:128-130,
is deleted with the vendored market network). System tests start applications only through one helper,
internal/apptest.Start(t, app, profile, values) (a fresh child of a market network or a fresh dev instance),
and read DIDs and URLs from root status, never stdout (accept.sh's DID grep and the bidder_ready line
go; M2-DENOPOD-LISTENERS). N bidders = N bidder applications, N market networks = N dev instances. Sign-in in the
market network is driven through MarketIdentity status and hono-pds's session minting (0007 M1-MARKET-IDENTITY),
never --skip-qr.
Principle (user 2026-10-12): the market network is the seed of the application-instance model - a composition of
application instances (its atproto network, bidders each with their own PDS pod under the market-network profile,
requesters) brought up by applying manifest sets, each instance talking to the others only over the
market protocol; how its services split into instances is the fifth-fold paragraph above. Third fold 2026-10-12 (decided by the user: dev run mode kcp-native now; drop the fixed ports 2583-2589
and the one-network-per-host lock): `market-network up` = `dev up --profile market-network` (0006 M1-DEV-INSTANCE, M2-BOOTSTRAP-LAYER:
the instance's own kcp on kine -> postgres behind a port-0 front, host-backend=proc), then the market network's
sets; every market network service port renders to 0 and is read back from the service's listener report (the
leases of M1-MARKET-NETWORK-PARALLEL are kept only for ports that must be stable within a run); there is no lock
file and no one-network-per-host rule; N market networks run on one host, one per dev instance.
`socialweb-computer market-network up|accept|down`: kcp on port 0 (0006:M2-KCP-PORT0), an OpenBaoServer object for the market network's dev vault, the denoruntime and market-network sets, the
same checks as accept.sh run 9. The market network's bidders are MarketBidder child applications (on the
firecracker provider until 0007 M5-RUN-BIDDER, then `run bidder`'s compute-provider-kcp) and their
guests come up only through a requester invocation's RFP, bid, accept and cloud-init. Decided by Claude, user may veto
(2026-10-11): every unit a market network starts is named socialweb-market-network-<runid>-... so no production prefix
(socialweb-vm-<hid>-, socialweb-bao-, socialweb-pg-, socialweb-cluster-) ever matches it, and a market network on
hat-8 never adopts, audits or garbage-collects a real bidder's units on the same account (or the
reverse); under proc the market network's units are its own devsup's, which already separates them. accept.sh and
apply.sh are deleted after two passes.
files: ["internal/stack/market network", "internal/marketnetwork/live_test.go", "deploy/market-network/accept.sh, apply.sh (deleted at the end)"]
test_first: "TestTheMarketNetworkRentsAFirecrackerGuestThroughTheRFPFlow switched to the subcommand; TestTwoNetworksOnOneHostEachRentAGuestThroughTheRFPFlow through `market-network up` in two dev instances (two requesters, two bidders; guests only via runComputeContract); fifth fold: TestNoTwoApplicationsInANetworkShareAKcp (network, every bidder, the gateway and each requester invocation: distinct kcp roots and fronts), TestANetworkSystemTestReadsTheBidderDidFromRootStatusNotStdout, TestTheNetworkBidderSignsInThroughItsIdentityObjectWithoutSkipQr; red until it exists."
done_when: "two PASS readings through the subcommand with accept.sh run 9's check list, and one reading of two market networks on hat-8 (UTC); 0006 M1-NO-FIXED-PORTS' ratchet holds no market network entry."
risks: []
status: done
evidence:
acceptance:
where: hat-8
checks:
- id: ACC8-OFFLINE
text: Every production service DenoPod starts with jsr.io and registry.npmjs.org unreachable, from the embedded closure alone.
status: done
evidence: []
- id: ACC8-NO-STALE-CODE
text: After an upgrade to a binary with a different closure, every DenoPod runs the new code (checked by a version marker each service logs), with the previous DENO_DIR still on disk.
status: done
evidence: []
- id: ACC8-RESTART
text: "A `systemctl --user restart` of the unit running socialweb-computer, a kill -s KILL of its main process, and an upgrade each bring every recorded DenoPod back to Running exactly once, leave no deno process of the previous run, and no deleted DenoPod returns."
status: done
evidence: []
- id: ACC8-PERMISSIONS
text: "Every production service DenoPod runs with an explicit permission list and no -A; the bidder's deno cannot open /dev/kvm or connect to the user bus or the systemd private socket (decided by the user 2026-10-11)."
status: done
evidence: []
questions_for_user: []