feat(recipes): add remote recipes - #67
Conversation
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: trueThanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
Signed-off-by: NovusEdge <novusedge0@gmail.com>
recipes.RemoveChecked calls the users callback while holding the scope lock exclusively. core.List resolved every VM's manifests through recipes.ManifestFor, which takes the same flock on a second descriptor, so stoat recipe rm hung whenever any VM listed a recipe. Signed-off-by: NovusEdge <novusedge0@gmail.com>
Remote recipe live gate: PASSThe revised cloud checks passed: Earlier home/project add, list, and sync evidence is from 5b340ee. The binary for resumed step 7 onward is 631eeb9,
Fixture and scope evidenceSource: Home-scope tests used Optional index search failed with the envelope below, the allowed unavailable-index exception. Steps 5–6: exact commands and JSON at 5b340eeGlobal working directory: env STOAT_HOME=/home/novusedge/vms/stoat/.worktrees/.live-wave-b.ytR40g/cloud STOAT_GRAPHICAL=0 /home/novusedge/vms/stoat/.worktrees/.live-wave-b.ytR40g/cloud/stoat recipe add file:///tmp/stoat-live-recipes/tsprobe@v1 -y --jsonExit 0. {"v":3,"type":"result","cmd":"recipe","ok":true,"data":{"name":"tsprobe","source":"file:///tmp/stoat-live-recipes/tsprobe","ref":"v1","commit":"765c2a364b845273deb9089035e04d250964224a","scope":"global"}}env STOAT_HOME=/home/novusedge/vms/stoat/.worktrees/.live-wave-b.ytR40g/cloud STOAT_GRAPHICAL=0 /home/novusedge/vms/stoat/.worktrees/.live-wave-b.ytR40g/cloud/stoat recipe list --jsonExit 0. {"v":3,"type":"result","cmd":"recipe","ok":true,"data":{"roots":[{"path":"/home/novusedge/vms/stoat/.worktrees/.live-wave-b.ytR40g/cloud/recipes","scope":"global"},{"path":"/home/novusedge/vms/stoat/.worktrees/.live-wave-b.ytR40g/cloud/recipes","scope":"local"},{"path":"/home/novusedge/vms/stoat/.worktrees/.live-wave-b.ytR40g/cloud/recipes","scope":"bundled"}],"recipes":[{"name":"cloud-probe","description":"disposable cloud delivery verification","scope":"local","source":"","ref":"","commit":""},{"name":"devtools","description":"git, a compiler, an editor and basic fetch tools","scope":"bundled","source":"","ref":"","commit":""},{"name":"docker","description":"Docker engine and the compose plugin","scope":"bundled","source":"","ref":"","commit":""},{"name":"tailscale","description":"Tailscale daemon, installed and started (join manually)","scope":"bundled","source":"","ref":"","commit":""},{"name":"tsprobe","description":"Tailscale daemon, installed and started (join manually)","scope":"global","source":"file:///tmp/stoat-live-recipes/tsprobe","ref":"v1","commit":"765c2a3"},{"name":"xfce","description":"XFCE desktop with autologin startx on tty1","scope":"bundled","source":"","ref":"","commit":""}]}}env STOAT_HOME=/home/novusedge/vms/stoat/.worktrees/.live-wave-b.ytR40g/cloud STOAT_GRAPHICAL=0 GIT_TERMINAL_PROMPT=0 /home/novusedge/vms/stoat/.worktrees/.live-wave-b.ytR40g/cloud/stoat recipe search --jsonExit 1. {"v":3,"type":"result","cmd":"recipe","ok":false,"error":{"code":"internal","message":"git clone --quiet --depth 1 -- https://github.com/novusedge/stoat-recipes /home/novusedge/vms/stoat/.worktrees/.live-wave-b.ytR40g/cloud/.stoat-index-stage-2129083916: exit status 128: fatal: could not read Username for 'https://github.com': terminal prompts disabled"}}Project working directory: env STOAT_HOME=/home/novusedge/vms/stoat/.worktrees/.live-wave-b.ytR40g/cloud STOAT_GRAPHICAL=0 /home/novusedge/vms/stoat/.worktrees/.live-wave-b.ytR40g/cloud/stoat recipe add file:///tmp/stoat-live-recipes/tsprobe@v1 -y --jsonExit 0. {"v":3,"type":"result","cmd":"recipe","ok":true,"data":{"name":"tsprobe","source":"file:///tmp/stoat-live-recipes/tsprobe","ref":"v1","commit":"765c2a364b845273deb9089035e04d250964224a","scope":"project"}}test ! -e /home/novusedge/vms/stoat/.worktrees/.live-wave-b.ytR40g/remote-gate.bDwkFe/tsprobe/project-cache-before-sync && mv /home/novusedge/vms/stoat/.worktrees/.live-wave-b.ytR40g/remote-gate.bDwkFe/project.041WQP/.stoat /home/novusedge/vms/stoat/.worktrees/.live-wave-b.ytR40g/remote-gate.bDwkFe/tsprobe/project-cache-before-sync && test ! -e .stoatExit 0. No output. env STOAT_HOME=/home/novusedge/vms/stoat/.worktrees/.live-wave-b.ytR40g/cloud STOAT_GRAPHICAL=0 /home/novusedge/vms/stoat/.worktrees/.live-wave-b.ytR40g/cloud/stoat recipe sync --jsonExit 0. {"v":3,"type":"result","cmd":"recipe","ok":true,"data":{"recipes":[{"name":"tsprobe","source":"file:///tmp/stoat-live-recipes/tsprobe","ref":"v1","commit":"765c2a364b845273deb9089035e04d250964224a","scope":"project"}]}}Resumed step 7: Debian 13 cloud bootAll commands below ran from The previously blocked removal completed in 126 ms at the new tip. The v2 fixture was rejected because an optional authkey had no default; v3 removed that parameter table as directed and was accepted. The original v1 VM had been removed before this v3 VM was created. New-tip build, fixture replacement, create/up, and apply outputBuild working directory: env GOMAXPROCS=2 GOFLAGS=-p=2 nice -n 10 go build -o /home/novusedge/vms/stoat/.worktrees/.live-wave-b.ytR40g/cloud/stoat ./cmd/stoatExit 0. No unit gate was rerun. env STOAT_HOME=/home/novusedge/vms/stoat/.worktrees/.live-wave-b.ytR40g/cloud STOAT_GRAPHICAL=0 timeout --signal=TERM --kill-after=5s 30s /home/novusedge/vms/stoat/.worktrees/.live-wave-b.ytR40g/cloud/stoat recipe rm tsprobe -y --jsonExit 0. {"v":3,"type":"result","cmd":"recipe","ok":true,"data":{"name":"tsprobe","scope":"project"}}env STOAT_HOME=/home/novusedge/vms/stoat/.worktrees/.live-wave-b.ytR40g/cloud STOAT_GRAPHICAL=0 timeout --signal=TERM --kill-after=5s 30s /home/novusedge/vms/stoat/.worktrees/.live-wave-b.ytR40g/cloud/stoat recipe add file:///tmp/stoat-live-recipes/tsprobe@v2 -y --jsonExit 1. {"v":3,"type":"result","cmd":"recipe","ok":false,"error":{"code":"invalid_spec","message":"invalid recipe tree: tsprobe.authkey: needs a default or required = true"}}env STOAT_HOME=/home/novusedge/vms/stoat/.worktrees/.live-wave-b.ytR40g/cloud STOAT_GRAPHICAL=0 timeout --signal=TERM --kill-after=5s 30s /home/novusedge/vms/stoat/.worktrees/.live-wave-b.ytR40g/cloud/stoat recipe add file:///tmp/stoat-live-recipes/tsprobe@v3 -y --jsonExit 0. {"v":3,"type":"result","cmd":"recipe","ok":true,"data":{"name":"tsprobe","source":"file:///tmp/stoat-live-recipes/tsprobe","ref":"v3","commit":"18e98e8612bce4c752480c1eab077b76280cd14c","scope":"project"}}env STOAT_HOME=/home/novusedge/vms/stoat/.worktrees/.live-wave-b.ytR40g/cloud STOAT_GRAPHICAL=0 /home/novusedge/vms/stoat/.worktrees/.live-wave-b.ytR40g/cloud/stoat create remote-tsprobe-v3-041wqp --image debian-13 --ram 2048 --cpus 2 --recipes tsprobe --jsonExit 0. {"v":3,"type":"result","cmd":"create","ok":true,"data":{"vm":{"name":"remote-tsprobe-v3-041wqp","os":"debian","mode":"cloud","backend":"cloudinit","state":"stopped","cpus":2,"ram_mb":2048,"disk":"8G","share":"","recipes":["tsprobe"],"ssh_port":2203,"ssh_user":"stoat","installed":false,"forwards":[],"allow_exec":true,"display":"vnc"}}}umask 077 && env STOAT_HOME=/home/novusedge/vms/stoat/.worktrees/.live-wave-b.ytR40g/cloud STOAT_GRAPHICAL=0 GOMAXPROCS=2 nice -n 10 timeout --signal=TERM --kill-after=30s 900s /home/novusedge/vms/stoat/.worktrees/.live-wave-b.ytR40g/cloud/stoat up remote-tsprobe-v3-041wqp --json > /home/novusedge/vms/stoat/.worktrees/.live-wave-b.ytR40g/remote-gate.bDwkFe/tsprobe/v3-631eeb9/up.txt 2>&1Exit 0. {"v":3,"type":"result","cmd":"up","ok":true,"data":{"vm":{"name":"remote-tsprobe-v3-041wqp","os":"debian","mode":"cloud","backend":"cloudinit","state":"running","cpus":2,"ram_mb":2048,"disk":"8G","share":"","recipes":["tsprobe"],"ssh_port":2203,"ssh_user":"stoat","installed":false,"forwards":[],"allow_exec":true,"display":"vnc"}}}One explicit plain umask 077 && env STOAT_HOME=/home/novusedge/vms/stoat/.worktrees/.live-wave-b.ytR40g/cloud STOAT_GRAPHICAL=0 GOMAXPROCS=2 nice -n 10 timeout --signal=TERM --kill-after=15s 240s /home/novusedge/vms/stoat/.worktrees/.live-wave-b.ytR40g/cloud/stoat apply remote-tsprobe-v3-041wqp > /home/novusedge/vms/stoat/.worktrees/.live-wave-b.ytR40g/remote-gate.bDwkFe/tsprobe/v3-631eeb9/apply-output.txt 2>&1Exit 0. The earlier Revised cloud probes: get, healthy wait, and consoleSaved first-boot console command (exit 0; 200 lines captured). Installation/completion excerpts from that capture: env STOAT_HOME=/home/novusedge/vms/stoat/.worktrees/.live-wave-b.ytR40g/cloud STOAT_GRAPHICAL=0 /home/novusedge/vms/stoat/.worktrees/.live-wave-b.ytR40g/cloud/stoat logs remote-tsprobe-v3-041wqp --which console -n 200Restarted the preserved VM without reapplying: env STOAT_HOME=/home/novusedge/vms/stoat/.worktrees/.live-wave-b.ytR40g/cloud STOAT_GRAPHICAL=0 GOMAXPROCS=2 nice -n 10 timeout --signal=TERM --kill-after=10s 120s /home/novusedge/vms/stoat/.worktrees/.live-wave-b.ytR40g/cloud/stoat up remote-tsprobe-v3-041wqp --no-applyExit 0. Applied-state result: env STOAT_HOME=/home/novusedge/vms/stoat/.worktrees/.live-wave-b.ytR40g/cloud STOAT_GRAPHICAL=0 /home/novusedge/vms/stoat/.worktrees/.live-wave-b.ytR40g/cloud/stoat get remote-tsprobe-v3-041wqp --jsonExit 0. {"v":3,"type":"result","cmd":"get","ok":true,"data":{"vm":{"name":"remote-tsprobe-v3-041wqp","os":"debian","mode":"cloud","backend":"cloudinit","state":"running","cpus":2,"ram_mb":2048,"disk":"8G","share":"","recipes":["tsprobe"],"ssh_port":2203,"ssh_user":"stoat","installed":false,"forwards":[],"allow_exec":true,"display":"vnc","health":"unknown","recipes_detail":[{"name":"tsprobe","applied":true,"version":"","at":"2026-09-05T10:09:40Z","health":"unknown","params":{},"outputs":{}}]}}}Live health result: env STOAT_HOME=/home/novusedge/vms/stoat/.worktrees/.live-wave-b.ytR40g/cloud STOAT_GRAPHICAL=0 /home/novusedge/vms/stoat/.worktrees/.live-wave-b.ytR40g/cloud/stoat wait remote-tsprobe-v3-041wqp --healthy --timeout 90s --jsonExit 0. {"v":3,"type":"result","cmd":"wait","ok":true,"data":{"reached":true,"until":"healthy","vm":"remote-tsprobe-v3-041wqp","waited_ms":677}}
Host apply-log tail command: env STOAT_HOME=/home/novusedge/vms/stoat/.worktrees/.live-wave-b.ytR40g/cloud STOAT_GRAPHICAL=0 /home/novusedge/vms/stoat/.worktrees/.live-wave-b.ytR40g/cloud/stoat logs remote-tsprobe-v3-041wqp --which apply -n 30Exit 0. No output. Superseded host-log probe, retained here for the audit trail (not rerun): env STOAT_HOME=/home/novusedge/vms/stoat/.worktrees/.live-wave-b.ytR40g/cloud STOAT_GRAPHICAL=0 /home/novusedge/vms/stoat/.worktrees/.live-wave-b.ytR40g/cloud/stoat wait remote-tsprobe-v3-041wqp --until applied --timeout 8m --jsonExit 1. {"v":3,"type":"result","cmd":"wait","ok":false,"error":{"code":"timeout","message":"context deadline exceeded"}}Step 8: in-use refusal and cleanupThe refusal's exit 1 is expected. All cleanup commands exited 0. The VM directory and project recipe cache are now absent, and the project declaration/lock no longer contain Exact removal and cleanup commands/resultsenv STOAT_HOME=/home/novusedge/vms/stoat/.worktrees/.live-wave-b.ytR40g/cloud STOAT_GRAPHICAL=0 timeout --signal=TERM --kill-after=5s 30s /home/novusedge/vms/stoat/.worktrees/.live-wave-b.ytR40g/cloud/stoat recipe rm tsprobe --jsonExit 1. {"v":3,"type":"result","cmd":"recipe","ok":false,"error":{"code":"in_use","message":"in use: tsprobe is used by remote-tsprobe-v3-041wqp; pass --force to remove it anyway"}}env STOAT_HOME=/home/novusedge/vms/stoat/.worktrees/.live-wave-b.ytR40g/cloud STOAT_GRAPHICAL=0 timeout --signal=TERM --kill-after=10s 90s /home/novusedge/vms/stoat/.worktrees/.live-wave-b.ytR40g/cloud/stoat down remote-tsprobe-v3-041wqpExit 0. env STOAT_HOME=/home/novusedge/vms/stoat/.worktrees/.live-wave-b.ytR40g/cloud STOAT_GRAPHICAL=0 timeout --signal=TERM --kill-after=5s 30s /home/novusedge/vms/stoat/.worktrees/.live-wave-b.ytR40g/cloud/stoat rm remote-tsprobe-v3-041wqp -yExit 0. env STOAT_HOME=/home/novusedge/vms/stoat/.worktrees/.live-wave-b.ytR40g/cloud STOAT_GRAPHICAL=0 timeout --signal=TERM --kill-after=5s 30s /home/novusedge/vms/stoat/.worktrees/.live-wave-b.ytR40g/cloud/stoat recipe rm tsprobe -y --jsonExit 0. {"v":3,"type":"result","cmd":"recipe","ok":true,"data":{"name":"tsprobe","scope":"project"}}Local artifacts: |
What changed
Recipes can come from git.
stoat recipe add <name|url>[@ref]clones one recipe repository into the recipe cache and pins its commit instoat.lock;recipe lock,recipe sync,recipe update,recipe rmandrecipe searchmanage the lock, the cache and the curated index. Two scopes: home (~/.stoat) and project (the directory holdingstoat.toml).recipe listreports every manifest in shadow order with its scope, source, ref and commit, and the JSON contract goes to 3 for that shape change.stoat doctorreports git as an optional dependency. The MCP server gainssearch_recipes,add_recipe,update_recipeandremove_recipe. New packageinternal/gitxwraps the git subprocess.Why
Spec
docs/specs/2026-09-04-remote-recipes-design.md. A recipe existed only when bundled or copied by hand into~/.stoat/recipes/. A lockfile at two scopes lets a repository pin the recipes its VMs need, which the project file spec builds on.Tests run
go build ./... && go vet ./... && go test ./... && golangci-lint run ./...clean at the branch tip, after the rebase onto feat(recipes): add recipe contract v3 #65.recipe addby file URL at home and project scope, lock and cache sync, an apply from the project cache, healthy wait,in_userefusal, cleanup. The gate found one deadlock, fixed in 631eeb9:recipe rmtook its own scope lock twice throughcore.List.Two pre-existing behaviours observed, logged as follow-ups and out of scope here:
up --jsonon a cloud VM returns before the boot-time apply, andwait --appliedreads a host apply log the cloud path never writes.Rebase notes
Rebased from the shared Wave B foundation onto main after #65. Four conflicts resolved by hand: the
recipesubcommand union ininternal/cli/grammar.go, the recipe rows and contract line indocs/reference/json.md,go.mod, andinternal/core/apply.go, where the hash helpers now resolve a script through the manifest's own directory (m.ScriptHash) so a project recipe that shadows a global one hashes the right file.