# Market network A market network is one `MarketNetwork` application: the shared atproto services of a local market, in one kcp, plus child applications (bidders, requesters, gateways) that find each other over the atproto market protocol. Bring one up with `socialweb-computer dev up --profile market-network`. The operator or developer who runs a market network and the applications it hosts reads this file. ## What a market network is A market network is one application, `MarketNetwork`, in one kcp. `socialweb-computer dev up --profile market-network` brings up the instance (kine, one kcp slot behind its front, the socialweb-computer core process) and applies one root object, `MarketNetwork network`, into the workspace `root` of the instance's kcp. The core process (the `socialweb-computer` process that runs the controllers, the component runtime and the HostProcesses) runs the composition controller of `internal/app/marketnetwork`. It composes the root into: - an OpenBaoServer, `OpenBaoServer network`, that holds the network PKI; - the network's own components, in `root`: `Plc plc`, `Relay relay`, `IngressProxy dispatcher`, `DispatcherTLS dispatcher-tls`, `Pds alice`, `Pds bob` and `PolicyEngine policy-engine`, with the Service and Exposure objects that publish them; - one child application for each `spec.apps[]` entry: `bidder-1` .. `bidder-N` (`MarketBidder`) and, with `--with-gateway`, `gateway` (`Gateway`). ``` one market network (one dev instance, one kcp) | +- root MarketNetwork network | OpenBaoServer network | Plc plc, Relay relay, IngressProxy dispatcher, | DispatcherTLS dispatcher-tls, Pds alice, Pds bob, | PolicyEngine policy-engine, their Services and Exposures | +- root:bidder-1 .. root:bidder-N MarketBidder app (components in root::app) +- root:gateway Gateway (dev up --with-gateway) +- root: MarketRequester app (run request --network) +- root: SshGateway app (run gateway ssh --network) | +- root:dev:- the HostProcess of each child's core ``` A child application runs in its own workspace `root:` of the network's kcp, with its components in `root::app`. It has no kcp or kine of its own. Its core process is a HostProcess in `root:dev:-`, placed by the network's devsup. That core runs the child's controllers and components for its base workspace `root:`. Its kubeconfig for `root:dev:-` (`parent.kubeconfig` in the child's state dir) is a token of the ServiceAccount `app-core`, which may manage only the HostProcess objects of that workspace. Once the child's core reports that it adopted its units, the network applies the child's root object (`MarketBidder app`, `MarketRequester app` or `SshGateway app`) into `root:`, with the network value (`status.network`) copied into `spec.network` and the bidders' DIDs into `spec.bidderDids`. The network then reads back only the child's root status (phase, message, did, components, listeners). A `Gateway` child applies its own root object from the spec file the network writes for it (see [Add a cluster gateway](#add-a-cluster-gateway)). The copied value is how a child learns about the network. The network also lets the `pds` account of each child read the MarketIdentity objects of the other children (the ClusterRole and ClusterRoleBinding `market-network-peers`). Applications still talk to each other only over the atproto market protocol; the shared kcp is composition and isolation, never a data channel. The names `global`, `relay`, `alice`, `bob`, `dev` and `app` are reserved: no child may take them as `apps[].name`. `OpenBaoServer network` (`deploy/market-network/05-vault.yaml`) holds the network PKI. It declares a `pki` mount, the `network-pki` policy, the client `components` (holding only that policy) and the client `owner` (kv mount `owner`). Its storage is under the instance's state (`openbaoservers/network`). The OpenBaoServer controller initialises it, writes the unseal keys to a mode 0600 file of its state and never to kcp, and revokes the initial root token, so no root token is kept. The composer waits for the server to be Ready with the client `components`, then reads the root CA of the `pki` mount through the bus (a `vault.pki.issue` ComponentLink scoped to `pki`). Every bidder has its own OpenBaoServer `bidder`, in its own workspace (`deploy/apps/market-bidder/vault.yaml`), applied by the bidder's own core. That core generates the account password into the vault, signs up for an account of its own on bob's PDS (`MarketIdentity` mode `pdsAccount`, the PDS URL from `spec.identity.pdsUrl` of its root object) and reaches its vault only through the bus. No password or key is in a kcp object, and the network never touches a child's vault. Each network component listens on a port the composer leases from the network's own port registry (`/network/ports.json`, ports 30000 to 39999, each probed free before it is leased), so the number survives a restart of the core. Each local Exposure listens on a port of the core's port registry (`/ports/ports.json`) and records it in `status.address`. Peers find the addresses by name in `status.network.hosts`. ### TLS and the certificate authority The network's CA bundle has two parts. One is the root CA of the `pki` mount of `OpenBaoServer network`. The other is the guest TLS CA that the composer makes in `/network/guest-tls/` (`ca.crt`, `ca.key`, and the leaf `relay.localhost.crt` with its key, for `relay.localhost` and `*.relay.localhost`, valid 30 days and made again within a day of expiry). `kcp start` generates its own CA in memory and persists only the serving key, so kcp cannot sign these leaves. The composer publishes the bundle inline, as PEM, in `status.network.caBundle`. Every child gets it in its copied network value. Where a process needs a file, the child writes the bundle to a file in its own state dir. `DispatcherTLS dispatcher-tls` is the guest TLS front: it serves the `relay.localhost` leaf and forwards to `IngressProxy dispatcher`. The ingress Exposures of the PDSes, plc and relay have TLS enabled with the network CA bundle. Their local Exposures serve plain HTTP on loopback. ### How names resolve There is no DNS for the network's names. Each name is an entry of `status.network.hosts`, mapped to a loopback address: - inside a core, a request to a name that a running component serves goes to that component's handler on the bus (`internal/bus`), without a socket; - every other dial goes through `internal/kit/networkdial`. It builds a host map from `status.network.hosts` (plus the guest TLS name). A name, or any subdomain of it, maps to its address. An IP literal passes through. Any other name is refused, because system DNS is not allowed by default. The dialer changes only the address it connects to. The request keeps the name in its URL and its `Host` header, so a service that derives its identity from `Host` (the PDS for its `did:web` and OAuth issuer) still sees the name it was configured with. ## Bring up a market network ### Prerequisites - The `socialweb-computer` binary. A dev build from `scripts/build-socialweb-computer.sh --dev --publish ` embeds every pinned asset. `--with-gateway` needs the gateway's embedded k3s and external-secrets assets; a binary not built by that script refuses it. - dumbpipe, for any guest access: `run request` and `run gateway ssh` reach guests only over iroh. A dev build needs `--dumbpipe-path` (or `SOCIALWEB_DUMBPIPE_PATH`) on `dev up` and `run request`; `run gateway ssh --network` uses the one the network was brought up with. A release build runs its embedded dumbpipe. - The VM runtime, when the network runs bidders: `--guest-image-source` and `--vm-host-backend systemd-user`, plus `--userns-stage-path`, `--pasta-path`, `--vmexec-path` and `--firecracker-path`, or the matching `SOCIALWEB_*_PATH` environment variables. - Run as the account that owns the subordinate id range. `dev up` refuses to run as root. ### Start the network Run one command. `--bidders` sets the number of child bidders (default 1, 0 for none). `--guest-image-source` and `--vm-host-backend` fill the bidders' compute provider. ```bash socialweb-computer dev up --instance mn1 --profile market-network --bidders 2 \ --guest-image-source file:///var/tmp/guest-image --vm-host-backend systemd-user ``` `dev up` brings up the instance's bootstrap layer (kine, kcp behind its front, the core process) under its own devsup and applies the `MarketNetwork` root object. It waits up to `--network-timeout` (default 3m) for the network and every child to be Ready, and prints the result. `--app-values` names a YAML (or JSON) file merged over the built-in defaults. Its keys are `bidder` and `gateway`: the NetworkApp fields every bidder or the gateway gets. For example, the owner handle of the bidders' ComputeVMs: ```json {"bidder": {"kcp": {"handle": "{app}.mn1.bob.test"}}} ``` `{app}` becomes the bidder's name; make the handle unique per worktree on a shared account. N market networks on one host are N dev instances. Every port is probed free before it is leased, so two `dev up --instance` with different names never collide. ### Check the network is Ready ```bash socialweb-computer dev status --instance mn1 ``` `dev status` prints the instance record (`.record`, with the state dir in `.record.layout.stateDir`), devsup's units (`.devsup.units`) and the whole `MarketNetwork` object (`.network`). The network is Ready when `.network.status.phase` is `Ready`; see [Conditions](#conditions). ### Rent a guest `run request --network` adds a `MarketRequester` entry to `spec.apps[]`. The network places it in its workspace `root:` of the network's kcp and applies its root object, and the requester runs the RFP flow against the network's bidders. ```bash socialweb-computer run request --network mn1 --secrets secrets.json --exec 'cat /root/x' ``` The network copies the network value (with `hosts`) and the bidders' DIDs onto the requester's root object. The CLI then talks only to the invocation's own workspace. The guest comes up only through the RFP flow: the `Requester` component posts the RFP, a bidder bids, the requester accepts, and the winner boots the guest from cloud-init `user_data`. `--secrets` is written only into the invocation's own OpenBao, and the guest fetches it from the requester's secrets server. The CLI reaches the guest only over its iroh ticket, with `StrictHostKeyChecking=yes` against the host keys the guest reported. Without `--hold` the CLI releases the contract (vm.delete) and removes the entry from `spec.apps[]`. To hold the invocation and manage it later: ```bash socialweb-computer run request --network mn1 --secrets secrets.json --hold --exec 'cat /root/x' socialweb-computer run request list socialweb-computer run request release socialweb-computer run request resume ``` At the end of a held invocation the entry is marked `stopped`: its core and its components stop, and its workspace and state dir are kept until `release` or `resume` starts them again. ### Deploy with Infra and EphemeralInfra A deployment is a kcp object in a requester's workspace that owns its `ComputeContract` objects (plan `docs/plans/0029-infra-and-ephemeral-infra.yaml`). An `Infra` lives until it is deleted; an `EphemeralInfra` ends at `creationTimestamp + spec.ttlSeconds` or, with `endWhen: Complete` (the default), once every requested contract is Released or Failed; then it releases its contracts and deletes itself. Each contract has a desired `state` that you write and a `phase` that the controller reports: | state | what happens | phase | | --- | --- | --- | | `Staged` (default) | nothing: no ComputeContract, no RFP | `Staged` | | `Requested` | the controller applies the ComputeContract and the Requester runs the RFP flow | `Requesting`, then `Deployed` (Held) or `Failed` | | `Released` | the controller writes `spec.release` on the ComputeContract | `Releasing`, then `Released` | Deleting a deployment releases every contract first (finalizer `infra.socialweb.computer/release`), then deletes the ComputeContracts and the record, then the object goes. A requester app for deployments that stays up (no held guest, no CLI) is one `spec.apps[]` entry of the network: ```bash kubectl --server "$KCP/clusters/root" apply --server-side --field-manager socialweb.computer/network-app/infra-r -f - <<'EOF' apiVersion: socialweb.computer/v1alpha1 kind: MarketNetwork metadata: {name: network} spec: apps: - {name: infra-r, kind: MarketRequester, profile: market-network} EOF ``` Its workspace is `root:infra-r`. The root links its own account's repo (`repo-did-plc-...`, the `AtprotoRepo` named in `status.did` of the MarketRequester) to the deployment kinds: repo.read for the four kinds and repo.write for their AtprotoRecords, both scoped to `computer.socialweb.infra` and `computer.socialweb.ephemeralInfra`. A staged Infra, bound to the requester's own repo: ```bash kubectl --server "$KCP/clusters/root:infra-r" apply -f - <<'EOF' apiVersion: socialweb.computer/v1alpha1 kind: Infra metadata: {name: web} spec: description: one guest contracts: - {name: vm1, state: Staged} record: repoRef: {kind: AtprotoRepo, name: repo-did-plc-REPLACE} eventSourceRefs: [{kind: EventSource, name: relay-network}] EOF kubectl --server "$KCP/clusters/root:infra-r" get infra web -o yaml ``` The record is `at:///computer.socialweb.infra/web` (`status.record.uri`). Request the guest: ```bash kubectl --server "$KCP/clusters/root:infra-r" patch infra web --type merge \ -p '{"spec":{"contracts":[{"name":"vm1","state":"Requested"}]}}' kubectl --server "$KCP/clusters/root:infra-r" get computecontracts -l infra.socialweb.computer/name=web ``` `spec.contracts` is replaced whole by a patch or a pull, so a patch names every contract. `spec.secrets` (`[{contract: vm1, secretsRef: {...}}]`) becomes the ComputeContract's `secretsRef` and is never written to the record. Two-way sync. Any client of the account may edit the record (for example `com.atproto.repo.putRecord` to its PDS). The `AtprotoRecordWatch` the Infra owns sees the commit on the event source, the controller reads the record, and when only the record changed since the last sync it applies the record's `description` and `contracts` to the spec (field manager `socialweb.computer/infra-record`; `status.record.direction: Pulled`). When only the spec changed it writes the record (`Pushed`), compare-and-swap on the cid it read. When both changed the record wins and `SyncConflict` is True with reason `RecordWon`. A record deleted elsewhere deletes the object. A following object names an existing record with `spec.record.uri` instead of publishing its own. An account's whole collection, enumerated into child objects (one per record, named by the rkey): ```bash kubectl --server "$KCP/clusters/root:infra-r" apply -f - <<'EOF' apiVersion: socialweb.computer/v1alpha1 kind: InfraCollection metadata: {name: mine} spec: repoRef: {kind: AtprotoRepo, name: repo-did-plc-REPLACE} eventSourceRefs: [{kind: EventSource, name: relay-network}] EOF kubectl --server "$KCP/clusters/root:infra-r" get infracollection mine -o jsonpath='{.status.children}' ``` A record that another object already binds is listed with `boundBy` and gets no child. Deleting the collection deletes its children, so it releases their contracts and deletes their records; `kubectl delete --cascade=orphan` keeps both. `EphemeralInfraCollection` is the same for `computer.socialweb.ephemeralInfra`. An EphemeralInfra that rents one guest for ten minutes: ```bash kubectl --server "$KCP/clusters/root:infra-r" apply -f - <<'EOF' apiVersion: socialweb.computer/v1alpha1 kind: EphemeralInfra metadata: {name: burst} spec: ttlSeconds: 600 contracts: - {name: vm1, state: Requested} record: repoRef: {kind: AtprotoRepo, name: repo-did-plc-REPLACE} eventSourceRefs: [{kind: EventSource, name: relay-network}] EOF ``` For an account other than the root's, write the links the root writes for its own: an `AtprotoRepo` for the account (a `LocalRepo` on a Pds component, or a `RemoteRepo` with its credentials), then ComponentLinks in the workspace from the four kinds (selector `kind: Infra`, and so on) to that AtprotoRepo with `repo.read` and from `AtprotoRecord` objects with the label `infra.socialweb.computer/kind` with `repo.write`, both scoped to the two NSIDs. ### Add a bidder `run bidder --network` adds a `MarketBidder` entry to `spec.apps[]`. Its name is the handle's first label, or `--name`. The command waits until the network reports the bidder Ready with its did, prints a summary and returns. `--fg` stays and removes the bidder from the network on Ctrl-C. `--handle` names the owner of its ComputeVMs; its atproto handle in the network is `.bob.test`. ```bash socialweb-computer run bidder --profile market-network --host-backend proc --network mn1 \ --handle mybidder-1.bob.test --guest-image-source file:///var/tmp/guest-image --vm-host-backend systemd-user ``` With `--network`, the market network places the bidder under its own devsup, so `--host-backend` must be `proc` and `--profile` must be `market-network`; `run bidder` refuses any other pair. ### Add an SSH gateway `run gateway ssh --network` adds an `SshGateway` entry to `spec.apps[]`. The network places it in its workspace `root:` and copies the network value and the bidders' DIDs onto its root object. The gateway holds an OpenBaoServer `ssh-gateway` (OAuth sessions and web sessions), the iroh dialer HostProcess (`socialweb-computer internal iroh-dial`, `internal/irohdial`, on a unix socket in the SshServer's state dir; the only process that runs `dumbpipe connect`) and the in-process `Requester` and `SshServer ssh-server` components. The gateway declares two local Exposures for the `SshServer`: one serves `ssh` and one serves its HTTP keys API. `run gateway ssh` prints the local ssh Exposure address in `.ssh` and every local endpoint in `.listeners`. ```bash socialweb-computer run gateway ssh --host-backend proc --network mn1 --name sshgw \ -- --test-owner-sessions-file /abs/owner-sessions.json ssh -p -o SendEnv=LC_POLICY @127.0.0.1 'hostname' ``` The market network must be brought up with dumbpipe (`--dumbpipe-path` or `SOCIALWEB_DUMBPIPE_PATH` in a dev build); `run gateway ssh` refuses a network without it. The owner sessions file is a market-network seam: a JSON object keyed by account DID of `{service, identifier, password}`, used in place of a deposited OAuth session. The requester signs in to that PDS with the session, writes its RFP records there, and mints service-auth tokens through it; it publishes the badgeBlueKeys record that binds its own signing key. The same credentials authenticate the keys API: `POST /keys` with HTTP basic auth (`identifier` and `password`) adds the key the account may use at the ssh server. The account must have a `badgeBlueKeys` `requester_associate` record for the ssh key. Every ssh connection rents its guest through the RFP flow and releases it (vm.delete) when the command exits. A connection that sends `LC_SESSION=` holds the guest under that name, reuses it for later connections of the same account, and releases it after five idle minutes (`sessionIdleSeconds`, default 300) or when the requester stops. That is what lets a connection resume after `socialweb-computer dev restart mn1 sshgw/ssh-server` without a second RFP. A connection that does not send `LC_SESSION` keeps the one-shot rent-and-release behavior. ### Add a cluster gateway A gateway that runs tenant clusters joins a market network as its own application: ```bash socialweb-computer serve gateway kcp --host-backend proc --state-dir /abs/gateway-state \ --gateway-profile market-network --gateway-spec-file ``` The spec is a YAML GatewaySpec. Its `network` is the network value (`dev status --instance mn1 | jq .network.status.network`, which carries the CA bundle inline) and `marketNetwork.ownerSessionsFile` names the cluster owners' accounts on the network's PDSes. The gateway's web app hands every node guest the network's CA bundle. Its core runs a loopback egress proxy and sets `HTTPS_PROXY` to it, so every vault it places reaches a bidder's port-less issuer URL through the guest TLS front. Unless `SSL_CERT_FILE` is set, the core writes the system roots plus the network's CA bundle to `/tls/outbound-ca-bundle.pem` and sets `SSL_CERT_FILE` to it, so it and every vault it places verify the bidder's issuer. `dev up --with-gateway` places the same gateway as a child of the network, in its workspace `root:gateway` of the network's kcp. Its core runs what `serve gateway kcp --gateway-profile market-network` runs (the sets, the Gateway root object, the root vault, the web app, the clusterprovider, the egress proxy). The network writes its spec (`apps/dev/-gateway/gateway-spec.json` under the network's state dir, holding the network value and `marketNetwork.ownerSessionsFile`), creates the owner account `-gateway-owner.alice.test` on alice's PDS (`owner-sessions.json` next to it, mode 0600), and the gateway brings up the tenant cluster `tenant` of that owner at zero nodes. A pod applied to that cluster gets a node through the RFP flow from the network's bidders. The tenant cluster's nodes are one Infra in `root:gateway`, named `nodes-<20 hex>` with the label `infra.socialweb.computer/cluster=tenant` and `spec.clusterRef` naming the Cluster (plan 0031). Each NodeClaim in the cluster's home workspace is one entry `node-<12 hex>` of it: `spec.contracts` holds its state (Staged until the join bundle is written, Requested, Released when the claim is deleted), `spec.nodes` its ownerDid, nodeJoin and nodeClaimRef, and `spec.secrets` the bundle; the claim's `status.infra` names the entry and its ComputeContract `infra-nodes--node-`. Read it with `kubectl get infras -l infra.socialweb.computer/cluster=tenant -o yaml` in `root:gateway`. Deleting the Cluster deletes its Infra, which releases every node. Every cluster's nodes are published to the owner's own repo; there is no opt-in. The NodeClaim controller gives the Infra `spec.record` (`repoRef: AtprotoRepo/repo-` in `root:gateway:app`, `sync: Publish`), and the record `at:///computer.socialweb.infra/nodes-` carries each node entry's name, state, vmName and bidder DIDs and the observed phases, never `spec.nodes`, `spec.secrets`, user data or the node policy. The gateway composes that repo for the owner of `owner-sessions.json`: a `pdsAccount` MarketIdentity `owner-<16 hex>`, a RemoteRepo and the AtprotoRepo, in `root:gateway:app`. For an owner of a cluster who is not in that file, it composes the MarketIdentity `owner-<16 hex>` in mode `oauthSessionProxy` instead (label `apps.socialweb.computer/session-owner`): it reads the owner's web app session from the root vault's atproto auth mount, and without one it reads `SessionExpired` with reason `NoSession`, so no repo is composed and the Infra reports Synced False RecordUnavailable. A session that lacks part of the scope the web app now requests reads `SessionExpired` with reason `ScopeChanged`; the Gateway stays Ready and its condition `SessionOwnersReady` names the owner. The record is push-only: an edit or a delete made on the PDS is written over. The record goes with the Infra when the cluster's last node goes or the Cluster is deleted. The gateway's requester trusts a bidder through its operator's vouches. The network declares, in the gateway's `root:gateway:app`, one AtprotoRecord `operator-vouch-` (`spec.repoRef: AtprotoRepo/operator`, label `apps.socialweb.computer/record-repo: operator`, collection `sh.tangled.graph.vouch`, rkey and subject the bidder operator's DID from its MarketIdentity `approvedAssociations`). The gateway composes the AtprotoRepo `operator` beside it: a RemoteRepo on the owner account's RemotePDS whose credentials are a `pdsAccount` MarketIdentity `operator` (password and session in the root vault; for an https PDS its `caRef` and the RemotePDS's name the root, which publishes the network CA, and both verify against the system roots plus that CA), and the ComponentLinks from the labelled records to the repo, the repo to the RemoteRepo, the RemoteRepo to the RemotePDS and the MarketIdentity, and the MarketIdentity to the root (vault.read on the CA). The AtprotoRecord controller writes the record and its status (Applied with the uri). The other direction is declared too: in each bidder's workspace `root:` one AtprotoRecord `operator-vouch-` (the bidder operator's vouch for the gateway operator), written the same way through the bidder's AtprotoRepo `operator`, so a bidder under `bidder-tangled-vouch` (the default of every network bidder) bids on the gateway's RFPs. Every MarketRequester app (one added to `spec.apps`, and each `run request --network` invocation) gets the same two directions once it and the bidders have DIDs: in each bidder's workspace one AtprotoRecord `operator-vouch-requester-` (the bidder operator's vouch for the requester DID, through the bidder's AtprotoRepo `operator`; the MarketBidder reconciles on every AtprotoRecord labelled `apps.socialweb.computer/record-repo: operator`), and in the requester's workspace one AtprotoRecord `network-vouch-` per bidder (the requester's vouch for the bidder DID, `deletionPolicy: Retain`, written to its own AtprotoRepo through the ComponentLink `link-network-vouches-` from AtprotoRecords labelled `apps.socialweb.computer/network-vouch`). `run request --network` waits until both are Applied before it posts its contract, so its RFP meets `bidder-tangled-vouch` and the attached `requester-tangled-vouch` with the vouches in place. No network app runs `open` unless its spec names it. ### Point an installed bidder account at the network An installed bidder account (`install bidder`, host-backend systemd-user) is not a child of the network: it is its own application and reaches the network only over the atproto market protocol. Its `config.json` names `"profile": "market-network"` and `"market-network-file"`, a file holding the network's status (`dev status --instance mn1 | jq .network.status`): the network value, with its CA bundle inline, and `bidderPdsUrl`, the PDS where the bidder makes its account. `run bidder` refuses `--market-network-file` together with `--network`. Its VM units are units of that account's own user manager, so a restart of the whole manager (`systemctl restart user@`) touches only that account. ## Tear down a market network ```bash socialweb-computer dev down --instance mn1 ``` `dev down` deletes the `MarketNetwork` and waits for it to go. Its finalizer tears every child down: it deletes the child's root object (the root releases its ComputeVMs first), then the child's workspace `root:`, then `root:dev:-`, which stops the child's core, and then the child's state. It then stops the network's components and deletes `OpenBaoServer network`. `dev down` then stops any VM unit a bidder child left (`vmsStopped`, `vmsLeft`), stops devsup, and kills any process left under the instance (`leftoverKilled`, `remaining`). `--keep-state` keeps the instance state. The network keeps its state across a restart of its core: the OpenBao storage, the port leases, the PLC operation log (`plc.json`) and each PDS's repos and accounts (`repos.db`, `accounts.db`) in their component state dirs under `/network/components/`. For a Go change, rebuild and run `socialweb-computer dev restart mn1 core --binary /bin/socialweb-computer`; do not take the network down and up again. ## The MarketNetwork object `socialweb-computer dev up --profile market-network` applies one `MarketNetwork network` into the workspace `root` of the instance's kcp. The types are in `api/v1alpha1/types_application.go`. ### MarketNetwork spec | Field | Meaning | | --- | --- | | `profile` | the built profile, `market-network`; any other value fails the network | | `suspend` | stop participation; cascades into each child root's `spec.suspend` | | `guestLabel` | the label the guests carry, defaulting to the instance id | | `comms` | the market comms mode, passed to every child | | `ingress` | the PDS ingress selection (see below) | | `components` | overrides of the network's own components | | `apps` | the child applications, as `NetworkApp` entries | ### NetworkApp | Field | Meaning | | --- | --- | | `name` | the child's name (`bidder-1`, `gateway`, an ssh gateway's name), a DNS label | | `kind` | `MarketBidder`, `MarketRequester`, `Gateway` or `SshGateway` | | `profile` | the child's profile | | `policy` | the bidder policy; unset means `bidder-tangled-vouch` (the `dev up` default); `open` only when `dev up --bidder-policy open` or app-values name it | | `firecracker` | the FirecrackerProvider for a bidder | | `kcp` | the KcpProvider for a bidder (handle, vmHostBackend, guestImageSource, stopGraceSeconds) | | `options` | application options passed through to the root object | | `quota` | the child's quota | | `ownerSessionsFile` | the owner sessions file for a gateway or ssh gateway | | `clusterProvider` | the cluster provider for a `Gateway` | | `components` | overrides of the child's components | | `suspend` | suspend this child only | | `stopped` | the child's core and components are stopped; its workspace and state dir are kept | ### MarketNetwork status | Field | Meaning | | --- | --- | | `phase` | `Pending`, `Provisioning`, `Ready`, `Suspended`, `Deleting` or `Failed` | | `message` | the current step or the failure | | `network` | the network value (see below) | | `bidderPdsUrl` | the PDS URL the bidders make their accounts on (bob's PDS) | | `components`, `listeners` | the network's components and listeners | | `componentSummary`, `componentOverrides` | the component counts and the state of each override | | `apps` | each child: `name`, `kind`, `instance`, `kcpUrl`, `workspace`, `phase`, `message`, `did`, `components`, `listeners` | | `stateDir`, `gc`, `ttl` | state directory usage, garbage collection and TTL | | `conditions` | see [Conditions](#conditions) | The network value (`status.network`, copied onto each child's root object): | Field | Meaning | | --- | --- | | `plcDirectoryUrl` | the network PLC directory URL (the plc local Exposure) | | `ingressProxyHost` | the dispatcher's host and port, `relay.localhost:` | | `qrOrigin` | the QR session-transfer origin; the market network leaves it empty | | `caBundle` | the CA bundle the children trust, inline PEM | | `hosts` | the network's service names and their loopback addresses | | `comms` | the market comms mode | | `ingress` | the network's PDS ingress selection: provider, the `dispatcher` ingress ref, and any xrpc or opentunnel settings; children inherit it | The guest TLS front URL is not a field: it is `https://relay.localhost:`, with the port of the `dispatcher-tls.default.relay.svc.kcp.local` entry of `hosts`. The network has one `IngressProxy/dispatcher` for its PDSes. `MarketNetwork.spec.ingress` selects its provider; with no spec it is `xrpc-subscription`. `spec.ingress.opentunnel` selects `opentunnel`, and `spec.ingress.provider` names the provider explicitly; the network PDSes refuse `iroh`. The selected value is published as `status.network.ingress`; every child root inherits it, and a child's own `spec.ingress` overrides it. Alice, bob and the PDS of every child app are exposed through the dispatcher with ingress Exposures. ### Conditions | Condition | Holds when | | --- | --- | | `ServicesReady` | every component the network composes reports ready with its endpoints | | `OneKcp` | every child reports the network's kcp front and a workspace of its own | | `VerifierPassed` | the composer's probes answer: plc `/health`, relay, alice and bob `/xrpc/_health`, dispatcher `/.well-known/did.json`, and every bidder and gateway child is Ready | | `PdsIngressAlice`, `PdsIngressBob` | the PDS's ingress is ready | | `ComponentsReady`, `ComponentOverridesValid` | the component summary and the overrides | The network is Ready only when `ServicesReady`, `OneKcp` and `VerifierPassed` hold. The probes run in the core (`internal/app/marketnetwork/vault.go`); there is no separate verifier process. A requester invocation reports in its own workspace: the ComputeContract's `status.contract` (winner, bids, receipt, the guest's host keys, `release` with the vm.delete status, and `termination` when the bidder ended the contract, for example a guest that powered itself off). `run request` prints a summary of it. ## The manifest set The network's manifests are the `market-network` set in `deploy/sets.yaml`. The composer applies only its `vault` phase; the network's components are not manifests but objects the composer writes (see [How components start](#how-components-start)). ### Manifest files | File | Kind | What it is | | --- | --- | --- | | `05-vault.yaml` | `OpenBaoServer` | `OpenBaoServer network`: the `pki` mount, the `network-pki` policy, the clients `components` and `owner`; phase `vault`, applied by the composer | | `00-workspaces.yaml` | `Workspace` | the `global`, `relay`, `alice` and `bob` workspaces (type `universal`); phase `workspaces`, which the composer does not apply | | `10-rbac.yaml` | `ServiceAccount`, `ClusterRole`, `ClusterRoleBinding` | a service account that may get OpenBaoServer objects, and the `pds-components` access of the `pds` account; applied in `root:app` by the `market-bidder`, `market-requester` and `ssh-gateway` sets | ### How components start The composer writes one object per component (`Plc`, `Relay`, `IngressProxy`, `DispatcherTLS`, `Pds`, `PolicyEngine`) and one Service and one or more Exposure objects per published handler, by server-side apply under the field manager `socialweb.computer/marketnetwork`, each with an ownerReference to the root and the root labels. It also writes the ComponentLinks those components call through. Each component's state dir is `/network/components/-`. The component controller (`internal/controllers/component`) in the core reconciles each object into the component runtime (`internal/component`). The runtime builds the Go service from the factory registered for its kind (`internal/stack/componentcore`), starts it in process, restarts it with backoff when it fails, and reports its phase, readiness and endpoints in the object's status. A Service names a component and a handler; an Exposure publishes the Service, local on a loopback port or through an ingress (`internal/exposure`). Third-party binaries, such as OpenBao and the child cores, run as HostProcess units instead. `spec.components[]` overrides a component (for example `suspend`, which stops it). `socialweb-computer dev restart mn1 [/][:][/]` restarts one component; the PolicyEngine restarts only with `core`. ### Services and addresses | Workspace | Component | Host name | Port lease | | --- | --- | --- | --- | | `root` | `Plc plc` | `plc.default.global.svc.kcp.local` | `Global/plc` | | `root` | `Relay relay` | `relay.default.relay.svc.kcp.local` | `Relay/relay` | | `root` | `IngressProxy dispatcher` | `dispatcher.default.relay.svc.kcp.local` | `IngressProxy/http` | | `root` | `DispatcherTLS dispatcher-tls` | `dispatcher-tls.default.relay.svc.kcp.local`, serving `relay.localhost` | `DispatcherTLS/tls` | | `root` | `Pds alice` (handle `alice.test`) | `pds.default.alice.svc.kcp.local` | `AlicePds/http` | | `root` | `Pds bob` (handle `bob.test`) | `pds.default.bob.svc.kcp.local` | `BobPds/http` | | `root` | `PolicyEngine policy-engine` | none | none | The host names keep the shape `...svc.` of the workspaces the services once ran in. They are fixed names in `internal/app/marketnetwork` and `api/v1alpha1`, not derived from `root`. plc, relay and both PDSes publish a Service `svc--` with a local Exposure `svc---local` and an ingress Exposure `svc---ingress`. The network value takes the plc, relay and PDS addresses from the local Exposures, and the dispatcher and dispatcher-tls addresses from those components' endpoints. ## Troubleshoot a market network - Start with `dev status --instance mn1`. `.network.status.message` names the current step, `.network.status.conditions` the condition that does not hold, and `.network.status.apps[].message` why a child is not Ready. - `socialweb-computer units list --instance mn1` lists the instance's units. The core's log is `logs/core.log` in the instance's state dir; a child's is in `apps/dev/-/logs/core.log` under it. - "waiting for the network's PDS ingress URLs" means `IngressProxy dispatcher` has not published the PDSes yet. Every network PDS requires an ingress. - "the root API of is not served in its workspace yet" means the child's core has not bound the application APIs in its workspace; the network retries. - A dial to a name that is not in `status.network.hosts` fails with "the host is not in the network host map": system DNS is not allowed. - A binding that is not Ready lists empty but rejects creates. Waiting for a resource to be listable is not waiting for its binding to be Ready. - kubectl caches discovery per server, and the cache outlives a schema change. A cached `NAMESPACED=false` makes kubectl POST a namespaced resource without a namespace. Use a throwaway `--cache-dir` when you read the network with kubectl. ## Limits - **Real DNS for humans is not available.** `curl pds.default.alice.svc.kcp.local` from a shell does not resolve. Use the address of that name in `status.network.hosts`, or the `status.address` of its local Exposure. - **Only names in the host map are dialled.** A subprocess or library that does not use `internal/kit/networkdial` or the bus does not see the network's names. - **Every network PDS requires an ingress.** Without `IngressProxy dispatcher` a PDS has no public URL and the network does not become Ready. - **The guest TLS CA is local.** The composer makes it in the instance's state; only a client that trusts the network CA bundle verifies `relay.localhost`.