~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
373 lines · 23.8 KB Raw History

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 on a local network
  • example files: ../examples/infra/
  • against prod bidders (the browser page, the CLI per contract, the prod gateway, the agent join): quickstart-prod.md
  • deeper: ../deploy/market-network/README.md (the network), SYSTEM.md, KCP.md
flowchart TD
  CLONE["git clone + build<br/>scripts/build-socialweb-computer.sh --dev"] --> UP["dev up --profile market-network<br/>--bidders 1 --with-gateway"]
  UP --> KC["eval dev env: KUBECONFIG<br/>server .../clusters/root:&lt;ws&gt;"]
  KC --> REQ["MarketNetwork spec.apps += MarketRequester qs<br/>workspace root:qs"]
  REQ --> PRE["prerequisites in root:qs<br/>MarketIdentity (did), OpenBaoServer requester,<br/>vouch records"]
  PRE --> DEF["define Infra / EphemeralInfra<br/>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<br/>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)<br/>(join: quickstart-prod.md)"]
  POD["kubectl apply a pod to the tenant cluster"] --> NC["NodeClaim -> gateway Infra nodes-&lt;hex&gt;"]
  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
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

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

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:<app>, its components in root:<app>: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:

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 <did>; 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 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 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=<app password> $SWC run requester --name pa --network $I --state-dir "$ST" -- --atproto-identity-mode pdsAccount --atproto-handle <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):

objectwhat to read
MarketRequester appstatus.phase Ready, status.did
MarketIdentity requesterthe requester's DID; mode localRepo (its own Pds component pds in root:qs)
AtprotoRepo repo-<did with - for :>the requester's own repo
OpenBaoServer requesterstatus.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.

directionrecordin whose repowhy
bidder operator -> requesterrkey = requester DIDthe bidder operator's repothe 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 -> bidderrkey = bidder DID (or its operator)the requester's repothe 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-<stamp of the requester DID>, 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 and vouch-bidder-operator.yaml are the same two records by hand, for a requester or bidder the network does not compose.

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 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, the same placeholder entry) into the requester's own OpenBao before applying:

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.

fieldmeaning
spec.descriptionfree text, max 1024
spec.contracts[]one ComputeContract each; name (DNS label) + state + every ComputeContractTemplate field inline
spec.contracts[].stateStaged (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.clusterRefthe Cluster an Infra serves; set by the NodeClaim controller
spec.recordoptional mirror to computer.socialweb.infra / ephemeralInfra in a repo (repoRef, sync Both or Publish, uri, eventSourceRefs)
EphemeralInfra spec.ttlSecondsdefault 3600, min 60; ends at creation + ttl
EphemeralInfra spec.endWhenComplete (default: ends when every requested contract is Released or Failed) or Deadline (only the ttl)

ComputeContractTemplate fields used here (api/v1alpha1/types_computecontract.go):

fieldmeaning
vmNamethe guest name the RFP asks for
sshAuthorizedKeys[]your public keys, for ssh as root
userDatacloud-init for vm; the Deno program for deno
computeKindvm (default) or deno
agent.envFilean atirc agent; default /etc/atirc/atirc.env; needs spec.secrets
bidderDids[], denyBidderDids[]pin or exclude bidders
tunnels[]`{tag, port, transport opentunnel
policy, policyArgsleave out: the default requester-tangled-vouch applies

Examples, one per scenario:

filekindscenario
vm.yamlInfraone firecracker VM, your ssh key, a cloud-init marker
k8s-cluster.yamlDeploymentapplied to the tenant cluster; its node comes from the gateway's Infra
deno-sandbox.yamlEphemeralInfraone Deno program, endWhen: Complete
agent-vm.yamlEphemeralInfraone VM with spec.agent and its secrets ref
agent-start.yamlContractEventguest.agentStart for the agent VM
mixed.yamlEphemeralInfravm1 + sbx1 + agent1 for 30 minutes (the browser page uses the same file)
vouch.yamlComponentLink + AtprotoRecordthe requester's vouch for the bidder
vouch-bidder-operator.yamlAtprotoRecordthe bidder operator's vouch for the requester

6. Apply And Await

[ -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:

levelvalues
Infra / EphemeralInfra status.phasePending, Staged, Deploying, Deployed, Releasing, Released, Degraded, Ending, Deleting, Ended
status.contracts[].phaseStaged, Requesting, Deployed (contract Held), Releasing, Released, Failed
ComputeContract status.phaseRequesting, Held, Releasing, Released, Failed; status.message names the step
EphemeralInfra extrastatus.expiresAt, status.endedAt, status.endReason (Complete, Deadline, Deleted)
  • child names: Infra infra-<infra>-<contract>, EphemeralInfra einfra-<infra>-<contract>
  • 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.

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 <name> --contract <cc> 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-<hex> in root:gateway, and that entry rents a VM that joins as a k3s node.

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 <account>-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 section 8); its nodes-<hex> Infra is in root:me
  • the Infra mirrors to at://<owner did>/computer.socialweb.infra/nodes-<hex> (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.

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://<computevm>
  • 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)

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-<name>.requester.local (the workspace, - for :), so locally the contract is Failed with refused: the atirc agent needs a requester handle that resolves to <did>: ... 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 <label>-<rfp rkey>.<handle> at irc.fedfork.com into the bundle (/etc/atirc/atirc.env, 0600; never in a kcp object, record or log), the guest has atirc-agent.service installed and inactive, and examples/infra/agent-start.yaml (guest.agentStart) starts it
  • the join needs a requester identity irc.fedfork.com resolves: quickstart-prod.md section 5 (which identity signs, the ingress options of a local PDS)

All in one

mixed.yaml: vm1, sbx1 and agent1 in one EphemeralInfra quickstart for 30 minutes (endWhen: Deadline). Put your key in vm1, apply, and read each contract as above (einfra-quickstart-vm1, einfra-quickstart-sbx1, einfra-quickstart-agent1); locally agent1 is refused before its RFP (the .local handle, above) while vm1 and sbx1 run.

8. Release And Clean Up

k root:qs patch infras.socialweb.computer quickstart-vm --type json \
  -p '[{"op":"replace","path":"/spec/contracts/0/state","value":"Released"}]'
k root:qs wait infras.socialweb.computer/quickstart-vm --for=jsonpath='{.status.phase}'=Released --timeout=2m
k root:qs delete infras.socialweb.computer quickstart-vm
kubectl --kubeconfig "$TK" delete -f examples/infra/k8s-cluster.yaml
$SWC dev down --instance $I --state-dir "$ST"
  • Released keeps the object; delete releases every contract first (finalizer infra.socialweb.computer/release), then deletes the ComputeContracts and the record
  • an EphemeralInfra deletes itself at its end (Complete or Deadline; see the Complete note in section 7)
  • the tenant node: deleting the pod deletes the NodeClaim, the Infra entry goes Released, the VM goes; the provider waits about 2 minutes idle first
  • dev down releases every VM the bidder still holds and removes the instance state

Not Supported Locally Today

Each one is an item of plan 0039:

gapplan 0039 item
an atirc agent join from a local network requester (its DID is on the network PLC and its handle is .local; a local PDS on plc.directory with a public handle and ingress mints: quickstart-prod.md section 5)D-AGENT-IDENTITY is on main; the join is a prod step: acceptance/0013-mixed-prod.md