# Quickstart (local): your own market network, an Infra, and every kind of compute in it
- clone -> build -> `dev up` a market network -> apply prerequisites -> define Infra -> apply -> await -> use -> release -> `dev down`
- one local market network (one kcp, its own PLC, relay, PDSes, a bidder, a cluster gateway) on one Linux host; nothing here touches prod
- scenarios: firecracker VM, tenant Kubernetes cluster, Deno sandbox, VM with an atirc agent (refused before its RFP locally: `.local` handle), all in one EphemeralInfra
- everything on this page is proven by acceptance [0012](acceptance/0012-infra-quickstart.md) on a local network
- example files: [../examples/infra/](../examples/infra/)
- against prod bidders (the browser page, the CLI per contract, the prod gateway, the agent join): [quickstart-prod.md](quickstart-prod.md)
- deeper: [../deploy/market-network/README.md](../deploy/market-network/README.md) (the network), [SYSTEM.md](SYSTEM.md), [KCP.md](KCP.md)
```mermaid
flowchart TD
CLONE["git clone + build scripts/build-socialweb-computer.sh --dev"] --> UP["dev up --profile market-network --bidders 1 --with-gateway"]
UP --> KC["eval dev env: KUBECONFIG server .../clusters/root:<ws>"]
KC --> REQ["MarketNetwork spec.apps += MarketRequester qs workspace root:qs"]
REQ --> PRE["prerequisites in root:qs MarketIdentity (did), OpenBaoServer requester, vouch records"]
PRE --> DEF["define Infra / EphemeralInfra examples/infra/*.yaml"]
DEF --> APPLY["kubectl apply -f"]
APPLY --> CC["Infra controller: one ComputeContract per contract"]
CC --> RFP["Requester: RFP -> bid -> accept -> receipt"]
RFP --> HELD["ComputeContract Held Infra contract Deployed"]
HELD --> VM["VM: ssh over iroh (dumbpipe)"]
HELD --> SBX["DenoSandbox: status.contract.sandbox.stdout"]
HELD --> AG["agent VM: refused locally (.local handle) (join: quickstart-prod.md)"]
POD["kubectl apply a pod to the tenant cluster"] --> NC["NodeClaim -> gateway Infra nodes-<hex>"]
NC --> RFP
HELD --> K8S["tenant node Ready, pod Running"]
VM --> REL["state: Released or kubectl delete"]
SBX --> REL
AG --> REL
K8S --> REL
REL --> DOWN["dev down"]
```
## 1. Clone And Build
- Linux host, run as the account that owns a subordinate uid range (here the VM account `test-0001`); `dev up` refuses root
- needs `/dev/kvm`, `kubectl`, `jq`, `ssh` on PATH
```bash
git clone https://github.com/publicdomainrelay/socialweb-computer-kcp
cd socialweb-computer-kcp
scripts/build-socialweb-computer.sh --dev --publish /var/tmp/qs # or: make dev (publishes to /var/tmp/swc)
SWC=/var/tmp/qs/bin/socialweb-computer
$SWC version
```
- the `--dev` build embeds every pinned asset: kcp, kine, OpenBao, dumbpipe, firecracker, vmexec, k3s, deno
- it refuses `--dumbpipe-path`; it runs its embedded dumbpipe
## 2. Start A kcp With A Market In It
```bash
I=qs # instance name, unique on the host
ST=/var/tmp/qs-state # short: tenant socket paths must fit in 107 bytes
mkdir -p "$ST"
echo '{"bidder":{"kcp":{"handle":"{app}.qs.bob.test"}}}' > "$ST/app-values.json"
$SWC dev up --instance $I --profile market-network --host-backend proc --bidders 1 --with-gateway --compute-kinds vm,deno \
--guest-image-source file:///var/lib/socialweb-computer/guest-image --vm-host-backend systemd-user \
--app-values "$ST/app-values.json" --ready-timeout 90s --network-timeout 6m --state-dir "$ST"
$SWC dev status --instance $I --state-dir "$ST" | jq -r '.network.status.phase, (.network.status.apps[] | "\(.name) \(.phase)")'
```
- one kcp, workspace `root` holds `MarketNetwork network`; children: `root:bidder-1` (MarketBidder), `root:gateway` (Gateway + tenant cluster `tenant` at zero nodes)
- `--compute-kinds vm,deno` sets `spec.computeKinds` on every network bidder's MarketBidder (the same values and check
as `run bidder --compute-kinds`); without it a network bidder serves `vm` only and a deno contract gets no bid
- `--guest-image-source` names a guest image dir (`image.json`, `rootfs.ext4`, `vmlinux`); without it `dev up` fetches the pinned one
- Ready when `.network.status.phase` is `Ready` (about 35 s on hat-8)
- network bidders run the default `bidder-tangled-vouch` and also the contract's attached `requester-tangled-vouch`
before they bid; the network composes the vouches of section 4 for its own requesters. `open` runs only when you
name it (`--bidder-policy open` or `{"bidder":{"policy":"open"}}` in `--app-values`); it is insecure
- `dev up` waits through a component that is Failed for a moment (seen: gateway Requester "names no identity DID or handle"); it fails only when one stays Failed for a minute
## 3. kubectl Access
```bash
eval "$($SWC dev env --instance $I --state-dir "$ST")" # KUBECONFIG, SOCIALWEB_KCP_HOST
k() { ws=$1; shift; kubectl --cache-dir "$ST/kcache" --server "$SOCIALWEB_KCP_HOST/clusters/$ws" "$@"; }
k root get workspaces
k root get marketnetworks.socialweb.computer network -o jsonpath='{.status.phase}{"\n"}'
```
- every app is a workspace of the one kcp: `root:`, its components in `root::app`
- name kinds by full resource name (`infras.socialweb.computer`): several exports share short names
- `--cache-dir` throwaway: kubectl discovery caches outlive schema changes
## 4. Prerequisites
### A requester that stays up
An Infra lives in a requester's workspace; that requester's core reconciles it. Add one to the network:
```bash
k root apply --server-side --field-manager socialweb.computer/network-app/qs -f - <<'EOF'
apiVersion: socialweb.computer/v1alpha1
kind: MarketNetwork
metadata: {name: network}
spec:
apps:
- {name: qs, kind: MarketRequester, profile: market-network}
EOF
k root get marketnetworks.socialweb.computer network -o json | jq -r '.status.apps[] | select(.name=="qs") | "\(.phase) \(.did)"'
```
- the last line prints nothing until the network lists the app, then `Pending`, then `Ready `; run it again until it
reads Ready before section 6
Or `$SWC run requester --name qs --network $I --state-dir "$ST"` in its own terminal: the same child application, plus
a ready file and a kubeconfig of the workspace `root:qs` (`$ST/requesters/qs/`; [quickstart-prod.md](quickstart-prod.md)
section 7). A restart must repeat `--network $I`, and must wait after the Ctrl-C until the child's dev workspace is gone
(`k root:dev get workspaces $I-qs` NotFound, a few seconds): a restart while it is Deleting fails after 3 minutes
([acceptance/0012](acceptance/0012-infra-quickstart.md) Known Pitfalls). Held contracts stay Held across the restart.
With `--network` the identity is the network's own (a `.local` handle) unless you give an account on the network PDS (`.network.status.bidderPdsUrl` of `dev status`; make one there with
`com.atproto.server.createAccount`, a handle under `.bob.test`):
`ATPROTO_PASSWORD= $SWC run requester --name pa --network $I --state-dir "$ST" -- --atproto-identity-mode
pdsAccount --atproto-handle ` (the PDS URL defaults to the network PDS; `run request --network` takes the same
options). The password goes into the requester's own OpenBao; set it from a file in that one command's environment. No
other identity mode is taken with `--network`, and pdsAccount needs `--atproto-handle`.
What it composes in `root:qs` (read them, do not write them):
| object | what to read |
|---|---|
| `MarketRequester app` | `status.phase` Ready, `status.did` |
| `MarketIdentity requester` | the requester's DID; mode `localRepo` (its own Pds component `pds` in `root:qs`) |
| `AtprotoRepo repo-` | the requester's own repo |
| `OpenBaoServer requester` | `status.clients[name=requester]`: kv mount `requester`, cert auth role `requester` |
| `PolicyEngine policy-engine` (`policyengines.policy.socialweb.computer`) | runs every policy decision |
### Vouches
Default policy everywhere is tangled-vouch: the contract's attached policy `requester-tangled-vouch` and, on prod
bidders, `bidder-tangled-vouch`. Never set `policy: open`; it is insecure and not for user-facing use. Both
policies read `sh.tangled.graph.vouch` records: rkey = the subject DID, `record.subject` = that DID, any `kind`
but `denounce` counts.
| direction | record | in whose repo | why |
|---|---|---|---|
| bidder operator -> requester | rkey = requester DID | the bidder operator's repo | the bidder runs the RFP's attached policy before it bids, as itself: the requester must be in the bidder (operator) vouch graph; `bidder-tangled-vouch` asks the same |
| requester -> bidder | rkey = bidder DID (or its operator) | the requester's repo | the requester's own check of the bid: the bidder must be in the requester's vouch graph |
- verdicts and trust lookups are cached for 5 minutes (`defaultTrustCacheTTL`), denials too: write the vouches
before the first Infra, or wait 5 minutes after a `not vouched`
- an Infra contract carries no `policy` field; the default applies
- `MarketBidder app` `status.did` is the bidder DID; `MarketIdentity bidder` `status.approvedAssociations` holds its operator
In the local network the MarketNetwork composes both records for every MarketRequester app (yours, and each `run
request --network` invocation), once the requester and the bidder have DIDs; read them, do not write them:
- in `root:bidder-1`: AtprotoRecord `operator-vouch-requester-`, the bidder operator's
record for the requester, through the bidder's link `link-records-operator` (label `apps.socialweb.computer/record-repo: operator`)
- in `root:qs`: AtprotoRecord `network-vouch-bidder-1`, the requester's record for the bidder, through the ComponentLink
`link-network-vouches-root-qs` (AtprotoRecords labelled `apps.socialweb.computer/network-vouch`)
- `run request --network` waits until both are Applied before it posts its contract
The requester's record is `deletionPolicy: Retain`. Nothing here deletes a user's record; remove a vouch from the
account itself (`goat`). [vouch.yaml](../examples/infra/vouch.yaml) and
[vouch-bidder-operator.yaml](../examples/infra/vouch-bidder-operator.yaml) are the same two records by hand, for a
requester or bidder the network does not compose.
```bash
k root:bidder-1 get atprotorecords.socialweb.computer -l apps.socialweb.computer/record-repo=operator \
-o jsonpath='{range .items[*]}{.metadata.name} {.status.phase} {.status.uri}{"\n"}{end}'
k root:qs get atprotorecords.socialweb.computer network-vouch-bidder-1 -o jsonpath='{.status.phase} {.status.uri}{"\n"}'
```
On prod the same two records are written by people with their own accounts: [quickstart-prod.md](quickstart-prod.md) section 2.
### Secrets bundle (agent VMs only)
A contract with `spec.agent` needs `spec.secretsRef`: the requester writes the minted atirc token into that bundle and
the bidder hands the bundle to the guest. When the path holds no bundle, the requester writes the placeholder itself
(one entry at `agent.envFile`, mode 0600). To write it by hand ([../examples/infra/agent-bundle.json](../examples/infra/agent-bundle.json),
the same placeholder entry) into the requester's own OpenBao before applying:
```bash
D=$ST/dev/$I/apps/dev/$I-qs/openbaoservers/requester/openbao
BAO=$(find ~/.cache/socialweb-computer/runtime -maxdepth 2 -name bao -type f | head -1)
export BAO_ADDR=$(k root:qs get openbaoservers.socialweb.computer requester -o jsonpath='{.status.address}')
export BAO_CACERT=$D/tls/ca.crt BAO_CLIENT_CERT=$D/clients/requester/client.crt BAO_CLIENT_KEY=$D/clients/requester/client.key
BAO_TOKEN=$($BAO write -field=token auth/cert/login name=requester) \
$BAO kv put -mount=requester contracts/einfra-quickstart-agent-agent1/secrets bundle=@examples/infra/agent-bundle.json >/dev/null
```
- the token lives only in that shell's environment for one command; nothing is printed
- the path must match `spec.secrets[].secretsRef.path` of the example
## 5. Define An Infra Or EphemeralInfra
Both kinds: group `socialweb.computer/v1alpha1`, export `computecontracts`, types in `api/v1alpha1/types_infra.go`.
| field | meaning |
|---|---|
| `spec.description` | free text, max 1024 |
| `spec.contracts[]` | one ComputeContract each; `name` (DNS label) + `state` + every ComputeContractTemplate field inline |
| `spec.contracts[].state` | `Staged` (default: nothing), `Requested` (apply the contract, run the RFP), `Released` (release it) |
| `spec.secrets[]` | `{contract, secretsRef: {mount, path}}`: becomes that contract's `spec.secretsRef`; never written to a record |
| `spec.nodes[]` | tenant cluster node join (`contract`, `ownerDid`, `nodeJoin`, `nodeClaimRef`); written by the NodeClaim controller, not by you |
| `spec.clusterRef` | the Cluster an Infra serves; set by the NodeClaim controller |
| `spec.record` | optional mirror to `computer.socialweb.infra` / `ephemeralInfra` in a repo (`repoRef`, `sync` Both or Publish, `uri`, `eventSourceRefs`) |
| EphemeralInfra `spec.ttlSeconds` | default 3600, min 60; ends at creation + ttl |
| EphemeralInfra `spec.endWhen` | `Complete` (default: ends when every requested contract is Released or Failed) or `Deadline` (only the ttl) |
ComputeContractTemplate fields used here (`api/v1alpha1/types_computecontract.go`):
| field | meaning |
|---|---|
| `vmName` | the guest name the RFP asks for |
| `sshAuthorizedKeys[]` | your public keys, for ssh as root |
| `userData` | cloud-init for `vm`; the Deno program for `deno` |
| `computeKind` | `vm` (default) or `deno` |
| `agent.envFile` | an atirc agent; default `/etc/atirc/atirc.env`; needs `spec.secrets` |
| `bidderDids[]`, `denyBidderDids[]` | pin or exclude bidders |
| `tunnels[]` | `{tag, port, transport opentunnel|webrtc}` |
| `policy`, `policyArgs` | leave out: the default `requester-tangled-vouch` applies |
Examples, one per scenario:
| file | kind | scenario |
|---|---|---|
| [vm.yaml](../examples/infra/vm.yaml) | Infra | one firecracker VM, your ssh key, a cloud-init marker |
| [k8s-cluster.yaml](../examples/infra/k8s-cluster.yaml) | Deployment | applied to the tenant cluster; its node comes from the gateway's Infra |
| [deno-sandbox.yaml](../examples/infra/deno-sandbox.yaml) | EphemeralInfra | one Deno program, `endWhen: Complete` |
| [agent-vm.yaml](../examples/infra/agent-vm.yaml) | EphemeralInfra | one VM with `spec.agent` and its secrets ref |
| [agent-start.yaml](../examples/infra/agent-start.yaml) | ContractEvent | `guest.agentStart` for the agent VM |
| [mixed.yaml](../examples/infra/mixed.yaml) | EphemeralInfra | vm1 + sbx1 + agent1 for 30 minutes (the browser page uses the same file) |
| [vouch.yaml](../examples/infra/vouch.yaml) | ComponentLink + AtprotoRecord | the requester's vouch for the bidder |
| [vouch-bidder-operator.yaml](../examples/infra/vouch-bidder-operator.yaml) | AtprotoRecord | the bidder operator's vouch for the requester |
## 6. Apply And Await
```bash
[ -f ~/.ssh/id_ed25519 ] || ssh-keygen -q -t ed25519 -N '' -f ~/.ssh/id_ed25519 # no key yet
sed "s|ssh-ed25519 AAAAREPLACE-WITH-YOUR-PUBLIC-KEY you@laptop|$(cat ~/.ssh/id_ed25519.pub)|" examples/infra/vm.yaml > "$ST/vm.yaml"
k root:qs apply -f "$ST/vm.yaml"
k root:qs wait infras.socialweb.computer/quickstart-vm --for=jsonpath='{.status.phase}'=Deployed --timeout=3m
k root:qs get infras.socialweb.computer quickstart-vm -o json | jq '.status.contracts'
k root:qs get computecontracts.socialweb.computer -l infra.socialweb.computer/name=quickstart-vm
```
Phases:
| level | values |
|---|---|
| Infra / EphemeralInfra `status.phase` | Pending, Staged, Deploying, Deployed, Releasing, Released, Degraded, Ending, Deleting, Ended |
| `status.contracts[].phase` | Staged, Requesting, Deployed (contract Held), Releasing, Released, Failed |
| ComputeContract `status.phase` | Requesting, Held, Releasing, Released, Failed; `status.message` names the step |
| EphemeralInfra extra | `status.expiresAt`, `status.endedAt`, `status.endReason` (Complete, Deadline, Deleted) |
- child names: Infra `infra--`, EphemeralInfra `einfra--`
- `status.contracts[]` carries `computeContract`, `winner` (bidder DID), `receiptUri`, `message`
- `Degraded` + `no bids received within 30s`: no bidder bid. Read the bidder's Bids: `k root:bidder-1 get bids.socialweb.computer -o json | jq '.items[].status'`; `policy rejected ... not vouched` means a vouch is missing (section 4)
- a Failed contract stays Failed under `Requested`: set it `Released`, or delete and apply the Infra again
## 7. Use Each Resource
### VM: ssh over iroh
The contract reports the guest's iroh ticket and ssh host keys; ssh checks the keys strictly.
```bash
CC=infra-quickstart-vm-vm1
k root:qs get computecontracts.socialweb.computer $CC -o json > "$ST/cc.json"
TICKET=$(jq -r '.status.contract.vmAddress | sub("^iroh://"; "")' "$ST/cc.json")
ALIAS=$(jq -r '.status.contract.receipt.uri | ascii_downcase | gsub("[^a-z0-9]+"; "-") | ltrimstr("-") | rtrimstr("-")' "$ST/cc.json")
jq -r --arg a "$ALIAS" '.status.contract.hostKeys.keys[] | "\($a) \(.type) \(.publicKey)"' "$ST/cc.json" > "$ST/known_hosts"
DP=$(find ~/.cache/socialweb-computer/runtime -maxdepth 2 -name dumbpipe -type f | head -1)
ssh -i ~/.ssh/id_ed25519 -o IdentitiesOnly=yes -o ProxyCommand="$DP connect $TICKET 2>/dev/null" \
-o StrictHostKeyChecking=yes -o UserKnownHostsFile="$ST/known_hosts" -o HostKeyAlias="$ALIAS" -o CheckHostIP=no \
root@"$ALIAS" 'cat /root/quickstart; uname -m'
```
- prints `quickstart-vm-ok`, `x86_64`
- `2>/dev/null` on the ProxyCommand: dumbpipe prints its own ephemeral key on stderr
- `run request ssh --contract ` does the same for contracts of a `run request` invocation (`--transport webrtc` with a webrtc tunnel)
### Kubernetes cluster
A tenant cluster is not a contract you write. The gateway owns it; a pod that does not fit makes a NodeClaim, the
NodeClaim controller adds one entry to the cluster's Infra `nodes-` in `root:gateway`, and that entry rents a VM
that joins as a k3s node.
```bash
TK=$(find "$ST/dev/$I/apps/dev/$I-gateway/clusterprovider" -path '*-tenant/server/cred/admin.kubeconfig' | head -1)
kubectl --kubeconfig "$TK" apply -f examples/infra/k8s-cluster.yaml
k root:gateway get infras.socialweb.computer -l infra.socialweb.computer/cluster=tenant -o json \
| jq '.items[] | {name: .metadata.name, phase: .status.phase, clusterRef: .spec.clusterRef, contracts: .status.contracts}'
kubectl --kubeconfig "$TK" wait --for=condition=Ready pod -l app=quickstart-k8s --timeout=5m
kubectl --kubeconfig "$TK" get nodes -o wide
kubectl --kubeconfig "$TK" logs deploy/quickstart-k8s
```
- prints `quickstart-k8s-ok`; the node is `-tenant-node1`
- a tenant cluster of your own requester instead of the network's gateway, on a network brought up without
`--with-gateway` (its gateway child would serve every Cluster): `run requester --network $I --name me --with-gateway`, then `run requester cluster me --cluster c1` and `run requester kubeconfig me --cluster c1`
([quickstart-prod.md](quickstart-prod.md) section 8); its `nodes-` Infra is in `root:me`
- the Infra mirrors to `at:///computer.socialweb.infra/nodes-` (`status.record.uri`)
- the NodeClaim is in the cluster's home workspace (`spec.clusterRef.workspace`): `status.phase`, `status.contract.winner.did`, `status.infra`
### Deno sandbox (needs a deno bidder)
`computeKind: deno` runs `userData` as a Deno program on the bidder host (no VM): `deno run --no-remote --deny-net
--deny-run --deny-ffi`, read and write of its own work dir only, at most 2 minutes. The ComputeVM gets backend
`DenoSandbox`. A bidder serves `deno` only when told to: `dev up --compute-kinds vm,deno` (section 2) sets it on the
network bidders.
```bash
k root:bidder-1 get marketbidders.socialweb.computer app -o jsonpath='{.spec.computeKinds}{"\n"}' # ["vm","deno"]
k root:qs apply -f examples/infra/deno-sandbox.yaml
k root:qs wait ephemeralinfras.socialweb.computer/quickstart-deno --for=jsonpath='{.status.phase}'=Deployed --timeout=2m
k root:qs get computecontracts.socialweb.computer einfra-quickstart-deno-sbx1 -o jsonpath='{.status.contract.sandbox}{"\n"}'
```
- prints `{"exitCode":0,...,"stdout":"quickstart-deno-ok sum=10 on linux/x86_64\n"}`; `vmAddress` is `sandbox://`
- the contract goes Held once the result is reported; set `state: Released` to release it
- `endWhen: Complete` ends the object in the reconcile that sees every requested contract Released or Failed
(`infra_ephemeral_complete`); it is deleted then, not at `status.expiresAt`
- a released sandbox's ComputeVM is deleted like a VM's: termination StateSync (vm.terminated), sandbox stopped,
finalizer dropped
- a bidder without `deno` in its kinds drops the RFP before bidding: `no bids received within 30s`
### Agent VM via atirc (refused locally before the RFP)
```bash
k root:qs apply -f examples/infra/agent-vm.yaml # the bundle of section 4 is optional
k root:qs get computecontracts.socialweb.computer einfra-quickstart-agent-agent1 -o jsonpath='{.status.phase} {.status.message}{"\n"}'
```
What happens:
- when the contract's `secretsRef` path holds no bundle, the requester writes the placeholder bundle itself (one entry at
`agent.envFile`, `ATIRC_TOKEN=`, mode 0600; log `requester_agent_bundle_placeholder`)
- before the RFP the requester checks its own handle: irc.fedfork.com mints only for a handle that resolves to the DID.
The network requester's handle is `root-.requester.local` (the workspace, `-` for `:`), so locally the contract is Failed with `refused: the
atirc agent needs a requester handle that resolves to : ... is a .local handle that nobody resolves; start the
requester with --atproto-handle ...`, with no RFP and no record; a plain VM contract of the same requester still runs
- with a handle that resolves, after the receipt the requester mints a leased atirc token `