Files
Panama/tests/quickshell/panama-agent-contract
T

418 lines
18 KiB
Bash
Executable File

#!/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.
# 7. T3 Code opens no terminal. It starts a thread in the checkout's project
# with the prompt intact on stdin, and opens that thread's t3code:// link.
#
# Hermetic on purpose: kitty, claude, codex, t3, xdg-open, 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" <<STUB
#!/usr/bin/env bash
printf '%s' "\$PWD" >"$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" <<STUB
#!/usr/bin/env bash
: >"$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" <<STUB
#!/usr/bin/env bash
: >"$work/agent-actually-ran"
STUB
done
# T3 Code's CLI. It records each call and its stdin, and answers `thread start`
# with a thread id the way the real one does.
cat >"$stub/t3" <<STUB
#!/usr/bin/env bash
printf '%s\0' "\$@" >>"$work/t3-calls"
printf '\n' >>"$work/t3-calls"
if [[ "\$1 \$2" == "thread start" ]]; then
cat >"$work/t3-stdin"
printf 'thread-123\n'
fi
STUB
cat >"$stub/xdg-open" <<STUB
#!/usr/bin/env bash
printf '%s' "\$1" >"$work/opened"
STUB
# 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" \
"$work/t3-calls" "$work/t3-stdin" "$work/opened"
T3CODE_HOME="$work/t3home" \
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
# ── 7. T3 Code ──────────────────────────────────────────────────────────────
# The recorded t3 calls, one per line, arguments joined by \x1f.
t3_calls() { tr '\0' '\037' <"$work/t3-calls" 2>/dev/null; }
mkdir -p "$work/t3home/userdata"
printf 'env-456' >"$work/t3home/userdata/environment-id"
printf '{"preferredAgent":"t3","agentAutoApprove":true}\n' >"$settings"
dash_prompt=$'--looks-like-a-flag\nsecond line'
launch "$launcher" --prompt "$dash_prompt"
status=$?
(( status == 0 )) || note "panama-agent exited $status launching T3 Code: $(head -1 "$work/stderr")"
[[ -e "$work/argv" ]] && note 'T3 Code was launched in a terminal; it should open a thread in the app'
t3_calls | grep -qxF "thread"$'\x1f'"start"$'\x1f'"$repo_dir"$'\x1f'"-"$'\x1f' \
|| note 'T3 Code was not asked to start a thread in the checkout with the prompt on stdin'
[[ "$(cat "$work/t3-stdin" 2>/dev/null)" == "$dash_prompt" ]] \
|| note 'the T3 thread did not receive the prompt intact on stdin'
[[ "$(cat "$work/opened" 2>/dev/null)" == "t3code://threads/env-456/thread-123" ]] \
|| note "T3 Code opened '$(cat "$work/opened" 2>/dev/null)' rather than the new thread's link"
# No prompt means nothing to start: the app opens on the project.
launch "$launcher"
t3_calls | grep -qF "thread"$'\x1f'"start" \
&& note 'with no prompt T3 Code still started a thread'
t3_calls | grep -qxF "app"$'\x1f'"$repo_dir"$'\x1f' \
|| note 'with no prompt T3 Code was not opened on the checkout'
# ── 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'