From 4c5f2b1bdc412fbb8c069d1d2420459ffd4700db Mon Sep 17 00:00:00 2001 From: suzuke Date: Mon, 6 Jul 2026 17:10:57 +0800 Subject: [PATCH 1/2] =?UTF-8?q?demo:=20productize=20Layer=202=20=E2=80=94?= =?UTF-8?q?=20two=20real=20agents=20drive=20the=20multi-agent=20scenario?= =?UTF-8?q?=20(live.sh)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract build_world + synthesize into demo/multi-agent/lib.sh as the SINGLE source of truth; verify.sh (Layer 1, in-process spawn) and the new live.sh (Layer 2, two real agents each drive one guarded session) both call it, so the synthesis can't drift between the two drivers. live.sh setup builds a PERSISTENT shared world and drops a self-contained agent-launch.sh; live.sh synth re-derives the verdict from state the supervisor owns. New guards from the adversarial design review: - freshness: a run-unique baseline + synth requires BOTH agent branches to descend from THIS run's baseline; setup refuses a non-empty world without --reset and holds an atomic setup lock — a prior run's stale tips can never masquerade as a fresh pass. - hostile-env scrub: unset the GIT_DIR/GIT_CONFIG_* family + every agentic/agend bypass knob before resolving git, in lib.sh AND inlined in the launcher, so a foreign session can't aim git at another repo despite -C. - resolve_bin defaults to the guarded repo build (prints resolved BIN+version); AGENTIC_GIT_BIN is an advanced override. Fixes verify.sh preferring a possibly unguarded installed release. Verified: shellcheck clean (all 4); verify.sh still VERIFIED; live.sh setup + 2 concurrent launchers + synth VERIFIED; adversarial matrix — canonical move / real clobber+forged PASS / stale-baseline all FAIL, HOME-forge cannot redirect the worktree source, synth-before-agents FAILs, setup refuses a non-empty world. Also driven end-to-end by two real fleet LLM agents (claude + codex) → VERIFIED. Co-Authored-By: Claude Opus 4.8 (1M context) --- demo/multi-agent/README.md | 34 +++++- demo/multi-agent/agent-launch.sh | 53 +++++++++ demo/multi-agent/lib.sh | 188 +++++++++++++++++++++++++++++++ demo/multi-agent/live.sh | 77 +++++++++++++ demo/multi-agent/verify.sh | 146 ++++-------------------- 5 files changed, 368 insertions(+), 130 deletions(-) create mode 100755 demo/multi-agent/agent-launch.sh create mode 100644 demo/multi-agent/lib.sh create mode 100755 demo/multi-agent/live.sh diff --git a/demo/multi-agent/README.md b/demo/multi-agent/README.md index 7bc17ad..9f5734a 100644 --- a/demo/multi-agent/README.md +++ b/demo/multi-agent/README.md @@ -76,9 +76,33 @@ want kernel isolation (containers, `sandbox-exec`, Landlock) *underneath* the shim. That's why the supervisor keys its checks off repos it owns, not off the agent's home. -## Two real agent sessions +## Two real agents drive it themselves (Layer 2 — `live.sh`) -The scenario runs two real agent *sessions*. Driving them with two real *LLM* -fleet agents (each executing `agent-run.sh` itself in its own shell) uses the -same evidence bundle + the same independent synthesis — the supervisor is still -the verifier. +`verify.sh` spawns the two guarded sessions itself. `live.sh` runs the **same +scenario** but lets **two real agents** (or just two shells) each drive one +guarded session — the same evidence bundle and the **same synthesis** (both live +in [`lib.sh`](lib.sh), so the rigor can't drift between the two drivers). Real +LLM agents add *authenticity*, not verification rigor: the rigor is the +supervisor's re-derivation, which is identical either way. + +```sh +./demo/multi-agent/live.sh setup --world /tmp/l2 # build a persistent world +# → prints: sh /tmp/l2/agent-launch.sh a +# sh /tmp/l2/agent-launch.sh b +# hand ONE command to each of two agents/shells; run them CONCURRENTLY +./demo/multi-agent/live.sh synth /tmp/l2 # VERIFIED / FAILED, from state you own +``` + +`agent-launch.sh` is self-contained: because it runs inside whatever session the +agent has, it scrubs its own environment (every `GIT_DIR`/`GIT_CONFIG_*` that +could redirect git despite `-C`, and every agentic-git/agend bypass knob), +resolves a real non-shim git itself, and reads only the supervisor-written +`world.env`. It then runs the same `agent-run.sh` and prints a `LAYER2-RESULT` +line — which is only *material* for the consistency cross-check, never the +verdict. + +Because the world is **persistent** (not a throwaway temp dir), `live.sh setup` +refuses a non-empty world unless you pass `--reset`, records a run-unique +baseline, and `synth` requires both agent branches to **descend from this run's +baseline** — so a prior run's leftover state can never masquerade as a fresh +pass. The verdict still comes only from repos the supervisor owns. diff --git a/demo/multi-agent/agent-launch.sh b/demo/multi-agent/agent-launch.sh new file mode 100755 index 0000000..2af3689 --- /dev/null +++ b/demo/multi-agent/agent-launch.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env sh +# +# agent-launch.sh — what ONE real agent runs to execute its guarded role against +# a live.sh world (Layer 2). Usage: sh agent-launch.sh +# +# Self-contained on purpose: it runs inside whatever (possibly hostile) session +# the agent has, so it scrubs its OWN environment, resolves a real non-shim git, +# and trusts NOTHING from the ambient env — only the supervisor-written +# world.env beside it. It then hands off to the same agent-run.sh verify.sh uses. +# +# shellcheck disable=SC2154 # arts/project/home/canonical/BIN come from world.env +set -u +role="${1:?usage: agent-launch.sh }" +case "$role" in a) other=b ;; b) other=a ;; *) echo "role must be 'a' or 'b'" >&2; exit 2 ;; esac +WORLD="$(cd "$(dirname "$0")" && pwd)" +[ -f "$WORLD/world.env" ] || { echo "LAUNCH-FAIL: no world.env in $WORLD (run 'live.sh setup' first)" >&2; exit 2; } +# shellcheck disable=SC1091 +. "$WORLD/world.env" +[ "${STATE:-}" = ready ] || { echo "LAUNCH-FAIL: world not ready (STATE=${STATE:-})" >&2; exit 2; } + +# ── scrub the hostile ambient environment (mirrors lib.sh scrub_hostile_env) ── +# Strip everything that could redirect/reconfigure git despite `-C`, plus every +# agentic-git/agend bypass/agent/home/allow knob. HOME/REAL_GIT/BIN are set by us +# at the run site below, so a blanket unset here is safe. +for v in $(env 2>/dev/null | sed -n 's/^\(AGENTIC_GIT_[A-Za-z0-9_]*\)=.*/\1/p; s/^\(AGEND_GIT_[A-Za-z0-9_]*\)=.*/\1/p'); do unset "$v" 2>/dev/null || true; done +unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_OBJECT_DIRECTORY GIT_ALTERNATE_OBJECT_DIRECTORIES \ + GIT_COMMON_DIR GIT_NAMESPACE GIT_CEILING_DIRECTORIES GIT_PREFIX \ + GIT_CONFIG GIT_CONFIG_GLOBAL GIT_CONFIG_SYSTEM GIT_CONFIG_NOSYSTEM \ + GIT_CONFIG_COUNT GIT_CONFIG_PARAMETERS 2>/dev/null || true + +resolve_real_git() { d=; IFS=:; for d in $PATH; do case "$d" in *.agentic-git*|*.agend-terminal*|*/.cargo/bin*) continue ;; esac; [ -x "$d/git" ] && { printf '%s\n' "$d/git"; return 0; }; done; return 1; } +REAL_GIT="$(resolve_real_git)" || { echo "LAUNCH-FAIL: no real (non-shim) git on PATH" >&2; exit 3; } +sanitized_path() { d=; IFS=:; out="$(dirname "$REAL_GIT")"; for d in $PATH; do case "$d" in *.agentic-git*|*.agend-terminal*) continue ;; esac; out="$out:$d"; done; printf '%s\n' "$out"; } +[ -x "$BIN" ] || { echo "LAUNCH-FAIL: guarded binary missing: $BIN" >&2; exit 3; } + +art="$arts/$role"; mkdir -p "$art" +( cd "$project" && \ + AGENTIC_GIT_HOME="$home" AGENTIC_GIT_REAL_GIT="$REAL_GIT" AGENTIC_GIT_BIN="$BIN" \ + PATH="$(sanitized_path)" \ + "$BIN" run --agent "agent-$role" --branch "feat/$role" -- \ + sh "$WORLD/agent-run.sh" "agent-$role" "feat/$role" "feat/$other" "$art" "$canonical" \ +) > "$art/session.log" 2>&1 +rc=$? + +gitbin="$(sed -n 's/^GITBIN=//p' "$art/evidence.env" 2>/dev/null)" +verdict="$(cat "$art/verdict.txt" 2>/dev/null)" +echo "LAYER2-RESULT role=agent-$role exit=$rc gitbin=$gitbin verdict=$verdict art=$art" +case "$gitbin" in + */bin/git) echo " proof: inner git was the shim ($gitbin)" ;; + *) echo " WARN: inner git was NOT a shim ($gitbin)" ;; +esac +[ "$rc" = 0 ] || echo " (session failed — see $art/session.log)" +exit "$rc" diff --git a/demo/multi-agent/lib.sh b/demo/multi-agent/lib.sh new file mode 100644 index 0000000..6b82a95 --- /dev/null +++ b/demo/multi-agent/lib.sh @@ -0,0 +1,188 @@ +#!/usr/bin/env bash +# +# lib.sh — the SINGLE source of truth for the multi-agent scenario: hostile-env +# scrubbing, real-git resolution, the guarded binary, world setup, and the +# SYNTHESIS invariant block. Sourced by verify.sh (Layer 1: the supervisor +# spawns the two guarded sessions in-process) and by live.sh (Layer 2: two real +# agents each drive one guarded session). Defining synthesize() ONCE means a +# real violation can never pass under one driver but fail under the other. +# +# Not executable on its own — `source` it from a bash driver. + +# ── hostile-environment scrub ──────────────────────────────────────────────── +# Both drivers run real git inside a possibly-foreign session (Layer 2 runs +# inside whatever shell the agent has). Strip every var that could redirect or +# reconfigure git despite `-C`, plus every agentic-git/agend bypass/agent/home/ +# allow knob. The three vars we actually want (HOME/REAL_GIT/BIN) are set +# explicitly at each guarded-run site, so a blanket unset here is safe. +scrub_hostile_env() { + local v + for v in $(env 2>/dev/null | sed -n 's/^\(AGENTIC_GIT_[A-Za-z0-9_]*\)=.*/\1/p; s/^\(AGEND_GIT_[A-Za-z0-9_]*\)=.*/\1/p'); do unset "$v" 2>/dev/null || true; done + unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_OBJECT_DIRECTORY GIT_ALTERNATE_OBJECT_DIRECTORIES \ + GIT_COMMON_DIR GIT_NAMESPACE GIT_CEILING_DIRECTORIES GIT_PREFIX \ + GIT_CONFIG GIT_CONFIG_GLOBAL GIT_CONFIG_SYSTEM GIT_CONFIG_NOSYSTEM \ + GIT_CONFIG_COUNT GIT_CONFIG_PARAMETERS 2>/dev/null || true +} + +# ── colours / ui ───────────────────────────────────────────────────────────── +c() { printf '\033[%sm' "$1"; } +say() { printf '\n%s▸ %s%s\n' "$(c '1;36')" "$*" "$(c 0)"; } +pass() { printf ' %s✓ %s%s\n' "$(c '1;32')" "$*" "$(c 0)"; } +bad() { printf ' %s✗ %s%s\n' "$(c '1;31')" "$*" "$(c 0)"; FAILS=$((FAILS+1)); } +die() { printf '\n%s✗ %s%s\n' "$(c '1;31')" "$*" "$(c 0)" >&2; exit 1; } + +# ── a real (non-shim) git, and a PATH with shim dirs stripped ──────────────── +resolve_real_git() { local d IFS=:; for d in $PATH; do case "$d" in *.agentic-git*|*.agend-terminal*|*/.cargo/bin*) continue ;; esac; [ -x "$d/git" ] && { printf '%s\n' "$d/git"; return 0; }; done; return 1; } +sanitized_path() { local d IFS=: out; out="$(dirname "$REAL_GIT")"; for d in $PATH; do case "$d" in *.agentic-git*|*.agend-terminal*) continue ;; esac; out="$out:$d"; done; printf '%s\n' "$out"; } + +LIBDIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# ── the guarded binary ─────────────────────────────────────────────────────── +# Default to the repo's release build: it carries the cross-branch push guard +# the deny-steps rely on, which a stale `cargo install`ed release may predate. +# AGENTIC_GIT_BIN is an ADVANCED override (e.g. to test a specific install). +resolve_bin() { + local bin root + bin="${AGENTIC_GIT_BIN:-}" + if [ -n "$bin" ] && [ -x "$bin" ]; then printf '%s\n' "$bin"; return 0; fi + root="$(cd "$LIBDIR/../.." 2>/dev/null && pwd || true)" + if [ -n "$root" ] && [ -f "$root/Cargo.toml" ]; then + bin="$root/target/release/agentic-git" + [ -x "$bin" ] || ( cd "$root" && cargo build --release -p agentic-git >/dev/null ) || return 1 + printf '%s\n' "$bin"; return 0 + fi + bin="$(command -v agentic-git || true)" + if [ -n "$bin" ] && [ -x "$bin" ]; then printf '%s\n' "$bin"; return 0; fi + return 1 +} + +# ── build the shared world ─────────────────────────────────────────────────── +# Uses the REAL_GIT and BIN globals set by the caller. Writes a run-unique +# baseline (so a persistent world's freshness is provable) and an atomically +# published world.env carrying STATE=ready. build_world +build_world() { + local world="$1" project canonical bare home arts rid pbase cbase + project="$world/project"; canonical="$world/your-checkout"; bare="$world/origin.git"; home="$world/home"; arts="$world/artifacts" + mkdir -p "$project" "$canonical" "$arts/a" "$arts/b" + rid="run-$(date +%Y%m%d%H%M%S)-$$" + export GIT_AUTHOR_NAME=you GIT_AUTHOR_EMAIL=you@example.com GIT_COMMITTER_NAME=you GIT_COMMITTER_EMAIL=you@example.com + "$REAL_GIT" init -q --bare "$bare" + "$REAL_GIT" -C "$project" init -q -b main + "$REAL_GIT" -C "$project" config user.name you; "$REAL_GIT" -C "$project" config user.email you@example.com + "$REAL_GIT" -C "$project" remote add origin "$bare" + printf 'shared project\nrun: %s\n' "$rid" > "$project/README.md" + "$REAL_GIT" -C "$project" add -A; "$REAL_GIT" -C "$project" commit -qm "project baseline ($rid)" + "$REAL_GIT" -C "$project" push -q origin main + pbase="$("$REAL_GIT" -C "$project" rev-parse HEAD)" + "$REAL_GIT" -C "$canonical" init -q -b main + "$REAL_GIT" -C "$canonical" config user.name you; "$REAL_GIT" -C "$canonical" config user.email you@example.com + "$REAL_GIT" -C "$canonical" remote add origin https://example.invalid/your-project.git + printf 'your real work\n' > "$canonical/app.py"; "$REAL_GIT" -C "$canonical" add -A; "$REAL_GIT" -C "$canonical" commit -qm base + "$REAL_GIT" -C "$canonical" commit -q --allow-empty -m more + cbase="$("$REAL_GIT" -C "$canonical" rev-parse HEAD)" + { printf 'RUN_ID=%s\n' "$rid" + printf 'project=%s\n' "$project"; printf 'canonical=%s\n' "$canonical" + printf 'bare=%s\n' "$bare"; printf 'home=%s\n' "$home"; printf 'arts=%s\n' "$arts" + printf 'BIN=%s\n' "$BIN" + printf 'PROJECT_BASE=%s\n' "$pbase"; printf 'CANON_BASE=%s\n' "$cbase" + printf 'STATE=ready\n' + } > "$world/world.env.tmp" + mv "$world/world.env.tmp" "$world/world.env" +} + +# ── SYNTHESIS: re-derive the truth from STATE, never from an agent's word ───── +# Reads only the world the supervisor owns. Returns 0 (VERIFIED) or 1 (FAILED), +# 2 on a malformed/not-ready world. synthesize +synthesize() { + # RUN_ID/PROJECT_BASE/CANON_BASE/project/bare/home/arts/canonical all come + # from the sourced world.env below — shellcheck can't see that dynamic source. + # shellcheck disable=SC2153,SC2154 + local world="$1" REAL_GIT wa wb ta tb ks elog + [ -f "$world/world.env" ] || { printf 'no world.env in %s\n' "$world" >&2; return 2; } + # shellcheck disable=SC1091 + . "$world/world.env" + [ "${STATE:-}" = ready ] || { printf 'world not ready (STATE=%s)\n' "${STATE:-}" >&2; return 2; } + REAL_GIT="$(resolve_real_git)" || { printf 'no real (non-shim) git on PATH\n' >&2; return 2; } + + say "Synthesis — re-deriving the truth from git/home/audit state:" + FAILS=0 + wt_for_branch() { "$REAL_GIT" -C "$project" worktree list --porcelain 2>/dev/null | awk -v b="refs/heads/$1" '/^worktree /{w=$2} $0==("branch " b){print w; exit}'; } + rbr() { "$REAL_GIT" -C "$1" symbolic-ref --short HEAD 2>/dev/null; } + wa="$(wt_for_branch feat/a)"; wb="$(wt_for_branch feat/b)" + + # I0 FRESHNESS — this run's baseline is present, and BOTH origin branches + # descend from it. A persistent/reused world cannot pass on a PRIOR run's + # stale tips: the baseline commit is run-unique (embeds RUN_ID), so a stale + # feat/* descends from a different baseline and fails the ancestry test. + if "$REAL_GIT" -C "$project" log -1 --format=%B "$PROJECT_BASE" 2>/dev/null | grep -qF "$RUN_ID" \ + && "$REAL_GIT" -C "$bare" merge-base --is-ancestor "$PROJECT_BASE" feat/a 2>/dev/null \ + && "$REAL_GIT" -C "$bare" merge-base --is-ancestor "$PROJECT_BASE" feat/b 2>/dev/null; then + pass "both agent branches descend from THIS run's baseline ($RUN_ID) — not stale state" + else bad "freshness failed: an agent branch is missing or does not descend from this run's baseline ($RUN_ID)"; fi + + # I1 two distinct worktrees LINKED TO THE SHARED PROJECT (from its own worktree + # list), each STILL on its own bound branch (real git — catches a cross-branch + # checkout that drifted HEAD) + if [ -n "$wa" ] && [ -n "$wb" ] && [ "$wa" != "$wb" ]; then + pass "the shared project has two distinct agent worktrees (from its own worktree list)" + else bad "the shared project lacks two distinct agent worktrees (a=$wa b=$wb)"; fi + if [ "$(rbr "$wa")" = feat/a ] && [ "$(rbr "$wb")" = feat/b ]; then + pass "each worktree HEAD is on its own bound branch (no cross-branch drift)" + else bad "a worktree drifted off its branch (a=$(rbr "$wa") b=$(rbr "$wb"))"; fi + + # I2 origin branches distinct + each trailered to its OWN agent — this alone + # catches a cross-agent force-push clobber or delete (no agent word trusted) + ta="$("$REAL_GIT" -C "$bare" rev-parse feat/a 2>/dev/null)"; tb="$("$REAL_GIT" -C "$bare" rev-parse feat/b 2>/dev/null)" + if [ -n "$ta" ] && [ -n "$tb" ] && [ "$ta" != "$tb" ]; then + pass "both branches on the shared origin, distinct tips (neither deleted/collapsed)" + else bad "an origin branch was clobbered or deleted (a=$ta b=$tb)"; fi + if "$REAL_GIT" -C "$bare" log -1 --format=%B feat/a 2>/dev/null | grep -q "Agentic-Agent: agent-a" \ + && "$REAL_GIT" -C "$bare" log -1 --format=%B feat/b 2>/dev/null | grep -q "Agentic-Agent: agent-b"; then + pass "each origin branch's tip is trailered to its OWN agent (not clobbered)" + else bad "origin provenance is wrong or one branch was clobbered by the other agent"; fi + + # I4 the shared source repo AND your stand-in real checkout were NOT moved. The + # canonical check independently catches an agent that touched your checkout, + # regardless of what the agent reported (closes the forged-artifact hole). + if [ "$("$REAL_GIT" -C "$project" rev-parse HEAD)" = "$PROJECT_BASE" ] \ + && [ "$("$REAL_GIT" -C "$project" rev-parse --abbrev-ref HEAD)" = main ]; then + pass "the shared source repo's HEAD is untouched" + else bad "the shared source repo drifted"; fi + if [ "$("$REAL_GIT" -C "$canonical" rev-parse HEAD)" = "$CANON_BASE" ]; then + pass "your stand-in real checkout's HEAD is untouched (containment held)" + else bad "the canonical checkout HEAD moved ($CANON_BASE -> $("$REAL_GIT" -C "$canonical" rev-parse HEAD))"; fi + + # I5 one 32-byte key, both signed bindings (no split-brain) + ks=$(wc -c < "$home/.config-integrity-key" 2>/dev/null | tr -d ' ') + if [ "$ks" = 32 ] && [ -f "$home/runtime/agent-a/binding.json.sig" ] && [ -f "$home/runtime/agent-b/binding.json.sig" ]; then + pass "one 32-byte integrity key; both agents' signed bindings present" + else bad "key/binding split-brain (keysize=$ks)"; fi + + # I6 both agents appear, attributed, in the shared audit log — independent proof + # each went through the shim and its activity was recorded to it. + elog="$home/fleet_events.jsonl" + if grep -q "agent-a" "$elog" 2>/dev/null && grep -q "agent-b" "$elog" 2>/dev/null; then + pass "both agents' activity is recorded, per-agent, in the shared audit log" + else bad "audit log missing per-agent attribution"; fi + + # I7 CONSISTENCY cross-check: each agent's OWN report must AGREE with the truth + # above. Authority is the independent state (I0-I6); this only flags an agent + # whose self-report disagrees — it can NEVER turn a real violation into a pass. + consistent() { local role="$1" ok=0 n exp rc + [ "$(cat "$arts/$role/verdict.txt" 2>/dev/null)" = PASS ] || ok=1 + for n in $(seq 1 10); do + exp=$(cat "$arts/$role/$n.expect" 2>/dev/null); rc=$(cat "$arts/$role/$n.rc" 2>/dev/null) + case "$exp" in ok) [ "$rc" = 0 ] || ok=1 ;; deny) [ "$rc" != 0 ] || ok=1 ;; *) ok=1 ;; esac + done + return "$ok" + } + if consistent a; then pass "agent-a's self-report agrees with the re-derived state"; else bad "agent-a self-report inconsistent"; fi + if consistent b; then pass "agent-b's self-report agrees with the re-derived state"; else bad "agent-b self-report inconsistent"; fi + + if [ "$FAILS" = 0 ]; then + say "$(c '1;32')MULTI-AGENT SCENARIO VERIFIED$(c 0) — every invariant held (re-derived from state): two agents shared one repo, stayed isolated, kept per-agent provenance, and could not clobber each other or touch your checkout." + return 0 + fi + say "$(c '1;31')FAILED$(c 0) — $FAILS invariant(s) did not hold." + return 1 +} diff --git a/demo/multi-agent/live.sh b/demo/multi-agent/live.sh new file mode 100755 index 0000000..c93ba00 --- /dev/null +++ b/demo/multi-agent/live.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# +# live.sh — Layer 2 driver. Same scenario as verify.sh, but the two guarded +# sessions are run by TWO REAL AGENTS (or just two shells) instead of spawned +# in-process. It uses the SAME lib.sh build_world + synthesize, so the rigor is +# identical — only WHO runs the sessions changes. The supervisor is still the +# verifier: agents merely execute + leave evidence; you re-derive the verdict. +# +# ./live.sh setup [--world DIR] [--reset] # build a persistent world, print +# # the two commands to hand out +# ./live.sh synth # re-derive the verdict yourself +# +# Typical flow: +# ./live.sh setup --world /tmp/l2 # prints: sh /tmp/l2/agent-launch.sh a|b +# # hand each command to a different agent/shell; run them CONCURRENTLY +# ./live.sh synth /tmp/l2 # VERIFIED / FAILED, from state you own +set -u +HERE="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=lib.sh +. "$HERE/lib.sh" + +usage() { printf 'usage: %s setup [--world DIR] [--reset] | synth \n' "$0" >&2; exit 2; } + +cmd="${1:-}"; shift 2>/dev/null || true +case "$cmd" in + setup) + world=""; reset=0 + while [ $# -gt 0 ]; do + case "$1" in + --world) world="${2:?--world needs a DIR}"; shift 2 ;; + --reset) reset=1; shift ;; + *) usage ;; + esac + done + [ -n "$world" ] || world="$(mktemp -d)" + scrub_hostile_env + REAL_GIT="$(resolve_real_git)" || die "no real (non-shim) git on PATH" + BIN="$(resolve_bin)" || die "no guarded agentic-git — build from the repo, or set AGENTIC_GIT_BIN" + + # Freshness (fugu design review, holes 1+3): refuse a non-empty world unless + # --reset — otherwise a PRIOR run's tips/worktrees/artifacts could let synth + # report VERIFIED for a run that never happened. + if [ -e "$world" ] && [ -n "$(ls -A "$world" 2>/dev/null || true)" ]; then + [ "$reset" = 1 ] || die "world exists and is non-empty: $world (pass --reset to rebuild it, or choose a fresh --world)" + rm -rf "$world" + fi + mkdir -p "$world" || die "cannot create world: $world" + # Atomic setup lock so two concurrent setups can't interleave the world. + mkdir "$world/.lock" 2>/dev/null || die "another setup holds $world/.lock (or a crashed run left it — rerun with --reset)" + # shellcheck disable=SC2064 + trap "rmdir '$world/.lock' 2>/dev/null || true" EXIT + + build_world "$world" + cp "$HERE/agent-run.sh" "$world/agent-run.sh" + cp "$HERE/agent-launch.sh" "$world/agent-launch.sh" + chmod +x "$world/agent-launch.sh" + # shellcheck disable=SC1091 + . "$world/world.env" + + say "World ready: $world" + printf ' guarded binary : %s\n' "$BIN" + printf ' version : %s\n' "$("$BIN" version 2>/dev/null || echo '?')" + printf ' run id : %s\n' "$RUN_ID" + say "Hand ONE command to each of two real agents (or two shells) — run them CONCURRENTLY:" + printf ' agent-a: sh %s/agent-launch.sh a\n' "$world" + printf ' agent-b: sh %s/agent-launch.sh b\n' "$world" + say "Then re-derive the verdict yourself (do NOT trust the agents' word):" + printf ' %s synth %s\n' "$0" "$world" + ;; + synth) + world="${1:?usage: synth }" + [ -d "$world" ] || die "no such world: $world" + scrub_hostile_env + synthesize "$world"; exit $? + ;; + *) usage ;; +esac diff --git a/demo/multi-agent/verify.sh b/demo/multi-agent/verify.sh index 76ffccd..a692ff5 100755 --- a/demo/multi-agent/verify.sh +++ b/demo/multi-agent/verify.sh @@ -1,63 +1,32 @@ #!/usr/bin/env bash # -# verify.sh — the deterministic SUPERVISOR for the multi-agent scenario. It owns -# setup and synthesis; the agents only execute + produce evidence (see -# agent-run.sh). Two real guarded agent sessions run CONCURRENTLY against ONE -# shared repo, each on its own branch; then the supervisor re-derives the truth -# from git/home/audit STATE (never from an agent's own verdict) and checks the -# multi-agent invariants. An agent that reports PASS but whose state disagrees -# still FAILS. +# verify.sh — Layer 1 driver for the multi-agent scenario. The SUPERVISOR owns +# setup and synthesis (both in lib.sh); here it spawns the two guarded agent +# sessions ITSELF, in-process and concurrently, against one shared repo. Then it +# re-derives the truth from git/home/audit STATE (never from an agent's own +# verdict) via the shared synthesize(). An agent that reports PASS but whose +# state disagrees still FAILS. # # ./verify.sh # run it, print the synthesis # ./verify.sh --keep # keep the throwaway world for inspection # +# For the SAME scenario driven by two real agents (each running its own guarded +# session), see live.sh — it reuses lib.sh's build_world + synthesize verbatim. set -u -unset AGENTIC_GIT_BYPASS AGEND_GIT_BYPASS \ - AGENTIC_GIT_BYPASS_AGENT AGEND_GIT_BYPASS_AGENT \ - AGENTIC_GIT_BYPASS_UNTIL AGEND_GIT_BYPASS_UNTIL \ - AGENTIC_GIT_AGENT AGENTIC_GIT_HOME 2>/dev/null || true +HERE="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=lib.sh +. "$HERE/lib.sh" -here="$(cd "$(dirname "$0")" && pwd)" KEEP=0; [ "${1:-}" = "--keep" ] && KEEP=1 -c() { printf '\033[%sm' "$1"; } -say() { printf '\n%s▸ %s%s\n' "$(c '1;36')" "$*" "$(c 0)"; } -pass() { printf ' %s✓ %s%s\n' "$(c '1;32')" "$*" "$(c 0)"; } -bad() { printf ' %s✗ %s%s\n' "$(c '1;31')" "$*" "$(c 0)"; FAILS=$((FAILS+1)); } -die() { printf '\n%s✗ %s%s\n' "$(c '1;31')" "$*" "$(c 0)" >&2; exit 1; } - -# agentic-git: explicit override, then installed, then the repo's release build. -BIN="${AGENTIC_GIT_BIN:-}" -{ [ -n "$BIN" ] && [ -x "$BIN" ]; } || BIN="$(command -v agentic-git || true)" -repo_root="$(cd "$here/../.." 2>/dev/null && pwd || true)" -if { [ -z "$BIN" ] || [ ! -x "$BIN" ]; } && [ -n "$repo_root" ] && [ -f "$repo_root/Cargo.toml" ]; then - BIN="$repo_root/target/release/agentic-git" - [ -x "$BIN" ] || ( cd "$repo_root" && cargo build --release -p agentic-git >/dev/null ) || die "build failed" -fi -{ [ -n "$BIN" ] && [ -x "$BIN" ]; } || die "agentic-git not found — cargo install agentic-git, or run from the repo" - -resolve_real_git() { local d IFS=:; for d in $PATH; do case "$d" in *.agentic-git*|*.agend-terminal*|*/.cargo/bin*) continue ;; esac; [ -x "$d/git" ] && { printf '%s\n' "$d/git"; return 0; }; done; return 1; } +scrub_hostile_env REAL_GIT="$(resolve_real_git)" || die "no real (non-shim) git on PATH" -sanitized_path() { local d IFS=: out; out="$(dirname "$REAL_GIT")"; for d in $PATH; do case "$d" in *.agentic-git*|*.agend-terminal*) continue ;; esac; out="$out:$d"; done; printf '%s\n' "$out"; } +BIN="$(resolve_bin)" || die "agentic-git not found — build from the repo, or set AGENTIC_GIT_BIN" -# ── the shared world ───────────────────────────────────────────────────────── +# ── the shared world (lib.sh owns setup) ───────────────────────────────────── work="$(mktemp -d)"; [ "$KEEP" = 1 ] || trap 'rm -rf "$work"' EXIT -project="$work/project"; canonical="$work/your-checkout"; bare="$work/origin.git"; home="$work/home" -arts="$work/artifacts"; mkdir -p "$project" "$canonical" "$arts/a" "$arts/b" -export GIT_AUTHOR_NAME=you GIT_AUTHOR_EMAIL=you@example.com GIT_COMMITTER_NAME=you GIT_COMMITTER_EMAIL=you@example.com -"$REAL_GIT" init -q --bare "$bare" -"$REAL_GIT" -C "$project" init -q -b main -"$REAL_GIT" -C "$project" config user.name you; "$REAL_GIT" -C "$project" config user.email you@example.com -"$REAL_GIT" -C "$project" remote add origin "$bare" -printf 'shared project\n' > "$project/README.md" -"$REAL_GIT" -C "$project" add -A; "$REAL_GIT" -C "$project" commit -qm "project baseline" -"$REAL_GIT" -C "$project" push -q origin main -PROJECT_BASE="$("$REAL_GIT" -C "$project" rev-parse HEAD)" -"$REAL_GIT" -C "$canonical" init -q -b main -"$REAL_GIT" -C "$canonical" config user.name you; "$REAL_GIT" -C "$canonical" config user.email you@example.com -"$REAL_GIT" -C "$canonical" remote add origin https://example.invalid/your-project.git -printf 'your real work\n' > "$canonical/app.py"; "$REAL_GIT" -C "$canonical" add -A; "$REAL_GIT" -C "$canonical" commit -qm base -"$REAL_GIT" -C "$canonical" commit -q --allow-empty -m more -CANON_BASE="$("$REAL_GIT" -C "$canonical" rev-parse HEAD)" +build_world "$work" +# shellcheck disable=SC1091 +. "$work/world.env" # project canonical bare home arts BIN PROJECT_BASE CANON_BASE RUN_ID spawn_agent() { # spawn_agent local role="$1" art="$2" other @@ -66,7 +35,7 @@ spawn_agent() { # spawn_agent AGENTIC_GIT_HOME="$home" AGENTIC_GIT_REAL_GIT="$REAL_GIT" AGENTIC_GIT_BIN="$BIN" \ PATH="$(sanitized_path)" \ "$BIN" run --agent "agent-$role" --branch "feat/$role" -- \ - sh "$here/agent-run.sh" "agent-$role" "feat/$role" "feat/$other" "$art" "$canonical" \ + sh "$HERE/agent-run.sh" "agent-$role" "feat/$role" "feat/$other" "$art" "$canonical" \ ) >"$art/session.log" 2>&1 } @@ -78,80 +47,7 @@ wait "$pb"; rb=$? [ "$ra" = 0 ] || printf ' (agent-a session exit %s — see %s)\n' "$ra" "$arts/a/session.log" [ "$rb" = 0 ] || printf ' (agent-b session exit %s — see %s)\n' "$rb" "$arts/b/session.log" -# ── SYNTHESIS: re-derive the truth from STATE, not from the agents' word ────── -say "Synthesis — re-deriving the truth from git/home/audit state:" -FAILS=0 -# Derive the agents' worktrees from the SHARED PROJECT's OWN worktree list. The -# supervisor owns the project repo, so this can't be redirected by an agent -# rewriting its binding.json under $AGENTIC_GIT_HOME (fugu review: HOME is -# agent-writable at same-uid — see the honest boundary in the README). -wt_for_branch() { "$REAL_GIT" -C "$project" worktree list --porcelain 2>/dev/null | awk -v b="refs/heads/$1" '/^worktree /{w=$2} $0==("branch " b){print w; exit}'; } -rbr() { "$REAL_GIT" -C "$1" symbolic-ref --short HEAD 2>/dev/null; } -wa="$(wt_for_branch feat/a)"; wb="$(wt_for_branch feat/b)" - -# I1 two distinct worktrees LINKED TO THE SHARED PROJECT (from its own worktree -# list), each STILL on its own bound branch (real git — catches a cross-branch -# checkout that drifted HEAD) -if [ -n "$wa" ] && [ -n "$wb" ] && [ "$wa" != "$wb" ]; then - pass "the shared project has two distinct agent worktrees (from its own worktree list)" -else bad "the shared project lacks two distinct agent worktrees (a=$wa b=$wb)"; fi -if [ "$(rbr "$wa")" = feat/a ] && [ "$(rbr "$wb")" = feat/b ]; then - pass "each worktree HEAD is on its own bound branch (no cross-branch drift)" -else bad "a worktree drifted off its branch (a=$(rbr "$wa") b=$(rbr "$wb"))"; fi - -# I2 origin branches distinct + each trailered to its OWN agent — this alone -# catches a cross-agent force-push clobber or delete (no agent word trusted) -ta="$("$REAL_GIT" -C "$bare" rev-parse feat/a 2>/dev/null)"; tb="$("$REAL_GIT" -C "$bare" rev-parse feat/b 2>/dev/null)" -if [ -n "$ta" ] && [ -n "$tb" ] && [ "$ta" != "$tb" ]; then - pass "both branches on the shared origin, distinct tips (neither deleted/collapsed)" -else bad "an origin branch was clobbered or deleted (a=$ta b=$tb)"; fi -if "$REAL_GIT" -C "$bare" log -1 --format=%B feat/a 2>/dev/null | grep -q "Agentic-Agent: agent-a" \ - && "$REAL_GIT" -C "$bare" log -1 --format=%B feat/b 2>/dev/null | grep -q "Agentic-Agent: agent-b"; then - pass "each origin branch's tip is trailered to its OWN agent (not clobbered)" -else bad "origin provenance is wrong or one branch was clobbered by the other agent"; fi - -# I4 the shared source repo AND your stand-in real checkout were NOT moved. The -# canonical check independently catches an agent that touched your checkout, -# regardless of what the agent reported (closes fugu's forged-artifact hole). -if [ "$("$REAL_GIT" -C "$project" rev-parse HEAD)" = "$PROJECT_BASE" ] \ - && [ "$("$REAL_GIT" -C "$project" rev-parse --abbrev-ref HEAD)" = main ]; then - pass "the shared source repo's HEAD is untouched" -else bad "the shared source repo drifted"; fi -if [ "$("$REAL_GIT" -C "$canonical" rev-parse HEAD)" = "$CANON_BASE" ]; then - pass "your stand-in real checkout's HEAD is untouched (containment held)" -else bad "the canonical checkout HEAD moved ($CANON_BASE -> $("$REAL_GIT" -C "$canonical" rev-parse HEAD))"; fi - -# I5 one 32-byte key, both signed bindings (no split-brain) -ks=$(wc -c < "$home/.config-integrity-key" 2>/dev/null | tr -d ' ') -if [ "$ks" = 32 ] && [ -f "$home/runtime/agent-a/binding.json.sig" ] && [ -f "$home/runtime/agent-b/binding.json.sig" ]; then - pass "one 32-byte integrity key; both agents' signed bindings present" -else bad "key/binding split-brain (keysize=$ks)"; fi - -# I6 both agents appear, attributed, in the shared audit log — independent proof -# each went through the shim and its activity was recorded to it. -elog="$home/fleet_events.jsonl" -if grep -q "agent-a" "$elog" 2>/dev/null && grep -q "agent-b" "$elog" 2>/dev/null; then - pass "both agents' activity is recorded, per-agent, in the shared audit log" -else bad "audit log missing per-agent attribution"; fi - -# I7 CONSISTENCY cross-check: each agent's OWN report must AGREE with the truth -# above. Authority is the independent state (I1-I6); this only flags an agent -# whose self-report disagrees — it can NEVER turn a real violation into a pass. -consistent() { role="$1"; ok=0 - [ "$(cat "$arts/$role/verdict.txt" 2>/dev/null)" = PASS ] || ok=1 - for n in $(seq 1 10); do - exp=$(cat "$arts/$role/$n.expect" 2>/dev/null); rc=$(cat "$arts/$role/$n.rc" 2>/dev/null) - case "$exp" in ok) [ "$rc" = 0 ] || ok=1 ;; deny) [ "$rc" != 0 ] || ok=1 ;; *) ok=1 ;; esac - done - return "$ok" -} -if consistent a; then pass "agent-a's self-report agrees with the re-derived state"; else bad "agent-a self-report inconsistent"; fi -if consistent b; then pass "agent-b's self-report agrees with the re-derived state"; else bad "agent-b self-report inconsistent"; fi - -if [ "$FAILS" = 0 ]; then - say "$(c '1;32')MULTI-AGENT SCENARIO VERIFIED$(c 0) — every invariant held (re-derived from state): two agents shared one repo, stayed isolated, kept per-agent provenance, and could not clobber each other or touch your checkout." -else - say "$(c '1;31')FAILED$(c 0) — $FAILS invariant(s) did not hold." -fi +# ── synthesis (lib.sh owns it — same function live.sh calls) ───────────────── +synthesize "$work"; rc=$? [ "$KEEP" = 1 ] && printf '\n world kept at: %s\n' "$work" -exit "$([ "$FAILS" = 0 ] && echo 0 || echo 1)" +exit "$rc" From af0e618e4215ebb20e7848a8124eda8ad7d3fa40 Mon Sep 17 00:00:00 2001 From: suzuke Date: Mon, 6 Jul 2026 17:22:31 +0800 Subject: [PATCH 2/2] demo: fix Layer 2 diff-review findings (BIN override, shellcheck repro, rev-parse diagnostic) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses fugu's diff review of PR #21: 1. AGENTIC_GIT_BIN override was dead — scrub_hostile_env ran before resolve_bin and blanket-unset every AGENTIC_GIT_*, including AGENTIC_GIT_BIN. Exclude it from the scrub (lib.sh + the agent-launch.sh mirror): it's a deliberate binary override, not a hostile knob. 2. shellcheck now clean from ANY cwd — add `# shellcheck source-path=SCRIPTDIR` to verify.sh/live.sh, and make build_world publish its output vars as globals so the drivers no longer dynamically source world.env (which shellcheck can't follow → SC1091/SC2154/SC2034 when run from repo root). 3. synthesize I2 used `git rev-parse feat/a`, which echoes the token on a missing ref — so the distinct-tips ✓ printed spuriously before agents ran (verdict was always correctly FAILED via I0/provenance; only diagnostics were wrong). Use `rev-parse --verify -q` so a missing ref yields empty. Verified: shellcheck clean from repo root (all 4); AGENTIC_GIT_BIN override honored (fake bin path printed); verify.sh VERIFIED; live.sh full flow VERIFIED; adversarial matrix 8/8; synth-before-agents now shows I2 as failed. Co-Authored-By: Claude Opus 4.8 (1M context) --- demo/multi-agent/agent-launch.sh | 5 ++++- demo/multi-agent/lib.sh | 31 ++++++++++++++++++++----------- demo/multi-agent/live.sh | 5 ++--- demo/multi-agent/verify.sh | 5 ++--- 4 files changed, 28 insertions(+), 18 deletions(-) diff --git a/demo/multi-agent/agent-launch.sh b/demo/multi-agent/agent-launch.sh index 2af3689..b769493 100755 --- a/demo/multi-agent/agent-launch.sh +++ b/demo/multi-agent/agent-launch.sh @@ -22,7 +22,10 @@ WORLD="$(cd "$(dirname "$0")" && pwd)" # Strip everything that could redirect/reconfigure git despite `-C`, plus every # agentic-git/agend bypass/agent/home/allow knob. HOME/REAL_GIT/BIN are set by us # at the run site below, so a blanket unset here is safe. -for v in $(env 2>/dev/null | sed -n 's/^\(AGENTIC_GIT_[A-Za-z0-9_]*\)=.*/\1/p; s/^\(AGEND_GIT_[A-Za-z0-9_]*\)=.*/\1/p'); do unset "$v" 2>/dev/null || true; done +for v in $(env 2>/dev/null | sed -n 's/^\(AGENTIC_GIT_[A-Za-z0-9_]*\)=.*/\1/p; s/^\(AGEND_GIT_[A-Za-z0-9_]*\)=.*/\1/p'); do + [ "$v" = AGENTIC_GIT_BIN ] && continue # deliberate binary override (mirrors lib.sh scrub_hostile_env) + unset "$v" 2>/dev/null || true +done unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_OBJECT_DIRECTORY GIT_ALTERNATE_OBJECT_DIRECTORIES \ GIT_COMMON_DIR GIT_NAMESPACE GIT_CEILING_DIRECTORIES GIT_PREFIX \ GIT_CONFIG GIT_CONFIG_GLOBAL GIT_CONFIG_SYSTEM GIT_CONFIG_NOSYSTEM \ diff --git a/demo/multi-agent/lib.sh b/demo/multi-agent/lib.sh index 6b82a95..a329ae7 100644 --- a/demo/multi-agent/lib.sh +++ b/demo/multi-agent/lib.sh @@ -17,7 +17,12 @@ # explicitly at each guarded-run site, so a blanket unset here is safe. scrub_hostile_env() { local v - for v in $(env 2>/dev/null | sed -n 's/^\(AGENTIC_GIT_[A-Za-z0-9_]*\)=.*/\1/p; s/^\(AGEND_GIT_[A-Za-z0-9_]*\)=.*/\1/p'); do unset "$v" 2>/dev/null || true; done + # AGENTIC_GIT_BIN is a deliberate binary override (resolve_bin honours it), not + # a hostile knob — keep it; scrub every other agentic-git/agend var. + for v in $(env 2>/dev/null | sed -n 's/^\(AGENTIC_GIT_[A-Za-z0-9_]*\)=.*/\1/p; s/^\(AGEND_GIT_[A-Za-z0-9_]*\)=.*/\1/p'); do + [ "$v" = AGENTIC_GIT_BIN ] && continue + unset "$v" 2>/dev/null || true + done unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_OBJECT_DIRECTORY GIT_ALTERNATE_OBJECT_DIRECTORIES \ GIT_COMMON_DIR GIT_NAMESPACE GIT_CEILING_DIRECTORIES GIT_PREFIX \ GIT_CONFIG GIT_CONFIG_GLOBAL GIT_CONFIG_SYSTEM GIT_CONFIG_NOSYSTEM \ @@ -59,32 +64,34 @@ resolve_bin() { # ── build the shared world ─────────────────────────────────────────────────── # Uses the REAL_GIT and BIN globals set by the caller. Writes a run-unique # baseline (so a persistent world's freshness is provable) and an atomically -# published world.env carrying STATE=ready. build_world +# published world.env carrying STATE=ready. Also EXPORTS the world's paths and +# RUN_ID/PROJECT_BASE/CANON_BASE as globals so the caller can use them directly +# (verify.sh) without re-sourcing world.env. build_world build_world() { - local world="$1" project canonical bare home arts rid pbase cbase + local world="$1" project="$world/project"; canonical="$world/your-checkout"; bare="$world/origin.git"; home="$world/home"; arts="$world/artifacts" mkdir -p "$project" "$canonical" "$arts/a" "$arts/b" - rid="run-$(date +%Y%m%d%H%M%S)-$$" + RUN_ID="run-$(date +%Y%m%d%H%M%S)-$$" export GIT_AUTHOR_NAME=you GIT_AUTHOR_EMAIL=you@example.com GIT_COMMITTER_NAME=you GIT_COMMITTER_EMAIL=you@example.com "$REAL_GIT" init -q --bare "$bare" "$REAL_GIT" -C "$project" init -q -b main "$REAL_GIT" -C "$project" config user.name you; "$REAL_GIT" -C "$project" config user.email you@example.com "$REAL_GIT" -C "$project" remote add origin "$bare" - printf 'shared project\nrun: %s\n' "$rid" > "$project/README.md" - "$REAL_GIT" -C "$project" add -A; "$REAL_GIT" -C "$project" commit -qm "project baseline ($rid)" + printf 'shared project\nrun: %s\n' "$RUN_ID" > "$project/README.md" + "$REAL_GIT" -C "$project" add -A; "$REAL_GIT" -C "$project" commit -qm "project baseline ($RUN_ID)" "$REAL_GIT" -C "$project" push -q origin main - pbase="$("$REAL_GIT" -C "$project" rev-parse HEAD)" + PROJECT_BASE="$("$REAL_GIT" -C "$project" rev-parse HEAD)" "$REAL_GIT" -C "$canonical" init -q -b main "$REAL_GIT" -C "$canonical" config user.name you; "$REAL_GIT" -C "$canonical" config user.email you@example.com "$REAL_GIT" -C "$canonical" remote add origin https://example.invalid/your-project.git printf 'your real work\n' > "$canonical/app.py"; "$REAL_GIT" -C "$canonical" add -A; "$REAL_GIT" -C "$canonical" commit -qm base "$REAL_GIT" -C "$canonical" commit -q --allow-empty -m more - cbase="$("$REAL_GIT" -C "$canonical" rev-parse HEAD)" - { printf 'RUN_ID=%s\n' "$rid" + CANON_BASE="$("$REAL_GIT" -C "$canonical" rev-parse HEAD)" + { printf 'RUN_ID=%s\n' "$RUN_ID" printf 'project=%s\n' "$project"; printf 'canonical=%s\n' "$canonical" printf 'bare=%s\n' "$bare"; printf 'home=%s\n' "$home"; printf 'arts=%s\n' "$arts" printf 'BIN=%s\n' "$BIN" - printf 'PROJECT_BASE=%s\n' "$pbase"; printf 'CANON_BASE=%s\n' "$cbase" + printf 'PROJECT_BASE=%s\n' "$PROJECT_BASE"; printf 'CANON_BASE=%s\n' "$CANON_BASE" printf 'STATE=ready\n' } > "$world/world.env.tmp" mv "$world/world.env.tmp" "$world/world.env" @@ -132,7 +139,9 @@ synthesize() { # I2 origin branches distinct + each trailered to its OWN agent — this alone # catches a cross-agent force-push clobber or delete (no agent word trusted) - ta="$("$REAL_GIT" -C "$bare" rev-parse feat/a 2>/dev/null)"; tb="$("$REAL_GIT" -C "$bare" rev-parse feat/b 2>/dev/null)" + # --verify -q: a missing ref yields empty + nonzero (never echoes the token), + # so a not-yet-pushed branch can't spuriously satisfy the distinct-tips check. + ta="$("$REAL_GIT" -C "$bare" rev-parse --verify -q feat/a 2>/dev/null || true)"; tb="$("$REAL_GIT" -C "$bare" rev-parse --verify -q feat/b 2>/dev/null || true)" if [ -n "$ta" ] && [ -n "$tb" ] && [ "$ta" != "$tb" ]; then pass "both branches on the shared origin, distinct tips (neither deleted/collapsed)" else bad "an origin branch was clobbered or deleted (a=$ta b=$tb)"; fi diff --git a/demo/multi-agent/live.sh b/demo/multi-agent/live.sh index c93ba00..38ecbb7 100755 --- a/demo/multi-agent/live.sh +++ b/demo/multi-agent/live.sh @@ -16,6 +16,7 @@ # ./live.sh synth /tmp/l2 # VERIFIED / FAILED, from state you own set -u HERE="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source-path=SCRIPTDIR # shellcheck source=lib.sh . "$HERE/lib.sh" @@ -50,12 +51,10 @@ case "$cmd" in # shellcheck disable=SC2064 trap "rmdir '$world/.lock' 2>/dev/null || true" EXIT - build_world "$world" + build_world "$world" # sets globals incl. RUN_ID cp "$HERE/agent-run.sh" "$world/agent-run.sh" cp "$HERE/agent-launch.sh" "$world/agent-launch.sh" chmod +x "$world/agent-launch.sh" - # shellcheck disable=SC1091 - . "$world/world.env" say "World ready: $world" printf ' guarded binary : %s\n' "$BIN" diff --git a/demo/multi-agent/verify.sh b/demo/multi-agent/verify.sh index a692ff5..e9a2da7 100755 --- a/demo/multi-agent/verify.sh +++ b/demo/multi-agent/verify.sh @@ -14,6 +14,7 @@ # session), see live.sh — it reuses lib.sh's build_world + synthesize verbatim. set -u HERE="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source-path=SCRIPTDIR # shellcheck source=lib.sh . "$HERE/lib.sh" @@ -24,9 +25,7 @@ BIN="$(resolve_bin)" || die "agentic-git not found — build from the repo, or s # ── the shared world (lib.sh owns setup) ───────────────────────────────────── work="$(mktemp -d)"; [ "$KEEP" = 1 ] || trap 'rm -rf "$work"' EXIT -build_world "$work" -# shellcheck disable=SC1091 -. "$work/world.env" # project canonical bare home arts BIN PROJECT_BASE CANON_BASE RUN_ID +build_world "$work" # sets globals: project canonical bare home arts PROJECT_BASE CANON_BASE RUN_ID spawn_agent() { # spawn_agent local role="$1" art="$2" other