#!/usr/bin/env bash # The launcher at the bottom of the escalation ladder. # # Every rung -- the crash toast, the failed reload, a red health check, # `panama diagnose` -- ends at bin/panama-agent, which turns two settings into # one terminal running one agent. It is the single place where the ladder can # quietly become a no-op, or launch the wrong thing, or lose the prompt it was # handed, so it is the single place worth pinning. # # What must hold: # # 1. "none" is silent. No agent chosen is the shipped state, not an error. # A rung that shouted about it is a rung that gets switched off, and a # missing settings file means the same thing as "none". # 2. The argv per agent is the one the installed binaries actually accept. # These flags move between releases; the table in panama-agent was read off # `claude --help` and `codex --help`, and this pins the shape of it. # 3. agentAutoApprove off means no mode flag at all. The agent's own default # is a choice the user already made. # 4. The prompt is ONE argv element. Crash and diagnose prompts are multi-line # paragraphs; a prompt that arrives as forty words is forty words of # nothing. # 5. The working directory is the checkout. The skills the prompts name, the # repository being asked about, and .claude/settings.json's pre-approved # read-only diagnostics all live here and nowhere else. # 6. panama-agent-crash carries all four coredump facts and the absolute path # to the skill. The path is what makes the ladder work for an agent whose # harness has no skill mechanism. # # Hermetic on purpose: kitty, claude, codex, setsid and coredumpctl are all # stubbed onto PATH and the settings file is fabricated, so no agent is ever # launched and no window is ever opened. This is NOT in tests/desktop-hijacking # for exactly that reason -- it must stay runnable mid-session. set -uo pipefail repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" launcher="$repo_dir/bin/panama-agent" crash="$repo_dir/bin/panama-agent-crash" reload="$repo_dir/bin/panama-agent-reload" findings=() note() { findings+=("$1"); } for script in "$launcher" "$crash" "$reload"; do [[ -x "$script" ]] || { printf 'panama agent contract: %s is not executable\n' "$script" >&2; exit 1; } done work="$(mktemp -d)" trap 'rm -rf "$work"' EXIT stub="$work/bin" mkdir -p "$stub" settings="$work/settings.json" # The terminal, replaced by something that records what it was asked to run. # NUL-separated, because the prompts are multi-line and any line-oriented record # would lose exactly the property this is here to check. cat >"$stub/kitty" <"$work/cwd" printf '%s\0' "\$@" >"$work/argv" STUB # setsid is stubbed rather than real so the launch is synchronous and there is # no race between the detached child writing and this reading it. cat >"$stub/setsid" <"$work/setsid-used" exec "\$@" STUB # Present so panama-agent's "is it installed" check passes. Reaching one of # these means the terminal stub was bypassed, which is itself a failure. for agent in claude codex; do cat >"$stub/$agent" <"$work/agent-actually-ran" STUB done # A hand-run PID has no core here; the real one would be a slow lookup against # this machine's journal. cat >"$stub/coredumpctl" <<'STUB' #!/usr/bin/env bash exit 1 STUB # The rungs that quote the journal get a deliberately enormous one. journalctl # counts entries, not lines, and a real one on this machine answered a # thirty-entry request with 2,430 lines and a quarter of a megabyte of # backtrace -- which exec rejects outright, because a single argv element is # capped at 128KB. The prompt builders have to bound what they quote, and this # is what proves they do. cat >"$stub/journalctl" <<'STUB' #!/usr/bin/env bash line="quickshell: QML Item: Cannot assign to non-existent property$(printf '%0.sx' {1..500})" for _ in {1..4000}; do printf '%s\n' "$line"; done STUB chmod +x "$stub"/* # Runs the launcher hermetically and returns its exit status. argv/cwd records # from the previous run are cleared first, so "was not launched" is a missing # file rather than a stale one. launch() { rm -f "$work/argv" "$work/cwd" "$work/setsid-used" "$work/agent-actually-ran" PATH="$stub:/usr/bin:/bin" \ PANAMA_PATH="$repo_dir" \ PANAMA_AGENT_SETTINGS="$settings" \ "$@" >"$work/stdout" 2>"$work/stderr" } # The recorded argv, as an array. recorded_argv() { argv=() [[ -s "$work/argv" ]] || return 1 mapfile -d '' -t argv <"$work/argv" return 0 } # The argv as one string with a separator no prompt contains, so a whole # element can be matched without a line-oriented search. argv_joined() { local IFS=$'\x1f' printf '%s' "${argv[*]}" } # Just the command kitty was told to run: everything after -e, with the # terminal's own flags dropped. Split out because the agent's argv is what these # checks are about, and matching it inside the whole line means writing patterns # that would also match a --title. agent_command() { agent_argv=() local element seen=0 for element in "${argv[@]}"; do if (( seen )); then agent_argv+=("$element") elif [[ "$element" == "-e" ]]; then seen=1 fi done (( ${#agent_argv[@]} > 0 )) } # The agent argv from element 1 on, joined -- the flags, without the resolved # binary path, which differs per machine and is checked separately. agent_flags() { local IFS=$'\x1f' printf '%s' "${agent_argv[*]:1}" } # ── 1. "none" is silent ───────────────────────────────────────────────────── printf '{"preferredAgent":"none"}\n' >"$settings" launch "$launcher" --prompt "anything" status=$? (( status == 0 )) || note "with no agent chosen panama-agent exited $status; choosing none is not an error" [[ -s "$work/stdout" ]] && note 'with no agent chosen panama-agent printed to stdout; it is meant to be silent' [[ -s "$work/stderr" ]] && note "with no agent chosen panama-agent complained: $(head -1 "$work/stderr")" [[ -e "$work/argv" ]] && note 'with no agent chosen panama-agent still opened a terminal' # A machine that has never been asked the question answers the same way. rm -f "$settings" launch "$launcher" --prompt "anything" status=$? (( status == 0 )) || note "with no settings file panama-agent exited $status; the default is none, which is silent" [[ -e "$work/argv" ]] && note 'with no settings file panama-agent opened a terminal anyway' # ── 2, 4, 5. Claude, auto-approving ───────────────────────────────────────── prompt=$'first line\nsecond line with spaces' printf '{"preferredAgent":"claude","agentAutoApprove":true}\n' >"$settings" launch "$launcher" --prompt "$prompt" status=$? (( status == 0 )) || note "panama-agent exited $status launching claude" [[ -e "$work/setsid-used" ]] || note 'the launch does not go through setsid, so the agent dies with whatever spawned it' [[ -e "$work/agent-actually-ran" ]] && note 'the contract reached a real agent binary; it must stop at the terminal stub' if recorded_argv && agent_command; then joined="$(argv_joined)" [[ "$joined" == *$'\x1f'"--class"$'\x1f'"panama-agent"* ]] \ || note 'the terminal is not given the fixed panama-agent window class, so no window rule can find it' [[ "$joined" == *"--directory"$'\x1f'"$repo_dir"* ]] \ || note 'the terminal is not opened in the Panama checkout' # The binary is resolved here, not left for kitty to find. The click that # reaches this script comes from the shell, whose PATH does not contain # ~/.local/bin, and kitty would inherit exactly that PATH. [[ "${agent_argv[0]}" == /* ]] \ || note "the agent is passed to the terminal as '${agent_argv[0]}' rather than a resolved path" [[ "${agent_argv[0]}" == */claude ]] \ || note "the resolved binary is ${agent_argv[0]}, which is not claude" [[ "$(agent_flags)" == "--permission-mode"$'\x1f'"auto"$'\x1f'"--"$'\x1f'"$prompt" ]] \ || note "claude with auto-approve on is launched as: $(agent_flags)" # 4. The prompt survives as one element, newlines and all. [[ "${argv[-1]}" == "$prompt" ]] \ || note 'the prompt did not arrive as a single argv element' # 5. Not just named as a flag -- actually the working directory. [[ "$(cat "$work/cwd" 2>/dev/null)" == "$repo_dir" ]] \ || note "the agent starts in $(cat "$work/cwd" 2>/dev/null), not in the checkout" else note 'launching claude opened no terminal at all' fi # ── 3. Claude, prompting normally ─────────────────────────────────────────── printf '{"preferredAgent":"claude","agentAutoApprove":false}\n' >"$settings" launch "$launcher" --prompt "$prompt" if recorded_argv && agent_command; then [[ "$(agent_flags)" == *"--permission-mode"* ]] \ && note 'agentAutoApprove is off and claude was still given a permission mode' [[ "$(agent_flags)" == "--"$'\x1f'"$prompt" ]] \ || note "claude with auto-approve off is launched as: $(agent_flags)" else note 'launching claude with auto-approve off opened no terminal' fi # ── 2 & 3. Codex, both ways ───────────────────────────────────────────────── printf '{"preferredAgent":"codex","agentAutoApprove":true}\n' >"$settings" launch "$launcher" --prompt "$prompt" if recorded_argv && agent_command; then [[ "${agent_argv[0]}" == */codex ]] \ || note "the resolved binary is ${agent_argv[0]}, which is not codex" [[ "$(agent_flags)" == "--approve-for-me"$'\x1f'"--"$'\x1f'"$prompt" ]] \ || note "codex with auto-approve on is launched as: $(agent_flags)" [[ "${argv[-1]}" == "$prompt" ]] \ || note 'codex did not receive the prompt as a single argv element' else note 'launching codex opened no terminal' fi printf '{"preferredAgent":"codex","agentAutoApprove":false}\n' >"$settings" launch "$launcher" --prompt "$prompt" if recorded_argv && agent_command; then [[ "$(agent_flags)" == *"--approve-for-me"* ]] \ && note 'agentAutoApprove is off and codex was still told to approve for itself' [[ "$(agent_flags)" == "--"$'\x1f'"$prompt" ]] \ || note "codex with auto-approve off is launched as: $(agent_flags)" else note 'launching codex with auto-approve off opened no terminal' fi # ── An agent installed where only the user's own PATH looks ───────────────── # # Both agents install themselves into ~/.local/bin. The rungs are clicked from # the shell, and systemd starts the shell with a PATH that does not contain it, # so a launcher that trusted the inherited PATH would report every installed # agent as missing -- from a detached process, into a stderr nobody reads. home="$work/home" mkdir -p "$home/.local/bin" "$work/bin-noagent" "$work/empty" cp "$stub/kitty" "$stub/setsid" "$work/bin-noagent/" cp "$stub/claude" "$home/.local/bin/claude" printf '{"preferredAgent":"claude","agentAutoApprove":true}\n' >"$settings" rm -f "$work/argv" "$work/cwd" HOME="$home" PATH="$work/bin-noagent:/usr/bin:/bin" \ PANAMA_PATH="$repo_dir" PANAMA_AGENT_SETTINGS="$settings" \ "$launcher" --prompt "$prompt" >"$work/stdout" 2>"$work/stderr" status=$? (( status == 0 )) || note "an agent installed in ~/.local/bin was not found: $(head -1 "$work/stderr")" if recorded_argv && agent_command; then [[ "${agent_argv[0]}" == "$home/.local/bin/claude" ]] \ || note "an agent reachable only through ~/.local/bin resolved to ${agent_argv[0]}" else note 'an agent installed in ~/.local/bin opened no terminal, so every click from the shell would be a dead end' fi # ── An agent that is genuinely not installed says so ──────────────────────── rm -f "$work/argv" "$work/cwd" HOME="$work/empty" PATH="$work/empty:/usr/bin:/bin" \ PANAMA_PATH="$repo_dir" PANAMA_AGENT_SETTINGS="$settings" \ "$launcher" --prompt "$prompt" >"$work/stdout" 2>"$work/stderr" status=$? (( status != 0 )) || note 'a preferred agent that is not installed exited 0, so the rung failed silently' grep -qi 'not installed' "$work/stderr" \ || note 'a missing agent binary produced no explanation naming it' # ── 6. The crash prompt carries the facts ─────────────────────────────────── printf '{"preferredAgent":"claude","agentAutoApprove":true}\n' >"$settings" launch "$crash" 4242 panama-test-cra /usr/bin/panama-test-crasher SIGSEGV status=$? (( status == 0 )) || note "panama-agent-crash exited $status" [[ -e "$work/agent-actually-ran" ]] && note 'panama-agent-crash reached a real agent binary' if recorded_argv; then crash_prompt="${argv[-1]}" for fact in 4242 panama-test-cra /usr/bin/panama-test-crasher SIGSEGV; do [[ "$crash_prompt" == *"$fact"* ]] \ || note "the crash prompt does not mention $fact, which a diagnosis needs" done [[ "$crash_prompt" == *"$repo_dir/skills/diagnose-crash/SKILL.md"* ]] \ || note 'the crash prompt does not give the absolute path to the diagnose-crash skill, so an agent without a skill mechanism has nothing to read' [[ -r "$repo_dir/skills/diagnose-crash/SKILL.md" ]] \ || note 'the path the crash prompt points at does not exist' else note 'panama-agent-crash opened no terminal' fi # Something that is not a PID is a usage error, not a prompt about "unknown". launch "$crash" not-a-pid status=$? (( status != 0 )) || note 'panama-agent-crash accepted a non-PID and launched anyway' [[ -e "$work/argv" ]] && note 'panama-agent-crash launched an agent for a non-PID' # ── The reload prompt ─────────────────────────────────────────────────────── launch "$reload" "shell.qml:42 Cannot assign to non-existent property" status=$? (( status == 0 )) || note "panama-agent-reload exited $status" if recorded_argv; then [[ "${argv[-1]}" == *"Cannot assign to non-existent property"* ]] \ || note 'the reload prompt does not carry what Quickshell reported' else note 'panama-agent-reload opened no terminal' fi launch "$reload" status=$? (( status != 0 )) || note 'panama-agent-reload with nothing to report launched an agent anyway' # ── The by-hand rung ──────────────────────────────────────────────────────── # # `panama diagnose` quotes the journal too, and quoted the whole of it once: # exec answered with "Argument list too long" and the rung did nothing at all. # The health summary here is the real one, which is read-only and takes about a # second. launch "$repo_dir/bin/panama" diagnose the bar disappears after unplugging status=$? (( status == 0 )) || note "panama diagnose exited $status: $(head -1 "$work/stderr")" if recorded_argv; then diagnose_prompt="${argv[-1]}" (( ${#diagnose_prompt} < 131072 )) \ || note "the diagnose prompt is ${#diagnose_prompt} bytes; one argv element caps at 128KB and exec would refuse it" [[ "$diagnose_prompt" == *"the bar disappears after unplugging"* ]] \ || note 'the diagnose prompt dropped the words the person typed, which are the part no collector reports' else note 'panama diagnose opened no terminal' fi if (( ${#findings[@]} > 0 )); then printf 'panama agent contract: %d finding(s)\n' "${#findings[@]}" >&2 printf ' - %s\n' "${findings[@]}" >&2 exit 1 fi printf 'panama agent contract: PASS\n'