Skip to content

Commit 7c30537

Browse files
authored
feat: support sandbox mode (#67)
Signed-off-by: Jan Pokorný <JenomPokorny@gmail.com>
1 parent f2385b7 commit 7c30537

4 files changed

Lines changed: 132 additions & 92 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ Services are available at `*.localhost:4444` automatically (Traefik on port 4444
4747

4848
Use `mise run cluster:kubectl -- <args>` and `mise run cluster:shell -- <cmd>` instead of raw `kubectl` or `export KUBECONFIG=...`. These are auto-approved.
4949

50-
Activate cluster environment for interactive use: `eval "$(mise run humr:shell)"` (sets KUBECONFIG, adds prompt prefix, `deactivate` to undo).
50+
Activate cluster environment for interactive use: `export KUBECONFIG="$(mise run cluster:kubeconfig)"`.
5151

5252
## Architecture
5353

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ Kubernetes platform for running AI agent harnesses (Claude Code, Codex, Gemini C
1414
## Quick Start
1515

1616
```sh
17-
mise run setup # install deps, configure git hooks
17+
mise install # install deps, configure git hooks
1818
mise run cluster:install # create local k3s cluster + deploy Humr
1919
mise run cluster:status # check pods
20-
eval "$(mise run humr:shell)" # activate cluster env
20+
export KUBECONFIG="$(mise run cluster:kubeconfig)" # activate cluster env
2121
```
2222

2323
Open **`humr.localhost:4444`** in your browser, create an instance from a template, and start chatting.
@@ -55,6 +55,8 @@ mise run ui:run # start UI dev server
5555
mise run cluster:upgrade # redeploy after changes
5656
```
5757

58+
Humr detects it is running in a sandbox by env `IS_SANDBOX` and skips provisioning the Lima VM, instead installing k3s directly to avoid nested virtualization.
59+
5860
## Architecture
5961

6062
See [docs/architecture.md](docs/architecture.md) for full details.

deploy/tasks.toml

Lines changed: 123 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,16 @@
11
# -- Shell environment --
22

3-
["humr:shell"]
4-
description = 'Print shell activation script. Usage: eval "$(mise run humr:shell)"'
5-
dir = "{{cwd}}"
3+
["cluster:kubeconfig"]
4+
description = 'Print the KUBECONFIG path for the cluster. Usage: export KUBECONFIG="$(mise run cluster:kubeconfig)"'
65
quiet = true
7-
run = """
8-
cat <<EOF
9-
deactivate () {
10-
export PS1="\\${__OLD_PS1:-}"
11-
[[ -n "\\${__OLD_KUBECONFIG:-}" ]] && export KUBECONFIG="\\$__OLD_KUBECONFIG" || unset KUBECONFIG
12-
unset __OLD_PS1
13-
unset __OLD_KUBECONFIG
14-
unset -f deactivate
15-
echo "humr-k3s environment deactivated."
16-
}
17-
18-
while [[ -n "\\${__OLD_PS1:-}" ]]; do
19-
deactivate;
20-
done
21-
22-
echo "Activating humr-k3s environment..."
23-
24-
export __OLD_PS1="\\${PS1:-}"
25-
export __OLD_KUBECONFIG="\\${KUBECONFIG:-}"
26-
27-
export KUBECONFIG="\\${HOME}/.lima/humr-k3s/copied-from-guest/kubeconfig.yaml"
28-
export PS1="(humr-k3s) \\${__OLD_PS1}"
29-
EOF
30-
"""
6+
run = '''
7+
#!/usr/bin/env bash
8+
if [ -n "${IS_SANDBOX:-}" ]; then
9+
echo "/etc/rancher/k3s/k3s.yaml"
10+
else
11+
echo "$HOME/.lima/humr-k3s/copied-from-guest/kubeconfig.yaml"
12+
fi
13+
'''
3114

3215
# -- Image builds (docker only, no k3s interaction) --
3316

@@ -58,14 +41,16 @@ docker build -t humr-example-agent:latest packages/example-agent
5841

5942
# -- Cluster lifecycle (k3s via lima) --
6043
#
44+
# When IS_SANDBOX is set, it is assumed we are already _inside_ a VM.
45+
#
6146
# Both install and upgrade share the same flow:
62-
# ensure VM → cert-manager → build images → load into k3s → helm install/upgrade
47+
# ensure VM/k3s → cert-manager → build images → load into k3s → helm install/upgrade
6348
#
64-
# install: creates VM if needed, uses helm install
65-
# upgrade: assumes VM exists, uses helm upgrade
49+
# install: provisions VM/k3s if needed, uses helm install
50+
# upgrade: assumes VM/k3s exists, uses helm upgrade
6651

6752
["cluster:install"]
68-
description = "Create k3s cluster, build images, install cert-manager + Humr chart. Options: --vm-name=NAME --lima-template=PATH --helm-values=PATH --set key=val"
53+
description = "Create k3s cluster, build images, install cert-manager + Humr chart. Options: --vm-name=NAME --lima-template=PATH --helm-values=PATH --set=key=val"
6954
depends = ["image:controller", "image:apiserver", "image:ui", "image:agent"]
7055
dir = "{{config_root}}"
7156
raw = true
@@ -77,36 +62,39 @@ LIMA_INSTANCE="humr-k3s"
7762
LIMA_TEMPLATE="deploy/lima-k3s.yaml"
7863
HELM_VALUES=""
7964
HELM_EXTRA_ARGS=()
80-
NEXT_IS_SET_VALUE=false
8165
for arg in "$@"; do
82-
if $NEXT_IS_SET_VALUE; then
83-
HELM_EXTRA_ARGS+=("$arg")
84-
NEXT_IS_SET_VALUE=false
85-
continue
86-
fi
8766
case "$arg" in
8867
--vm-name=*) LIMA_INSTANCE="${arg#--vm-name=}" ;;
8968
--lima-template=*) LIMA_TEMPLATE="${arg#--lima-template=}" ;;
9069
--helm-values=*) HELM_VALUES="${arg#--helm-values=}" ;;
91-
--set) HELM_EXTRA_ARGS+=("--set"); NEXT_IS_SET_VALUE=true ;;
9270
--set=*) HELM_EXTRA_ARGS+=("$arg") ;;
9371
esac
9472
done
95-
KUBECONFIG="$HOME/.lima/$LIMA_INSTANCE/copied-from-guest/kubeconfig.yaml"
9673
97-
# 1. Ensure k3s VM is running
98-
if ! limactl list -q 2>/dev/null | grep -qx "$LIMA_INSTANCE"; then
99-
echo "Creating k3s VM ($LIMA_INSTANCE)..."
100-
limactl create --name="$LIMA_INSTANCE" "$LIMA_TEMPLATE" --tty=false
101-
fi
102-
103-
STATUS=$(limactl list --json | python3 -c "import sys,json; [print(json.loads(l)['status']) for l in sys.stdin if json.loads(l)['name']=='$LIMA_INSTANCE']" 2>/dev/null || echo "Stopped")
104-
if [ "$STATUS" != "Running" ]; then
105-
echo "Starting k3s VM..."
106-
limactl start $LIMA_INSTANCE
74+
# 1. Provision cluster and set KUBECONFIG
75+
if [ -n "${IS_SANDBOX:-}" ]; then
76+
KUBECONFIG="/etc/rancher/k3s/k3s.yaml"
77+
if [ ! -d /var/lib/rancher/k3s ]; then
78+
echo "Provisioning k3s on host (sandbox mode)..."
79+
for i in $(seq 0 $(($(yq '.provision | length' "$LIMA_TEMPLATE") - 1))); do
80+
yq ".provision[$i].script" "$LIMA_TEMPLATE" | sudo bash
81+
done
82+
fi
83+
echo "Waiting for k3s to be ready..."
84+
timeout 120 bash -c "until [ -f $KUBECONFIG ]; do sleep 2; done"
85+
else
86+
KUBECONFIG="$HOME/.lima/$LIMA_INSTANCE/copied-from-guest/kubeconfig.yaml"
87+
if ! limactl list -q 2>/dev/null | grep -qx "$LIMA_INSTANCE"; then
88+
echo "Creating k3s VM ($LIMA_INSTANCE)..."
89+
limactl create --name="$LIMA_INSTANCE" "$LIMA_TEMPLATE" --tty=false
90+
fi
91+
STATUS=$(limactl list --json | python3 -c "import sys,json; [print(json.loads(l)['status']) for l in sys.stdin if json.loads(l)['name']=='$LIMA_INSTANCE']" 2>/dev/null || echo "Stopped")
92+
if [ "$STATUS" != "Running" ]; then
93+
echo "Starting k3s VM..."
94+
limactl start "$LIMA_INSTANCE"
95+
fi
96+
echo "Waiting for k3s to be ready..."
10797
fi
108-
109-
echo "Waiting for k3s to be ready..."
11098
kubectl --kubeconfig="$KUBECONFIG" wait --for=condition=Ready node --all --timeout=120s
11199
112100
# 2. Install cert-manager
@@ -121,29 +109,22 @@ fi
121109
122110
# 3. Load images into k3s (built by depends: image:*)
123111
echo "Loading images into k3s..."
124-
for img in humr-controller:latest humr-api-server:latest humr-ui:latest humr-example-agent:latest; do
125-
tar="/tmp/${img%%:*}.tar"
126-
docker save "$img" -o "$tar"
112+
tar="/tmp/humr-images.tar"
113+
docker save -o "$tar" humr-controller:latest humr-api-server:latest humr-ui:latest humr-example-agent:latest
114+
if [ -n "${IS_SANDBOX:-}" ]; then
115+
sudo k3s ctr images import "$tar"
116+
else
127117
limactl copy "$tar" "$LIMA_INSTANCE":"$tar"
128118
limactl shell "$LIMA_INSTANCE" sudo k3s ctr images import "$tar"
129-
rm -f "$tar"
130-
done
119+
fi
120+
rm -f "$tar"
131121
132122
# 4. Helm install or upgrade
133-
HELM_VALUES_FLAG=""
134-
if [ -n "$HELM_VALUES" ]; then
135-
HELM_VALUES_FLAG="-f $HELM_VALUES"
136-
fi
137123
# Dev-cluster defaults (overridable via --set)
138124
DEV_DEFAULTS=(--set keycloak.admin.password=admin)
139125
140-
if helm --kubeconfig="$KUBECONFIG" status humr >/dev/null 2>&1; then
141-
echo "Upgrading Humr chart..."
142-
helm --kubeconfig="$KUBECONFIG" upgrade humr deploy/helm/humr -f deploy/helm/humr/values-local.yaml --timeout 10m $HELM_VALUES_FLAG "${DEV_DEFAULTS[@]}" "${HELM_EXTRA_ARGS[@]}"
143-
else
144-
echo "Installing Humr chart..."
145-
helm --kubeconfig="$KUBECONFIG" install humr deploy/helm/humr -f deploy/helm/humr/values-local.yaml --timeout 10m $HELM_VALUES_FLAG "${DEV_DEFAULTS[@]}" "${HELM_EXTRA_ARGS[@]}"
146-
fi
126+
echo "Installing/upgrading Humr chart..."
127+
helm --kubeconfig="$KUBECONFIG" upgrade --install humr deploy/helm/humr -f deploy/helm/humr/values-local.yaml --timeout 10m ${HELM_VALUES:+-f "$HELM_VALUES"} "${DEV_DEFAULTS[@]}" "${HELM_EXTRA_ARGS[@]}"
147128
148129
# 5. Restart pods to pick up new local images (dev only — :latest tags don't trigger rollout)
149130
echo "Restarting pods..."
@@ -169,14 +150,19 @@ run = '''
169150
#!/usr/bin/env bash
170151
set -eo pipefail
171152
172-
LIMA_INSTANCE="humr-k3s"
173-
KUBECONFIG="$HOME/.lima/$LIMA_INSTANCE/copied-from-guest/kubeconfig.yaml"
174-
175153
echo "Loading into k3s..."
176154
tar="/tmp/humr-ui.tar"
177155
docker save humr-ui:latest -o "$tar"
178-
limactl copy "$tar" "$LIMA_INSTANCE":"$tar"
179-
limactl shell "$LIMA_INSTANCE" sudo k3s ctr images import "$tar"
156+
157+
if [ -n "${IS_SANDBOX:-}" ]; then
158+
KUBECONFIG="/etc/rancher/k3s/k3s.yaml"
159+
sudo k3s ctr images import "$tar"
160+
else
161+
LIMA_INSTANCE="humr-k3s"
162+
KUBECONFIG="$HOME/.lima/$LIMA_INSTANCE/copied-from-guest/kubeconfig.yaml"
163+
limactl copy "$tar" "$LIMA_INSTANCE":"$tar"
164+
limactl shell "$LIMA_INSTANCE" sudo k3s ctr images import "$tar"
165+
fi
180166
rm -f "$tar"
181167
182168
echo "Restarting UI pod..."
@@ -194,14 +180,19 @@ run = '''
194180
#!/usr/bin/env bash
195181
set -eo pipefail
196182
197-
LIMA_INSTANCE="humr-k3s"
198-
KUBECONFIG="$HOME/.lima/$LIMA_INSTANCE/copied-from-guest/kubeconfig.yaml"
199-
200183
echo "Loading into k3s..."
201184
tar="/tmp/humr-example-agent.tar"
202185
docker save humr-example-agent:latest -o "$tar"
203-
limactl copy "$tar" "$LIMA_INSTANCE":"$tar"
204-
limactl shell "$LIMA_INSTANCE" sudo k3s ctr images import "$tar"
186+
187+
if [ -n "${IS_SANDBOX:-}" ]; then
188+
KUBECONFIG="/etc/rancher/k3s/k3s.yaml"
189+
sudo k3s ctr images import "$tar"
190+
else
191+
LIMA_INSTANCE="humr-k3s"
192+
KUBECONFIG="$HOME/.lima/$LIMA_INSTANCE/copied-from-guest/kubeconfig.yaml"
193+
limactl copy "$tar" "$LIMA_INSTANCE":"$tar"
194+
limactl shell "$LIMA_INSTANCE" sudo k3s ctr images import "$tar"
195+
fi
205196
rm -f "$tar"
206197
207198
echo "Restarting agent pods..."
@@ -215,24 +206,40 @@ description = "Run kubectl against the humr-k3s cluster. Usage: mise run cluster
215206
raw = true
216207
run = '''
217208
#!/usr/bin/env bash
218-
KUBECONFIG="$HOME/.lima/humr-k3s/copied-from-guest/kubeconfig.yaml"
209+
if [ -n "${IS_SANDBOX:-}" ]; then
210+
KUBECONFIG="/etc/rancher/k3s/k3s.yaml"
211+
else
212+
KUBECONFIG="$HOME/.lima/humr-k3s/copied-from-guest/kubeconfig.yaml"
213+
fi
219214
exec kubectl --kubeconfig="$KUBECONFIG" "$@"
220215
'''
221216

222217
["cluster:shell"]
223-
description = "Open a shell inside the k3s VM"
218+
description = "Open a shell inside the k3s VM (or run command directly in sandbox mode)"
224219
raw = true
225220
run = '''
226221
#!/usr/bin/env bash
227-
exec limactl shell humr-k3s "$@"
222+
if [ -n "${IS_SANDBOX:-}" ]; then
223+
if [ $# -eq 0 ]; then
224+
exec bash
225+
else
226+
exec "$@"
227+
fi
228+
else
229+
exec limactl shell humr-k3s "$@"
230+
fi
228231
'''
229232

230233
["cluster:stop"]
231234
description = "Stop the k3s VM (preserves data)"
232235
run = '''
233236
#!/usr/bin/env bash
234237
set -eo pipefail
235-
limactl stop humr-k3s
238+
if [ -n "${IS_SANDBOX:-}" ]; then
239+
sudo systemctl stop k3s
240+
else
241+
limactl stop humr-k3s
242+
fi
236243
'''
237244

238245
["cluster:uninstall"]
@@ -241,7 +248,11 @@ dir = "{{config_root}}"
241248
run = '''
242249
#!/usr/bin/env bash
243250
set -eo pipefail
244-
KUBECONFIG="$HOME/.lima/humr-k3s/copied-from-guest/kubeconfig.yaml"
251+
if [ -n "${IS_SANDBOX:-}" ]; then
252+
KUBECONFIG="/etc/rancher/k3s/k3s.yaml"
253+
else
254+
KUBECONFIG="$HOME/.lima/humr-k3s/copied-from-guest/kubeconfig.yaml"
255+
fi
245256
helm --kubeconfig="$KUBECONFIG" uninstall humr || true
246257
kubectl --kubeconfig="$KUBECONFIG" delete pvc --all 2>/dev/null || true
247258
kubectl --kubeconfig="$KUBECONFIG" delete ns humr-agents --ignore-not-found 2>/dev/null || true
@@ -265,15 +276,25 @@ for arg in "$@"; do
265276
done
266277
267278
if [ "$FORCE" != "true" ]; then
268-
echo "This will delete the k3s VM ($LIMA_INSTANCE) and all cluster data."
279+
if [ -n "${IS_SANDBOX:-}" ]; then
280+
echo "This will uninstall k3s and delete all cluster data."
281+
else
282+
echo "This will delete the k3s VM ($LIMA_INSTANCE) and all cluster data."
283+
fi
269284
read -p "Continue? [y/N] " -r
270285
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
271286
echo "Cancelled."
272287
exit 0
273288
fi
274289
fi
275-
limactl delete --force "$LIMA_INSTANCE" 2>/dev/null || true
276-
echo "k3s cluster ($LIMA_INSTANCE) deleted."
290+
291+
if [ -n "${IS_SANDBOX:-}" ]; then
292+
/usr/local/bin/k3s-uninstall.sh 2>/dev/null || true
293+
echo "k3s uninstalled from host."
294+
else
295+
limactl delete --force "$LIMA_INSTANCE" 2>/dev/null || true
296+
echo "k3s cluster ($LIMA_INSTANCE) deleted."
297+
fi
277298
'''
278299

279300
["cluster:status"]
@@ -282,11 +303,20 @@ raw = true
282303
run = '''
283304
#!/usr/bin/env bash
284305
set -eo pipefail
285-
KUBECONFIG="$HOME/.lima/humr-k3s/copied-from-guest/kubeconfig.yaml"
306+
if [ -n "${IS_SANDBOX:-}" ]; then
307+
KUBECONFIG="/etc/rancher/k3s/k3s.yaml"
308+
else
309+
KUBECONFIG="$HOME/.lima/humr-k3s/copied-from-guest/kubeconfig.yaml"
310+
fi
286311
287312
STATUS_CMD="
288-
echo '=== Lima VM ==='
289-
limactl list
313+
if [ -n \"${IS_SANDBOX:-}\" ]; then
314+
echo '=== k3s service ==='
315+
sudo systemctl is-active k3s || true
316+
else
317+
echo '=== Lima VM ==='
318+
limactl list
319+
fi
290320
echo ''
291321
echo '=== Nodes ==='
292322
kubectl --kubeconfig='$KUBECONFIG' get nodes 2>/dev/null || echo 'Cluster not reachable'
@@ -319,6 +349,10 @@ description = "Show OneCLI pod logs"
319349
run = '''
320350
#!/usr/bin/env bash
321351
set -eo pipefail
322-
KUBECONFIG="$HOME/.lima/humr-k3s/copied-from-guest/kubeconfig.yaml"
352+
if [ -n "${IS_SANDBOX:-}" ]; then
353+
KUBECONFIG="/etc/rancher/k3s/k3s.yaml"
354+
else
355+
KUBECONFIG="$HOME/.lima/humr-k3s/copied-from-guest/kubeconfig.yaml"
356+
fi
323357
kubectl --kubeconfig="$KUBECONFIG" logs -l app.kubernetes.io/component=onecli --all-containers --tail=50
324358
'''

mise.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,16 @@ kubeconform = "latest"
1313

1414
# misc
1515
fd = "latest"
16+
yq = "latest"
1617

1718
[settings]
1819
experimental = true
1920
task.disable_spec_from_run_scripts = true
2021
raw = true
2122

23+
[hooks]
24+
postinstall = ["{{ mise_bin }} setup"]
25+
2226
[task_config]
2327
includes = [
2428
"./tasks.toml",

0 commit comments

Comments
 (0)