-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathJustfile
More file actions
472 lines (395 loc) · 20.8 KB
/
Copy pathJustfile
File metadata and controls
472 lines (395 loc) · 20.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
# justfile — devserver-wide commands only.
# Pipeline-specific recipes live in pipelines/Justfile (run them as
# `just pipelines <recipe>`, e.g. `just pipelines up-dagster`).
set shell := ["bash", "-euo", "pipefail", "-c"]
COMPOSE := "docker compose"
TEMPLATE_DIR := "./config"
SECRETS_DIR := "./secrets"
# Pipeline-specific recipes (see pipelines/Justfile).
mod pipelines
# Render config/*.tmpl -> secrets/* using `op inject` (toolbox `render-secrets`).
# Supports both *.env.tmpl and *.json.tmpl files. Renders ALL templates
# (pipeline secrets included), so it lives here at the devserver level.
rs: render_secrets
render_secrets strict="false":
render-secrets {{TEMPLATE_DIR}} {{SECRETS_DIR}} --pattern '*.env.tmpl' --pattern '*.json.tmpl' {{ if strict == "true" { "--strict" } else { "" } }}
clean-secrets:
@rm -f {{SECRETS_DIR}}/*.env
@echo "Removed generated secrets"
# ── Generic compose wrappers (work on any service[s]) ────────────────
# Examples:
# just up openclaw agent-browser
# just stop speaches
# just logs openclaw
# just build openclaw
# just restart openclaw
# just upgrade actualbudget (pulls latest image + recreates)
up *args:
{{COMPOSE}} up -d {{args}}
# `down` removes containers + networks; use `stop` if you want to keep them.
down *args:
{{COMPOSE}} down {{args}}
stop *args:
{{COMPOSE}} stop {{args}}
restart *args:
{{COMPOSE}} restart {{args}}
logs *args:
{{COMPOSE}} logs -f {{args}}
build *args:
{{COMPOSE}} build {{args}}
# Pull remote agent transcripts into AgentsView and tag them by machine.
agentsview-sync *args:
uv run bin/agentsview-sync-sources.py {{args}}
# Pull latest image(s) and recreate. --no-deps keeps dependent services running.
# Use this for registry-backed services with `image:` only.
upgrade *services:
{{COMPOSE}} pull {{services}}
{{COMPOSE}} up -d --no-deps {{services}}
# Upgrade openclaw (locally-built; build context = ankitson/dockers git repo).
# Rebuilds with npm latest by default. Pass a version only for rollback:
# `just upgrade-openclaw 2026.5.25`.
upgrade-openclaw version="":
#!/usr/bin/env bash
set -euo pipefail
V="{{version}}"
if [ -z "$V" ]; then
echo "openclaw: building with npm latest"
{{COMPOSE}} build --pull --no-cache openclaw
else
echo "openclaw: building with openclaw@$V"
{{COMPOSE}} build --pull --no-cache --build-arg OPENCLAW_VERSION="$V" --build-arg OPENCLAW_CODEX_VERSION="$V" openclaw
fi
{{COMPOSE}} up -d --no-deps openclaw
echo
docker exec openclaw bash -lc 'openclaw --version' 2>/dev/null | grep -v "Agent mode" | head -1 || true
echo
echo "── openclaw doctor (pending migrations — NOT auto-applied) ──────────"
# This rebuild path only swaps the binary + re-applies the config patch; it does
# NOT run version migrations. A bump can move stores (e.g. cron jobs.json → SQLite)
# and silently strand data. Surface them here so you apply them deliberately.
sleep 5 # let the gateway finish starting before doctor inspects state
docker exec openclaw openclaw doctor 2>&1 | grep -v "Agent mode detected. Run" || true
echo
echo "⚠️ REVIEW the doctor output above. If it lists changes, apply them with:"
echo " just openclaw-doctor-fix"
echo " (rewrites config/auth/session state — read 'just openclaw-doctor' first)."
# Surface pending OpenClaw migrations / config issues (read-only, safe).
openclaw-doctor:
docker exec openclaw openclaw doctor
# Apply OpenClaw migrations (rewrites config/auth/session state; review openclaw-doctor first).
openclaw-doctor-fix:
# cron jobs.json->SQLite import only triggers if a legacy ~/.openclaw/cron/jobs.json exists.
docker exec openclaw openclaw doctor --fix
# ── OpenClaw web app deployment surface ────────────────────────────
openclaw-apps-up:
{{COMPOSE}} up -d --no-deps openclaw-app-runner
openclaw-apps-logs:
{{COMPOSE}} logs -f openclaw-app-runner
openclaw-apps-smoke slug="hello-openclaw":
curl -fsS -H "Host: {{slug}}.dev.ankitson.com" http://127.0.0.1:18880/
# ── Phoenix OTLP trace viewer ─────────────────────────────────────
PHOENIX_URL := "http://127.0.0.1:6006"
AUTOSWEEP_SPIKE := "/home/ankit/hroot/projects/autosweep-spike"
phoenix-up:
{{COMPOSE}} up -d phoenix
phoenix-logs:
{{COMPOSE}} logs -f phoenix
phoenix-smoke:
curl -fsS {{PHOENIX_URL}}/ >/dev/null
@echo "Phoenix UI OK at {{PHOENIX_URL}}"
phoenix-post-autosweep-latest:
cd {{AUTOSWEEP_SPIKE}}/pydantic && uv run export_otlp_traces.py --latest-postcap-batch --post-url {{PHOENIX_URL}}/v1/traces
# ── MinIO S3-compatible object store ───────────────────────────────
MINIO_SECRET_ENV := "./secrets/minio.env"
MINIO_ENDPOINT := env('MINIO_ENDPOINT', 'https://minio.dev.ankitson.com')
MINIO_CONSOLE := env('MINIO_CONSOLE', 'https://minio-console.dev.ankitson.com')
minio-up:
test -f {{MINIO_SECRET_ENV}} || (echo "Missing {{MINIO_SECRET_ENV}}; run: just rs" >&2; exit 1)
{{COMPOSE}} up -d minio minio-init
minio-logs:
{{COMPOSE}} logs -f minio
minio-console:
@echo {{MINIO_CONSOLE}}
minio-client-env:
#!/usr/bin/env bash
set -euo pipefail
test -f {{MINIO_SECRET_ENV}} || (echo "Missing {{MINIO_SECRET_ENV}}; run: just rs" >&2; exit 1)
set -a
. {{MINIO_SECRET_ENV}}
set +a
endpoint="${MINIO_ENDPOINT:-${MINIO_SERVER_URL:-{{MINIO_ENDPOINT}}}}"
printf 'AWS_ACCESS_KEY_ID=%s\nAWS_SECRET_ACCESS_KEY=%s\nAWS_EC2_METADATA_DISABLED=true\nAWS_ENDPOINT_URL_S3=%s\n' "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" "$endpoint"
minio-put file key="":
#!/usr/bin/env bash
set -euo pipefail
test -f {{MINIO_SECRET_ENV}} || (echo "Missing {{MINIO_SECRET_ENV}}; run: just rs" >&2; exit 1)
set -a
. {{MINIO_SECRET_ENV}}
set +a
src="{{file}}"
key="{{key}}"
if [ -z "$key" ]; then
key="$(basename "$src")"
fi
docker run --rm --network mybridge --entrypoint /bin/sh --env-file {{MINIO_SECRET_ENV}} \
-e MINIO_OBJECT_KEY="$key" \
-v "$(realpath "$src"):/upload:ro" \
quay.io/minio/mc:latest \
-c 'mc alias set dev http://minio:9000 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" >/dev/null && mc cp /upload "dev/$MINIO_BUCKET/$MINIO_OBJECT_KEY" >/dev/null'
url_key="$(python3 -c 'from urllib.parse import quote; import sys; print(quote(sys.argv[1]))' "$key")"
endpoint="${MINIO_ENDPOINT:-${MINIO_SERVER_URL:-{{MINIO_ENDPOINT}}}}"
printf '%s/%s/%s\n' "$endpoint" "$MINIO_BUCKET" "$url_key"
minio-smoke:
#!/usr/bin/env bash
set -euo pipefail
test -f {{MINIO_SECRET_ENV}} || (echo "Missing {{MINIO_SECRET_ENV}}; run: just rs" >&2; exit 1)
set -a
. {{MINIO_SECRET_ENV}}
set +a
{{COMPOSE}} up -d minio minio-init
tmp="$(mktemp)"
trap 'rm -f "$tmp"' EXIT
printf 'minio smoke %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$tmp"
key=".smoke/minio.txt"
docker run --rm --network mybridge --entrypoint /bin/sh --env-file {{MINIO_SECRET_ENV}} \
-e MINIO_OBJECT_KEY="$key" \
-v "$tmp:/upload:ro" \
quay.io/minio/mc:latest \
-c 'mc alias set dev http://minio:9000 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" >/dev/null && mc cp /upload "dev/$MINIO_BUCKET/$MINIO_OBJECT_KEY" >/dev/null'
endpoint="${MINIO_ENDPOINT:-${MINIO_SERVER_URL:-{{MINIO_ENDPOINT}}}}"
curl -fsS "$endpoint/$MINIO_BUCKET/.smoke/minio.txt"
# ── Unsloth Studio patched UI ───────────────────────────────────────
unsloth-studio-up:
{{COMPOSE}} up -d unsloth-studio
unsloth-studio-logs:
{{COMPOSE}} logs -f unsloth-studio
unsloth-studio-smoke:
curl -fsS http://127.0.0.1:${UNSLOTH_STUDIO_PORT:-8892}/api/health | python3 -m json.tool
# ── Speaches-specific (no generic compose equivalent) ────────────────
# Preload the default whisper model (downloads weights if not cached).
speaches-pull model="deepdml/faster-whisper-large-v3-turbo-ct2":
curl -fsS -X POST "http://127.0.0.1:8800/v1/models/{{model}}" && echo
curl -fsS "http://127.0.0.1:8800/v1/models/{{model}}" | python3 -m json.tool
# Smoke test: transcribe an audio file. Usage: just speaches-test path/to/audio.wav
speaches-test file:
curl -fsS -X POST http://127.0.0.1:8800/v1/audio/transcriptions \
-H "Content-Type: multipart/form-data" \
-F "file=@{{file}}" \
-F "model=deepdml/faster-whisper-large-v3-turbo-ct2" \
-F "response_format=json" | python3 -m json.tool
# ── Bifrost LLM gateway ─────────────────────────────────────────────
BIFROST_URL := "http://127.0.0.1:8090"
# List the providers Bifrost loaded from config/bifrost.config.json.
bifrost-providers:
curl -fsS {{BIFROST_URL}}/api/providers | python3 -c 'import sys,json; [print("-", p["name"], "("+p.get("provider_status","?")+")") for p in json.load(sys.stdin)["providers"]]'
# Smoke test: call a free OpenRouter model end-to-end through Bifrost.
# Usage: just bifrost-test [model] (model is the bifrost id, default a free one)
bifrost-test model="openrouter/openai/gpt-oss-20b:free":
curl -fsS --max-time 120 {{BIFROST_URL}}/openai/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{"model":"{{model}}","messages":[{"role":"user","content":"Reply with exactly: BIFROST_OK"}],"max_tokens":20}' \
| python3 -c 'import sys,json; d=json.load(sys.stdin); print(d["choices"][0]["message"]["content"].strip() if d.get("choices") else "ERR "+str(d.get("status_code"))+" "+json.dumps(d.get("error",{})))'
# Smoke test the extra_params passthrough: pin an OpenRouter model to a specific
# upstream provider through Bifrost (the `provider` object is merged upstream only
# when `x-bf-passthrough-extra-params: true` is set). Default pins deepseek-v4-flash
# to the ZDR `wafer` BYOK endpoint. Usage: just bifrost-test-pin [model] [provider]
bifrost-test-pin model="openrouter/deepseek/deepseek-v4-flash" provider="wafer":
curl -fsS --max-time 120 {{BIFROST_URL}}/openai/v1/chat/completions \
-H 'Content-Type: application/json' \
-H 'x-bf-passthrough-extra-params: true' \
-d '{"model":"{{model}}","messages":[{"role":"user","content":"Reply with exactly: PIN_OK"}],"max_tokens":16,"extra_params":{"provider":{"only":["{{provider}}"],"allow_fallbacks":false}}}' \
| python3 -c 'import sys,json; d=json.load(sys.stdin); print((d["choices"][0]["message"]["content"].strip()+" | resolved="+str(d.get("model"))) if d.get("choices") else "ERR "+str(d.get("status_code"))+" "+json.dumps(d.get("error",{})))'
# ── codex-oauth (ChatGPT/Codex subscription -> Bifrost `codex` provider) ─────
# One-time auth: log a DEDICATED Codex session into secrets/codex-oauth/auth.json
# (kept separate from the host's ~/.codex so refresh tokens never contend).
# --device-auth prints a code + URL you open on any device — no localhost callback,
# so it works on this headless box without SSH port-forwarding.
codex-oauth-login:
mkdir -p {{SECRETS_DIR}}/codex-oauth
CODEX_HOME="$(pwd)/{{SECRETS_DIR}}/codex-oauth" codex login --device-auth
@echo "Wrote {{SECRETS_DIR}}/codex-oauth/auth.json — now: just up --build codex-oauth"
# Build + (re)start the proxy, then confirm it can list account models (proves auth).
codex-oauth-up:
{{COMPOSE}} up -d --build codex-oauth
codex-oauth-logs:
{{COMPOSE}} logs -f codex-oauth
# Verify the proxy is authed: list the Codex models your subscription exposes.
codex-oauth-models:
curl -fsS --max-time 30 http://127.0.0.1:{{env('CODEX_OAUTH_PORT', '10531')}}/v1/models \
| python3 -c 'import sys,json; d=json.load(sys.stdin); print("\n".join("- "+m["id"] for m in d.get("data",[]))) or "no models (check auth)"'
# End-to-end smoke test: call the subscription through Bifrost's `codex` provider.
# Usage: just codex-oauth-test [model]
codex-oauth-test model="codex/gpt-5.5":
curl -fsS --max-time 120 {{BIFROST_URL}}/openai/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{"model":"{{model}}","messages":[{"role":"user","content":"Reply with exactly: CODEX_OK"}],"max_tokens":20}' \
| python3 -c 'import sys,json; d=json.load(sys.stdin); print(d["choices"][0]["message"]["content"].strip() if d.get("choices") else "ERR "+str(d.get("status_code"))+" "+json.dumps(d.get("error",{})))'
# Sync declarative OpenRouter presets (config/openrouter-presets.json) to the
# OpenRouter account. Presets bake provider routing server-side, so they survive
# Bifrost (which strips the request-body `provider` field) — this is how we pin
# deepseek-v4-flash onto the ZDR `wafer` BYOK endpoint through the gateway.
# Reference a preset as model `openrouter/@preset/<slug>`. No inference cost.
openrouter-presets-sync:
python3 tools/openrouter_presets.py
openrouter-presets-check:
python3 tools/openrouter_presets.py --check
# Register OpenRouter BYOK provider credentials (config/openrouter-byok.json) so
# models route to a provider's first-party endpoint on YOUR key/credits. Needs an
# OpenRouter management key (OPENROUTER_MGMT_KEY or management_key_op in the json),
# not a normal sk-or- key. Provider keys read from 1Password at sync time.
openrouter-byok-sync:
python3 tools/openrouter_byok.py
openrouter-byok-list:
python3 tools/openrouter_byok.py --list
# Copy a SillyTavern Chat Completion preset JSON into the local user volume.
sillytavern-preset-copy file:
mkdir -p "volumes/sillytavern/data/default-user/OpenAI Settings"
cp "{{file}}" "volumes/sillytavern/data/default-user/OpenAI Settings/"
# List the MCP tools Bifrost discovered from mcpproxy (exa search + websets).
bifrost-mcp-tools:
curl -fsS {{BIFROST_URL}}/api/mcp/clients | python3 -c 'import sys,json; [ (print("client",c["config"]["name"]+":"), [print(" -",t["name"]) for t in c.get("tools",[])]) for c in json.load(sys.stdin)["clients"]]'
# Smoke test: drive a model through Bifrost that web-searches via mcpproxy->exa
# (agent mode auto-executes the read-only exa search tools). Usage: just bifrost-test-search [model] [query...]
bifrost-test-search model="nvidia/meta/llama-3.1-8b-instruct" *query="What is the maximhq Bifrost LLM gateway? Answer in one sentence.":
curl -fsS --max-time 120 {{BIFROST_URL}}/openai/v1/chat/completions \
-H 'Content-Type: application/json' \
-H 'x-bf-mcp-include-clients: mcpproxy' \
-d '{"model":"{{model}}","messages":[{"role":"user","content":"Use the web_search tool, then: {{query}}"}],"max_tokens":400}' \
| python3 -c 'import sys,json; d=json.load(sys.stdin); print(d["choices"][0]["message"].get("content") if d.get("choices") else "ERR "+json.dumps(d.get("error",{})))'
# ── MCPProxy shared MCP gateway ─────────────────────────────────────
# Keep the Compose project name stable when running from an isolated worktree.
mcpproxy-up:
{{COMPOSE}} -p devserver up -d --build --no-deps mcpproxy
mcpproxy-logs:
{{COMPOSE}} -p devserver logs -f mcpproxy
mcpproxy-health:
curl -fsS http://172.19.0.1:3130/healthz
@echo
# ── DNS probe: host-networked Go resolver diagnostics ───────────────
dns-probe-up:
{{COMPOSE}} -p devserver up -d --build --no-deps dns-probe
dns-probe-stop:
{{COMPOSE}} -p devserver stop dns-probe
dns-probe-logs:
{{COMPOSE}} -p devserver logs -f dns-probe
dns-probe-tail:
tail -f logs/dns-probe.jsonl
dns-probe-host-tail:
tail -f logs/dns-probe-host.jsonl
dns-probe-host-logs since="now":
journalctl --follow --output=json --since "{{since}}" --no-pager \
-u systemd-resolved -u tailscaled -u docker -u NetworkManager \
| tee -a logs/dns-probe-host.jsonl
dns-probe-clean:
rm -f logs/dns-probe.jsonl logs/dns-probe-host.jsonl
# Enable this before a sleep/wake repro. It is intentionally separate because
# resolved debug logging is noisy and should be turned back down after capture.
dns-debug-on:
sudo resolvectl log-level debug
dns-debug-off:
sudo resolvectl log-level info
# Run this on the devserver host: Fastmail redirects the browser to a
# 127.0.0.1 callback listener owned by the host-networked container.
mcpproxy-auth-fastmail:
#!/usr/bin/env bash
set -euo pipefail
started="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
output="$(mktemp /tmp/mcpproxy-auth-fastmail.XXXXXX)"
trap 'rm -f "$output"' EXIT
docker exec mcpproxy mcpproxy \
--config /data/mcp_config.json \
--data-dir /data \
auth login --server=fastmail --timeout=5m >"$output" 2>&1
for _ in $(seq 1 30); do
auth_url="$(
docker logs --since "$started" mcpproxy 2>&1 \
| python3 -c 'import json, re, sys; matches = re.findall(r"\"auth_url\": \"([^\"]+)\"", sys.stdin.read()); print(json.loads(f"\"{matches[-1]}\"") if matches else "")'
)"
if [[ -n "$auth_url" ]]; then
break
fi
sleep 1
done
if [[ -z "${auth_url:-}" ]]; then
echo "Timed out waiting for MCPProxy to emit a Fastmail authorization URL." >&2
cat "$output" >&2
exit 1
fi
printf 'Open this URL in a browser running on this host (%s):\n\n%s\n\n' "$(hostname)" "$auth_url"
echo "Waiting for Fastmail to complete the loopback callback and connect..."
for _ in $(seq 1 60); do
if docker exec mcpproxy mcpproxy \
--config /data/mcp_config.json \
--data-dir /data \
upstream list --output json 2>/dev/null \
| python3 -c 'import json, sys; server = next((s for s in json.load(sys.stdin) if s.get("id") == "fastmail"), {}); sys.exit(0 if server.get("connected") and server.get("status") == "ready" else 1)'; then
docker exec mcpproxy mcpproxy \
--config /data/mcp_config.json \
--data-dir /data \
upstream approve fastmail >/dev/null
echo "Fastmail OAuth is complete, the upstream is connected, and tools are approved."
exit 0
fi
sleep 5
done
echo "Timed out waiting for Fastmail OAuth to complete." >&2
echo "Open the URL above on the same host before the callback listener expires, then rerun mcpproxy-auth-status." >&2
exit 1
mcpproxy-auth-status:
docker exec mcpproxy mcpproxy \
--config /data/mcp_config.json \
--data-dir /data \
auth status --server=fastmail
# Prints the raw token exactly once. Store it in clankers/mcpproxy-agents.
mcpproxy-token-create:
docker exec mcpproxy mcpproxy \
--config /data/mcp_config.json \
--data-dir /data \
token create \
--name shared-agents \
--servers "*" \
--permissions read,write,destructive \
--expires 365d \
--output json
# Usage:
# MCPPROXY_AGENT_TOKEN="$(op read 'op://clankers/mcpproxy-agents/password')" just mcpproxy-smoke
mcpproxy-smoke:
#!/usr/bin/env bash
set -euo pipefail
: "${MCPPROXY_AGENT_TOKEN:?set MCPPROXY_AGENT_TOKEN from 1Password}"
curl --fail-with-body --silent --show-error \
http://172.19.0.1:3130/mcp/all \
--header "Authorization: Bearer ${MCPPROXY_AGENT_TOKEN}" \
--header "Accept: application/json, text/event-stream" \
--header "Content-Type: application/json" \
--data '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"devserver-smoke","version":"1.0.0"}}}'
echo
# ── agent-sandbox: SSH workspace for the remote agent ───────────────────
# Authorize an additional public key for SSH into agent-sandbox.
# Usage: just agent-sandbox-add-key "ssh-ed25519 AAAA... agent@host"
agent-sandbox-add-key key:
docker exec agent-sandbox sh -c 'echo "{{key}}" >> /home/ankit/.ssh/authorized_keys && ssh-keygen -lf /home/ankit/.ssh/authorized_keys'
# SSH reachability + write-mount smoke test (uses your local dev key).
agent-sandbox-smoke:
ssh -o BatchMode=yes -p 2222 ankit@127.0.0.1 \
'echo ok: $(whoami)@$(hostname) && touch /projects/agent_out/.smoke && rm /projects/agent_out/.smoke && echo "agent_out writable"'
# ── gilfoyle: homeserver ops agent (cron-driven loops) ──────────────────
# Workspace: /cybernetics/agents/gilfoyle/. Surfaces to Discord #homeserver-ops.
# (Re)register gilfoyle's cron jobs in the running gateway. Idempotent by name.
gil-cron-setup:
bin/setup-gilfoyle-cron.sh
# List gilfoyle's scheduled jobs.
gil-cron-list:
docker exec openclaw openclaw cron list --agent gilfoyle
# Force-run a loop now by job name and wait for terminal status.
# Usage: just gil-loop gilfoyle-health-watch
gil-loop name:
#!/usr/bin/env bash
set -euo pipefail
id="$(docker exec openclaw openclaw cron list --agent gilfoyle --json \
| python3 -c 'import json,sys; d=json.load(sys.stdin); jobs=d.get("jobs",d) if isinstance(d,dict) else d; print(next((j["id"] for j in jobs if j.get("name")=="{{name}}"), ""))')"
if [ -z "$id" ]; then echo "No gilfoyle job named {{name}} (run: just gil-cron-setup)" >&2; exit 1; fi
docker exec openclaw openclaw cron run "$id" --wait --wait-timeout 10m --poll-interval 2s
# Follow gilfoyle's gateway logs.
gil-logs:
{{COMPOSE}} logs -f openclaw