From 23d460542be2fd24f750f6379d3002645594561e Mon Sep 17 00:00:00 2001 From: openhands Date: Mon, 9 Mar 2026 03:28:10 +0000 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20feat:=20Red-team=20agent=20runner=20?= =?UTF-8?q?=E2=80=94=20adversarial=20floor=20attack=20(#520)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds scripts/harb-evaluator/red-team.sh which: - Verifies the Anvil stack is running and deployments exist - Grants recenterAccess to account 2 (impersonating feeDestination) - Takes an Anvil snapshot as the clean baseline - Computes ethPerToken before the agent run (mirrors floor.ts logic) - Builds a self-contained prompt with contract addresses, account keys, protocol mechanics, copy-paste cast command patterns, snapshot/revert instructions, and structured rules for the agent - Spawns `claude -p --dangerously-skip-permissions` with a 2-hour timeout - Captures output to tmp/red-team-report.txt - Computes ethPerToken after the agent run and reports pass/fail Exit code 0 = floor held, exit code 1 = floor broken, exit code 2 = infra error. Co-Authored-By: Claude Sonnet 4.6 --- scripts/harb-evaluator/red-team.sh | 446 +++++++++++++++++++++++++++++ 1 file changed, 446 insertions(+) create mode 100755 scripts/harb-evaluator/red-team.sh diff --git a/scripts/harb-evaluator/red-team.sh b/scripts/harb-evaluator/red-team.sh new file mode 100755 index 0000000..adf0f35 --- /dev/null +++ b/scripts/harb-evaluator/red-team.sh @@ -0,0 +1,446 @@ +#!/usr/bin/env bash +# red-team.sh — Adversarial floor-attack agent runner. +# +# Spawns a Claude sub-agent with tools and a goal: make ethPerToken() decrease. +# The agent iterates freely — snapshot → strategy → check floor → revert → repeat. +# +# Usage: red-team.sh +# +# Exit codes: +# 0 floor held (no confirmed decrease) +# 1 floor broken (agent found a strategy that decreased ethPerToken) +# 2 infra error (stack not running, missing dependency, etc.) +# +# Environment overrides: +# CLAUDE_TIMEOUT seconds for the agent run (default: 7200) +# RPC_URL Anvil RPC endpoint (default: http://localhost:8545) + +set -euo pipefail + +CAST=/home/debian/.foundry/bin/cast +RPC_URL="${RPC_URL:-http://localhost:8545}" +CLAUDE_TIMEOUT="${CLAUDE_TIMEOUT:-7200}" +REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +REPORT_DIR="$REPO_ROOT/tmp" +REPORT="$REPORT_DIR/red-team-report.txt" +DEPLOYMENTS="$REPO_ROOT/onchain/deployments-local.json" + +# ── Anvil accounts ───────────────────────────────────────────────────────────── +# Account 8 — adversary (10k ETH, 0 KRK) +ADV_PK=0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97 +# Account 2 — recenter caller (granted recenterAccess by bootstrap) +RECENTER_PK=0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a + +# ── Infrastructure constants ─────────────────────────────────────────────────── +WETH=0x4200000000000000000000000000000000000006 +SWAP_ROUTER=0x94cC0AaC535CCDB3C01d6787D6413C739ae12bc4 +V3_FACTORY=0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24 +NPM=0x27F971cb582BF9E50F397e4d29a5C7A34f11faA2 +POOL_FEE=10000 +FEE_DEST_CONST=0xf6a3eef9088A255c32b6aD2025f83E57291D9011 + +# ── Logging helpers ──────────────────────────────────────────────────────────── +log() { echo "[red-team] $*"; } +die() { echo "[red-team] ERROR: $*" >&2; exit 2; } + +# ── Prerequisites ────────────────────────────────────────────────────────────── +command -v "$CAST" &>/dev/null || die "cast not found at $CAST" +command -v claude &>/dev/null || die "claude CLI not found (install: npm i -g @anthropic-ai/claude-code)" +command -v python3 &>/dev/null || die "python3 not found" +command -v jq &>/dev/null || die "jq not found" + +# ── 1. Verify stack is running ───────────────────────────────────────────────── +log "Verifying Anvil is accessible at $RPC_URL ..." +"$CAST" chain-id --rpc-url "$RPC_URL" >/dev/null 2>&1 \ + || die "Anvil not accessible at $RPC_URL — run: ./scripts/dev.sh start" + +# ── 2. Read contract addresses ───────────────────────────────────────────────── +[[ -f "$DEPLOYMENTS" ]] || die "deployments-local.json not found at $DEPLOYMENTS (bootstrap not complete)" + +KRK=$(jq -r '.contracts.Kraiken' "$DEPLOYMENTS") +STAKE=$(jq -r '.contracts.Stake' "$DEPLOYMENTS") +LM=$(jq -r '.contracts.LiquidityManager' "$DEPLOYMENTS") +OPT=$(jq -r '.contracts.OptimizerProxy' "$DEPLOYMENTS") + +for var in KRK STAKE LM OPT; do + val="${!var}" + [[ -n "$val" && "$val" != "null" ]] \ + || die "$var address missing from deployments-local.json — was bootstrap successful?" +done + +log " KRK: $KRK" +log " STAKE: $STAKE" +log " LM: $LM" +log " OPT: $OPT" + +# Derive Anvil account addresses from their private keys +ADV_ADDR=$("$CAST" wallet address --private-key "$ADV_PK") +RECENTER_ADDR=$("$CAST" wallet address --private-key "$RECENTER_PK") +log " Adversary: $ADV_ADDR (account 8)" +log " Recenter: $RECENTER_ADDR (account 2)" + +# Get Uniswap V3 Pool address +POOL=$("$CAST" call "$V3_FACTORY" "getPool(address,address,uint24)(address)" \ + "$WETH" "$KRK" "$POOL_FEE" --rpc-url "$RPC_URL") +log " Pool: $POOL" + +# ── 3. Take Anvil snapshot (clean baseline) ──────────────────────────────────── +log "Taking Anvil snapshot..." +SNAP=$("$CAST" rpc anvil_snapshot --rpc-url "$RPC_URL" | tr -d '"') +log " Snapshot ID: $SNAP" + +# ── 4. Grant recenterAccess to account 2 (if not already) ───────────────────── +# LM.recenterAccess is a single slot — we replace it with account 2. +# The fee destination is the only address that can call setRecenterAccess(). +log "Granting recenterAccess to account 2 ($RECENTER_ADDR) ..." +"$CAST" rpc --rpc-url "$RPC_URL" anvil_impersonateAccount "$FEE_DEST_CONST" >/dev/null 2>&1 +"$CAST" send --rpc-url "$RPC_URL" --from "$FEE_DEST_CONST" --unlocked \ + "$LM" "setRecenterAccess(address)" "$RECENTER_ADDR" >/dev/null 2>&1 \ + || log " WARNING: setRecenterAccess failed — account 2 may already hold access or differ" +"$CAST" rpc --rpc-url "$RPC_URL" anvil_stopImpersonatingAccount "$FEE_DEST_CONST" >/dev/null 2>&1 + +# ── Helper: compute ethPerToken (mirrors floor.ts getEthPerToken) ────────────── +# ethPerToken = (lm_native_eth + lm_weth) * 1e18 / adjusted_outstanding_supply +# adjusted_supply = outstandingSupply() - KRK_at_feeDestination - KRK_at_stakingPool +compute_eth_per_token() { + local lm_eth lm_weth supply fee_bal stake_bal adj_supply + + lm_eth=$("$CAST" balance "$LM" --rpc-url "$RPC_URL" 2>/dev/null | tr -d '[:space:]') + lm_weth=$("$CAST" call "$WETH" "balanceOf(address)(uint256)" "$LM" \ + --rpc-url "$RPC_URL" 2>/dev/null | tr -d '[:space:]') + supply=$("$CAST" call "$KRK" "outstandingSupply()(uint256)" \ + --rpc-url "$RPC_URL" 2>/dev/null | tr -d '[:space:]') + + # Fee destination: read from contract (set at deploy time, may differ per fork) + local fee_dest + fee_dest=$("$CAST" call "$LM" "feeDestination()(address)" \ + --rpc-url "$RPC_URL" 2>/dev/null | tr -d '[:space:]') + + fee_bal=0 + local zero="0x0000000000000000000000000000000000000000" + if [[ "${fee_dest,,}" != "${zero,,}" ]]; then + fee_bal=$("$CAST" call "$KRK" "balanceOf(address)(uint256)" "$fee_dest" \ + --rpc-url "$RPC_URL" 2>/dev/null | tr -d '[:space:]' || echo 0) + fi + + # Staking pool: use the deployed Stake address (mirrors peripheryContracts()[1]) + stake_bal=$("$CAST" call "$KRK" "balanceOf(address)(uint256)" "$STAKE" \ + --rpc-url "$RPC_URL" 2>/dev/null | tr -d '[:space:]' || echo 0) + + python3 - <0 else 0, 'wei/token')" +\`\`\` + +### Wrap ETH to WETH +\`\`\`bash +/home/debian/.foundry/bin/cast send ${WETH} "deposit()" --value 100ether \ + --private-key ${ADV_PK} --rpc-url http://localhost:8545 +\`\`\` + +### Approve token spend +\`\`\`bash +/home/debian/.foundry/bin/cast send "approve(address,uint256)" \ + 115792089237316195423570985008687907853269984665640564039457584007913129639935 \ + --private-key ${ADV_PK} --rpc-url http://localhost:8545 +\`\`\` + +### Buy KRK (WETH → KRK via SwapRouter) +\`\`\`bash +# Must wrap ETH and approve WETH first +/home/debian/.foundry/bin/cast send ${SWAP_ROUTER} \ + "exactInputSingle((address,address,uint24,address,uint256,uint256,uint160))" \ + "(${WETH},${KRK},${POOL_FEE},${ADV_ADDR},,0,0)" \ + --private-key ${ADV_PK} --rpc-url http://localhost:8545 +\`\`\` + +### Sell KRK (KRK → WETH via SwapRouter) +\`\`\`bash +# Must approve KRK first +/home/debian/.foundry/bin/cast send ${SWAP_ROUTER} \ + "exactInputSingle((address,address,uint24,address,uint256,uint256,uint160))" \ + "(${KRK},${WETH},${POOL_FEE},${ADV_ADDR},,0,0)" \ + --private-key ${ADV_PK} --rpc-url http://localhost:8545 +\`\`\` + +### Stake KRK (snatch with no snatching) +\`\`\`bash +# Approve KRK to Stake first +/home/debian/.foundry/bin/cast send ${STAKE} \ + "snatch(uint256,address,uint32,uint256[])" \ + ${ADV_ADDR} 0 "[]" \ + --private-key ${ADV_PK} --rpc-url http://localhost:8545 +\`\`\` + +### Unstake KRK +\`\`\`bash +/home/debian/.foundry/bin/cast send ${STAKE} \ + "exitPosition(uint256)" \ + --private-key ${ADV_PK} --rpc-url http://localhost:8545 +\`\`\` + +### Trigger recenter (account 2 only) +\`\`\`bash +/home/debian/.foundry/bin/cast send ${LM} "recenter()" \ + --private-key ${RECENTER_PK} --rpc-url http://localhost:8545 +\`\`\` + +### Read KRK balance +\`\`\`bash +/home/debian/.foundry/bin/cast call ${KRK} "balanceOf(address)(uint256)" ${ADV_ADDR} \ + --rpc-url http://localhost:8545 +\`\`\` + +### Read ETH balance +\`\`\`bash +/home/debian/.foundry/bin/cast balance ${ADV_ADDR} --rpc-url http://localhost:8545 +\`\`\` + +### Add LP position via NPM (mint) +\`\`\`bash +# Must approve both tokens to NPM first. tickLower/tickUpper must be multiples of 200 (pool tickSpacing). +/home/debian/.foundry/bin/cast send ${NPM} \ + "mint((address,address,uint24,int24,int24,uint256,uint256,uint256,uint256,address,uint256))" \ + "(${WETH},${KRK},${POOL_FEE},,,,,0,0,${ADV_ADDR},)" \ + --private-key ${ADV_PK} --rpc-url http://localhost:8545 +\`\`\` + +### Remove LP position via NPM (decreaseLiquidity then collect) +\`\`\`bash +/home/debian/.foundry/bin/cast send ${NPM} \ + "decreaseLiquidity((uint256,uint128,uint256,uint256,uint256))" \ + "(,,0,0,)" \ + --private-key ${ADV_PK} --rpc-url http://localhost:8545 + +/home/debian/.foundry/bin/cast send ${NPM} \ + "collect((uint256,address,uint128,uint128))" \ + "(,${ADV_ADDR},340282366920938463463374607431768211455,340282366920938463463374607431768211455)" \ + --private-key ${ADV_PK} --rpc-url http://localhost:8545 +\`\`\` + +### Mine a block +\`\`\`bash +/home/debian/.foundry/bin/cast rpc evm_mine --rpc-url http://localhost:8545 +\`\`\` + +### Snapshot and revert (for resetting between strategies) +\`\`\`bash +# Take snapshot (returns ID — save it): +SNAP=\$(/home/debian/.foundry/bin/cast rpc anvil_snapshot --rpc-url http://localhost:8545 | tr -d '"') +# Revert to snapshot (one-shot — take a new snapshot immediately after): +/home/debian/.foundry/bin/cast rpc anvil_revert \$SNAP --rpc-url http://localhost:8545 +\`\`\` + +--- + +## Rules + +1. You have 10 000 ETH. Start by wrapping some if you need WETH for swaps. +2. Your goal is to make ethPerToken DECREASE vs the starting value (${FLOOR_BEFORE} wei/token). +3. Try at least 3 distinct strategies. After each attempt: + a. Run the floor-check command above to measure ethPerToken. + b. If the floor DECREASED — report this as a SUCCESS and describe the exact steps. + c. If the floor held or INCREASED — revert to the snapshot and try a new strategy. + Remember: \`anvil_revert\` is one-shot. Take a new snapshot immediately after reverting. +4. You may chain multiple actions in one strategy (e.g. large buy → recenter → large sell). +5. Be methodical. Report every strategy tried even if it failed. + +--- + +## Final report format + +After trying all strategies, output a clearly structured report: + +\`\`\` +=== RED-TEAM REPORT === + +floor_before: wei/token + +STRATEGY 1: + Steps: + floor_after: + Result: INCREASED / HELD / DECREASED + +STRATEGY 2: ... +... + +=== CONCLUSION === +Floor broken: YES / NO +Winning strategy: +floor_before: ${FLOOR_BEFORE} wei/token +floor_after: +\`\`\` +PROMPT_EOF +) + +# ── 7. Create output directory and run the agent ─────────────────────────────── +mkdir -p "$REPORT_DIR" + +log "Spawning Claude red-team agent (timeout: ${CLAUDE_TIMEOUT}s)..." +log " Report will be written to: $REPORT" + +set +e +CLAUDE_TIMEOUT="$CLAUDE_TIMEOUT" claude -p --dangerously-skip-permissions \ + "$PROMPT" >"$REPORT" 2>&1 +AGENT_EXIT=$? +set -e + +if [[ $AGENT_EXIT -ne 0 ]]; then + log "WARNING: claude exited with code $AGENT_EXIT — see $REPORT for details" +fi + +# ── 8. Read floor_after ──────────────────────────────────────────────────────── +log "Reading floor after agent run..." +FLOOR_AFTER=$(compute_eth_per_token) +log " floor_after = $FLOOR_AFTER wei/token" + +# ── 9. Summarise results ─────────────────────────────────────────────────────── +log "" +log "=== RED-TEAM SUMMARY ===" +log "" +log " floor_before : $FLOOR_BEFORE wei/token" +log " floor_after : $FLOOR_AFTER wei/token" +log "" + +BROKE=false +if python3 -c "import sys; sys.exit(0 if int('$FLOOR_AFTER') < int('$FLOOR_BEFORE') else 1)"; then + BROKE=true +fi + +if [[ "$BROKE" == "true" ]]; then + DELTA=$(python3 -c "print($FLOOR_BEFORE - $FLOOR_AFTER)") + log " RESULT: FLOOR BROKEN ❌" + log " Decrease: $DELTA wei/token" + log "" + log " See $REPORT for the winning strategy." + log "" + # Append a machine-readable summary to the report + cat >>"$REPORT" <>"$REPORT" < Date: Mon, 9 Mar 2026 03:59:12 +0000 Subject: [PATCH 2/2] fix: address review findings in red-team.sh (#520) - Move snapshot to after setRecenterAccess so agent reverts restore recenterAccess for account 2 on every retry - Read feeDestination() dynamically from LM (removes hardcoded constant) and add || die guards on impersonation calls - Add EXIT/INT/TERM cleanup trap that reverts to the baseline snapshot - Fix agent floor-check snippet: add FEE_DEST/FEE_BAL reads so formula matches compute_eth_per_token (adj=s-f-k, not adj=s-k) - Use `timeout "$CLAUDE_TIMEOUT"` to enforce wall-clock process limit - Correct taxRateIndex range: 0-29 (30-element TAX_RATES array) - Fix outstandingSupply() description: excludes LM-held KRK, not all KRK Co-Authored-By: Claude Sonnet 4.6 --- scripts/harb-evaluator/red-team.sh | 51 ++++++++++++++++++++---------- 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/scripts/harb-evaluator/red-team.sh b/scripts/harb-evaluator/red-team.sh index adf0f35..082f220 100755 --- a/scripts/harb-evaluator/red-team.sh +++ b/scripts/harb-evaluator/red-team.sh @@ -37,7 +37,6 @@ SWAP_ROUTER=0x94cC0AaC535CCDB3C01d6787D6413C739ae12bc4 V3_FACTORY=0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24 NPM=0x27F971cb582BF9E50F397e4d29a5C7A34f11faA2 POOL_FEE=10000 -FEE_DEST_CONST=0xf6a3eef9088A255c32b6aD2025f83E57291D9011 # ── Logging helpers ──────────────────────────────────────────────────────────── log() { echo "[red-team] $*"; } @@ -84,20 +83,37 @@ POOL=$("$CAST" call "$V3_FACTORY" "getPool(address,address,uint24)(address)" \ "$WETH" "$KRK" "$POOL_FEE" --rpc-url "$RPC_URL") log " Pool: $POOL" -# ── 3. Take Anvil snapshot (clean baseline) ──────────────────────────────────── +# ── 3. Grant recenterAccess to account 2 ────────────────────────────────────── +# Done BEFORE the snapshot so every revert restores account 2's access. +# LM.recenterAccess is a single address slot — replace it with account 2. +# Only the feeDestination is authorised to call setRecenterAccess(). +log "Granting recenterAccess to account 2 ($RECENTER_ADDR) ..." +FEE_DEST=$("$CAST" call "$LM" "feeDestination()(address)" --rpc-url "$RPC_URL") \ + || die "Failed to read feeDestination() from LM" +FEE_DEST=$(echo "$FEE_DEST" | tr -d '[:space:]') +"$CAST" rpc --rpc-url "$RPC_URL" anvil_impersonateAccount "$FEE_DEST" \ + || die "anvil_impersonateAccount $FEE_DEST failed" +"$CAST" send --rpc-url "$RPC_URL" --from "$FEE_DEST" --unlocked \ + "$LM" "setRecenterAccess(address)" "$RECENTER_ADDR" >/dev/null 2>&1 \ + || die "setRecenterAccess($RECENTER_ADDR) failed — check that feeDestination is correct" +"$CAST" rpc --rpc-url "$RPC_URL" anvil_stopImpersonatingAccount "$FEE_DEST" \ + || die "anvil_stopImpersonatingAccount $FEE_DEST failed" +log " recenterAccess granted" + +# ── 4. Take Anvil snapshot (clean baseline, includes recenterAccess grant) ───── log "Taking Anvil snapshot..." SNAP=$("$CAST" rpc anvil_snapshot --rpc-url "$RPC_URL" | tr -d '"') log " Snapshot ID: $SNAP" -# ── 4. Grant recenterAccess to account 2 (if not already) ───────────────────── -# LM.recenterAccess is a single slot — we replace it with account 2. -# The fee destination is the only address that can call setRecenterAccess(). -log "Granting recenterAccess to account 2 ($RECENTER_ADDR) ..." -"$CAST" rpc --rpc-url "$RPC_URL" anvil_impersonateAccount "$FEE_DEST_CONST" >/dev/null 2>&1 -"$CAST" send --rpc-url "$RPC_URL" --from "$FEE_DEST_CONST" --unlocked \ - "$LM" "setRecenterAccess(address)" "$RECENTER_ADDR" >/dev/null 2>&1 \ - || log " WARNING: setRecenterAccess failed — account 2 may already hold access or differ" -"$CAST" rpc --rpc-url "$RPC_URL" anvil_stopImpersonatingAccount "$FEE_DEST_CONST" >/dev/null 2>&1 +# Revert to the baseline snapshot on exit so subsequent runs start clean. +cleanup() { + local rc=$? + if [[ -n "${SNAP:-}" ]]; then + "$CAST" rpc anvil_revert "$SNAP" --rpc-url "$RPC_URL" >/dev/null 2>&1 || true + fi + exit $rc +} +trap cleanup EXIT INT TERM # ── Helper: compute ethPerToken (mirrors floor.ts getEthPerToken) ────────────── # ethPerToken = (lm_native_eth + lm_weth) * 1e18 / adjusted_outstanding_supply @@ -216,13 +232,14 @@ Only recenterAccess account can call it. ### Staking \`Stake.snatch(assets, receiver, taxRateIndex, positionsToSnatch)\` -- taxRateIndex: 0–4 (index into TAX_RATES array — not a raw percentage) +- taxRateIndex: 0–29 (index into the 30-element TAX_RATES array — not a raw percentage) - KRK staked is held by the Stake contract (excluded from adjusted_supply) - KRK in Stake does NOT count against the floor denominator ### outstandingSupply() vs totalSupply() -\`KRK.outstandingSupply()\` ≈ totalSupply but counts all KRK regardless of where it is. -The floor helper subtracts Stake and feeDestination balances to get adjusted_supply. +\`KRK.outstandingSupply() = totalSupply() - balanceOf(liquidityManager)\` +LM-held KRK (in pool positions) is excluded from outstandingSupply. +The floor formula then additionally subtracts KRK at Stake and feeDestination to get adjusted_supply. --- @@ -233,8 +250,10 @@ The floor helper subtracts Stake and feeDestination balances to get adjusted_sup LM_ETH=\$(/home/debian/.foundry/bin/cast balance ${LM} --rpc-url http://localhost:8545) LM_WETH=\$(/home/debian/.foundry/bin/cast call ${WETH} "balanceOf(address)(uint256)" ${LM} --rpc-url http://localhost:8545) SUPPLY=\$(/home/debian/.foundry/bin/cast call ${KRK} "outstandingSupply()(uint256)" --rpc-url http://localhost:8545) +FEE_DEST=\$(/home/debian/.foundry/bin/cast call ${LM} "feeDestination()(address)" --rpc-url http://localhost:8545) +FEE_BAL=\$(/home/debian/.foundry/bin/cast call ${KRK} "balanceOf(address)(uint256)" \$FEE_DEST --rpc-url http://localhost:8545) STAKE_BAL=\$(/home/debian/.foundry/bin/cast call ${KRK} "balanceOf(address)(uint256)" ${STAKE} --rpc-url http://localhost:8545) -python3 -c "e=\$LM_ETH; w=\$LM_WETH; s=\$SUPPLY; k=\$STAKE_BAL; adj=s-k; print('ethPerToken:', (e+w)*10**18//adj if adj>0 else 0, 'wei/token')" +python3 -c "e=\$LM_ETH; w=\$LM_WETH; s=\$SUPPLY; f=\$FEE_BAL; k=\$STAKE_BAL; adj=s-f-k; print('ethPerToken:', (e+w)*10**18//adj if adj>0 else 0, 'wei/token')" \`\`\` ### Wrap ETH to WETH @@ -385,7 +404,7 @@ log "Spawning Claude red-team agent (timeout: ${CLAUDE_TIMEOUT}s)..." log " Report will be written to: $REPORT" set +e -CLAUDE_TIMEOUT="$CLAUDE_TIMEOUT" claude -p --dangerously-skip-permissions \ +timeout "$CLAUDE_TIMEOUT" claude -p --dangerously-skip-permissions \ "$PROMPT" >"$REPORT" 2>&1 AGENT_EXIT=$? set -e