~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
648 lines · 62.2 KB Raw History
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: []