Walkthrough
Follow along in your own environment. Copy the commands and adapt them to match your setup. The walkthrough assumes you have completed the one-time environment setup and that docker, minikube, kubectl, and python3 all resolve on your PATH.
Complete the Hello Pico on Manifold lab so the single-Pico runtime story (Manifold as RuntimeEnvironment on a Kubernetes RuntimeSubstrate, one Wrangler-declared Channel, one event, one Observation) is already familiar. This lab extends that story by giving the hosted Pico Hands — one declared capability, one allow rule, one reversible action, one evidence record.
Step 1 — Create a working directory and copy the manifests
From the root of your local clone of the academy repository:
mkdir -p work/hello-pico-hands-kubernetes && cd work/hello-pico-hands-kubernetes
cp -r ../../labs/hello-pico-hands-kubernetes/downloads/. .
ls -1Expected output (order-independent):
01-namespace.yaml
02-hands-contract.yaml
03-rbac.yaml
04-state-configmap.yaml
05-events-configmap.yaml
06-pico-engine.yaml
event.json
verify.sh
02-hands-contract.yaml is the provider-neutral Hands contract: identity, declared capabilities, and rules. 03-rbac.yaml is the authorization boundary — a namespaced Role scoped by resourceNames to the two Pico-owned ConfigMaps. verify.sh is the automatable check the walkthrough runs in Step 8.
Step 2 — Start a local minikube cluster
Start a dedicated minikube profile so this lab does not interfere with other clusters you may run (for example the manifold-lab profile from the Hello Pico on Manifold lab):
minikube start --profile hands-lab \
--driver=docker --cpus=2 --memory=4g \
--kubernetes-version=v1.31.0Confirm the cluster is Ready:
kubectl config current-context
kubectl get nodesExpected output:
hands-lab
NAME STATUS ROLES AGE VERSION
hands-lab Ready control-plane 1m v1.31.0
Step 3 — Read the Hands contract
Before applying anything, read 02-hands-contract.yaml. The data block is the provider-neutral Hands contract: identity.json carries the Pico and the acting principal, hands.json declares one pico.state.set capability targeting one Pico-owned ConfigMap, and rules.json declares one allow rule (narrowed to the hands namespace and the pico-hello-hands-state resourceName) plus one explicit deny for kubernetes.resource.delete.
sed -n '/^data:/,$p' 02-hands-contract.yamlNothing about this contract is Kubernetes-specific — a LocalHands or PhysicalHands provider could consume the same hands.json and rules.json and implement the same pico.state.set capability against its own targets. Step 4 binds this contract to the concrete KubernetesHands adapter.
Step 5 — Start the Pico engine
Apply the Pico engine Pod. It runs as the pico-hello-hands ServiceAccount, mounts the Hands contract at /etc/pico/, and invokes its one Kubernetes Hand.
kubectl apply -f 06-pico-engine.yamlStep 6 — Wait for the Pico Hand to complete
The Pico engine is a single-shot Pod. It reaches Succeeded only when the full pico.hand.* lifecycle terminated in succeeded:
kubectl -n hands wait --for=jsonpath='{.status.phase}=Succeeded' \
--timeout=120s pod/hello-hands-pico
kubectl -n hands logs pod/hello-hands-picoExpected log lines (order-preserving):
pico[hello-hands-pico] pico.hand.requested capability=pico.state.set
pico[hello-hands-pico] pico.hand.authorized capability=pico.state.set
pico[hello-hands-pico] pico.hand.succeeded capability=pico.state.set
pico[hello-hands-pico] evidence: {"execution":{"actor":"hello-hands-pico","pico":"hello-hands-pico","capability":"pico.state.set","provider":"kubernetes","target":{"kind":"ConfigMap","namespace":"hands","resourceName":"pico-hello-hands-state","key":"greeted"},"requested_at":"...","status":"succeeded","reason":""}}
Step 7 — Read back the state and events ConfigMaps
The reversible action is one merge-patch on the Pico-owned state ConfigMap. Read it back to confirm the Hand landed:
kubectl -n hands get configmap pico-hello-hands-state \
-o jsonpath='{.data.greeted}'; echoExpected output:
Hello, Pico!
The Pico also appended the normalized lifecycle to the Pico-owned events ConfigMap:
kubectl -n hands get configmap pico-hello-hands-events \
-o jsonpath='{.data.events}'; echoExpected output (newline-separated):
pico.hand.requested\npico.hand.authorized\npico.hand.succeeded
Capture both, plus the evidence line, as the lab’s runtime evidence:
mkdir -p ../../build/hello-pico-hands-kubernetes
kubectl -n hands get configmap pico-hello-hands-events \
-o jsonpath='{.data.events}' \
> ../../build/hello-pico-hands-kubernetes/events.txt
kubectl -n hands logs pod/hello-hands-pico \
| sed -n 's/^pico\[hello-hands-pico\] evidence: //p' \
> ../../build/hello-pico-hands-kubernetes/evidence.jsonStep 8 — Run the automated verifier
Static checks always run; the live path runs when kubectl and a cluster are reachable, so this step re-executes everything above and asserts the invariants:
bash verify.shExpected final line:
verify: OK — live — state=Hello, Pico!, lifecycle=[requested, authorized, succeeded]
Step 9 — Reverse the action and clean up
The action is reversible by construction — re-apply the empty state ConfigMap to remove the greeted key, then delete the namespace to remove the entire Pico surface:
kubectl apply -f 04-state-configmap.yaml
kubectl -n hands get configmap pico-hello-hands-state -o yaml \
| grep -E '^data:' -A 1Expected output:
data: {}
Then tear the cluster down:
kubectl delete namespace hands --ignore-not-found
minikube delete --profile hands-lab