2026-01-11 10:20:50 +00:00
#!/usr/bin/env bash
set -euo pipefail
2026-03-08 19:10:48 +03:00
SCRIPT_DIR = " $( cd " $( dirname " ${ BASH_SOURCE [0] } " ) " && pwd ) "
VERIFY_HELPER_PATH = "/usr/local/install-sh-common/version-parse.sh"
if [ [ ! -f " $VERIFY_HELPER_PATH " ] ] ; then
VERIFY_HELPER_PATH = " ${ SCRIPT_DIR } /../install-sh-common/version-parse.sh "
fi
# shellcheck source=../install-sh-common/version-parse.sh
source " $VERIFY_HELPER_PATH "
2026-03-22 22:13:01 -07:00
INSTALL_URL = " ${ OPENCLAW_INSTALL_URL :- https : //openclaw.bot/install.sh } "
MODELS_MODE = " ${ OPENCLAW_E2E_MODELS :- both } " # both|openai|anthropic
INSTALL_TAG = " ${ OPENCLAW_INSTALL_TAG :- latest } "
E2E_PREVIOUS_VERSION = " ${ OPENCLAW_INSTALL_E2E_PREVIOUS :- } "
SKIP_PREVIOUS = " ${ OPENCLAW_INSTALL_E2E_SKIP_PREVIOUS :- 0 } "
2026-01-14 00:56:34 +00:00
OPENAI_API_KEY = " ${ OPENAI_API_KEY :- } "
ANTHROPIC_API_KEY = " ${ ANTHROPIC_API_KEY :- } "
ANTHROPIC_API_TOKEN = " ${ ANTHROPIC_API_TOKEN :- } "
2026-01-11 10:20:50 +00:00
2026-04-05 07:13:59 +01:00
# This image runs as a non-root user, so seed a user-local npm prefix before we
# preinstall an older global version to exercise the upgrade path.
export NPM_CONFIG_PREFIX = " ${ NPM_CONFIG_PREFIX :- $HOME /.npm-global } "
mkdir -p " $NPM_CONFIG_PREFIX "
export PATH = " $NPM_CONFIG_PREFIX /bin: $PATH "
2026-01-11 10:20:50 +00:00
if [ [ " $MODELS_MODE " != "both" && " $MODELS_MODE " != "openai" && " $MODELS_MODE " != "anthropic" ] ] ; then
2026-01-30 03:15:10 +01:00
echo "ERROR: OPENCLAW_E2E_MODELS must be one of: both|openai|anthropic" >& 2
2026-01-11 10:20:50 +00:00
exit 2
fi
if [ [ " $MODELS_MODE " = = "both" ] ] ; then
2026-01-14 00:56:34 +00:00
if [ [ -z " $OPENAI_API_KEY " ] ] ; then
2026-01-30 03:15:10 +01:00
echo "ERROR: OPENCLAW_E2E_MODELS=both requires OPENAI_API_KEY." >& 2
2026-01-11 10:20:50 +00:00
exit 2
fi
2026-01-14 00:56:34 +00:00
if [ [ -z " $ANTHROPIC_API_TOKEN " && -z " $ANTHROPIC_API_KEY " ] ] ; then
2026-01-30 03:15:10 +01:00
echo "ERROR: OPENCLAW_E2E_MODELS=both requires ANTHROPIC_API_TOKEN or ANTHROPIC_API_KEY." >& 2
2026-01-14 00:56:34 +00:00
exit 2
fi
elif [ [ " $MODELS_MODE " = = "openai" && -z " $OPENAI_API_KEY " ] ] ; then
2026-01-30 03:15:10 +01:00
echo "ERROR: OPENCLAW_E2E_MODELS=openai requires OPENAI_API_KEY." >& 2
2026-01-11 10:20:50 +00:00
exit 2
2026-01-14 00:56:34 +00:00
elif [ [ " $MODELS_MODE " = = "anthropic" && -z " $ANTHROPIC_API_TOKEN " && -z " $ANTHROPIC_API_KEY " ] ] ; then
2026-01-30 03:15:10 +01:00
echo "ERROR: OPENCLAW_E2E_MODELS=anthropic requires ANTHROPIC_API_TOKEN or ANTHROPIC_API_KEY." >& 2
2026-01-11 10:20:50 +00:00
exit 2
fi
echo "==> Resolve npm versions"
2026-01-30 03:15:10 +01:00
EXPECTED_VERSION = " $( npm view " openclaw@ ${ INSTALL_TAG } " version) "
2026-01-18 08:29:56 +00:00
if [ [ -z " $EXPECTED_VERSION " || " $EXPECTED_VERSION " = = "undefined" || " $EXPECTED_VERSION " = = "null" ] ] ; then
2026-01-30 03:15:10 +01:00
echo " ERROR: unable to resolve openclaw@ ${ INSTALL_TAG } version " >& 2
2026-01-18 08:29:56 +00:00
exit 2
fi
2026-01-14 00:38:26 +00:00
if [ [ -n " $E2E_PREVIOUS_VERSION " ] ] ; then
PREVIOUS_VERSION = " $E2E_PREVIOUS_VERSION "
else
PREVIOUS_VERSION = " $( node - <<'NODE'
2026-01-11 10:20:50 +00:00
const { execSync } = require("node:child_process");
2026-01-30 03:15:10 +01:00
const versions = JSON.parse(execSync("npm view openclaw versions --json", { encoding: "utf8" }));
2026-01-11 10:20:50 +00:00
if (!Array.isArray(versions) || versions.length === 0) process.exit(1);
process.stdout.write(versions.length >= 2 ? versions[versions.length - 2] : versions[0]);
NODE
2026-01-14 00:38:26 +00:00
) "
fi
2026-01-18 08:29:56 +00:00
echo " expected= $EXPECTED_VERSION previous= $PREVIOUS_VERSION "
2026-01-11 10:20:50 +00:00
2026-01-14 00:38:26 +00:00
if [ [ " $SKIP_PREVIOUS " = = "1" ] ] ; then
2026-01-30 03:15:10 +01:00
echo "==> Skip preinstall previous (OPENCLAW_INSTALL_E2E_SKIP_PREVIOUS=1)"
2026-01-14 00:38:26 +00:00
else
echo "==> Preinstall previous (forces installer upgrade path; avoids read() prompt)"
2026-01-30 03:15:10 +01:00
npm install -g " openclaw@ ${ PREVIOUS_VERSION } "
2026-01-14 00:38:26 +00:00
fi
2026-01-11 10:20:50 +00:00
echo "==> Run official installer one-liner"
2026-01-18 08:29:56 +00:00
if [ [ " $INSTALL_TAG " = = "beta" ] ] ; then
2026-03-22 22:13:01 -07:00
OPENCLAW_BETA = 1 curl -fsSL " $INSTALL_URL " | bash
2026-01-18 08:29:56 +00:00
elif [ [ " $INSTALL_TAG " != "latest" ] ] ; then
2026-03-22 22:13:01 -07:00
OPENCLAW_VERSION = " $INSTALL_TAG " curl -fsSL " $INSTALL_URL " | bash
2026-01-18 08:29:56 +00:00
else
curl -fsSL " $INSTALL_URL " | bash
fi
2026-01-11 10:20:50 +00:00
echo "==> Verify installed version"
2026-01-30 03:15:10 +01:00
INSTALLED_VERSION = " $( openclaw --version 2>/dev/null | head -n 1 | tr -d '\r' ) "
2026-03-08 19:10:48 +03:00
INSTALLED_VERSION = " $( extract_openclaw_semver " $INSTALLED_VERSION " ) "
2026-01-18 08:29:56 +00:00
echo " installed= $INSTALLED_VERSION expected= $EXPECTED_VERSION "
if [ [ " $INSTALLED_VERSION " != " $EXPECTED_VERSION " ] ] ; then
2026-01-30 03:15:10 +01:00
echo " ERROR: expected openclaw@ $EXPECTED_VERSION , got openclaw@ $INSTALLED_VERSION " >& 2
2026-01-11 10:20:50 +00:00
exit 1
fi
set_image_model( ) {
local profile = " $1 "
shift
local candidate
for candidate in " $@ " ; do
2026-01-30 03:15:10 +01:00
if openclaw --profile " $profile " models set-image " $candidate " >/dev/null 2>& 1; then
2026-01-11 10:20:50 +00:00
echo " $candidate "
return 0
fi
done
echo " ERROR: could not set an image model (tried: $* ) " >& 2
return 1
}
set_agent_model( ) {
local profile = " $1 "
local candidate
shift
for candidate in " $@ " ; do
2026-01-30 03:15:10 +01:00
if openclaw --profile " $profile " models set " $candidate " >/dev/null 2>& 1; then
2026-01-11 10:20:50 +00:00
echo " $candidate "
return 0
fi
done
echo " ERROR: could not set agent model (tried: $* ) " >& 2
return 1
}
write_png_lr_rg( ) {
local out = " $1 "
node - <<'NODE' "$out"
const fs = require("node:fs");
const zlib = require("node:zlib");
const out = process.argv[2];
const width = 96;
const height = 64;
const crcTable = (() => {
const table = new Uint32Array(256);
for (let i = 0; i < 256; i++) {
let c = i;
for (let k = 0; k < 8; k++) c = (c & 1) ? (0xedb88320 ^ (c >>> 1)) : (c >>> 1);
table[i] = c >>> 0;
}
return table;
})();
function crc32(buf) {
let c = 0xffffffff;
for (let i = 0; i < buf.length; i++) c = crcTable[(c ^ buf[i]) & 0xff] ^ (c >>> 8);
return (c ^ 0xffffffff) >>> 0;
}
function chunk(type, data) {
const typeBuf = Buffer.from(type, "ascii");
const len = Buffer.alloc(4);
len.writeUInt32BE(data.length, 0);
const crcBuf = Buffer.alloc(4);
crcBuf.writeUInt32BE(crc32(Buffer.concat([typeBuf, data])), 0);
return Buffer.concat([len, typeBuf, data, crcBuf]);
}
const sig = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
const ihdr = Buffer.alloc(13);
ihdr.writeUInt32BE(width, 0);
ihdr.writeUInt32BE(height, 4);
ihdr[8] = 8; // bit depth
ihdr[9] = 2; // color type: truecolor
ihdr[10] = 0; // compression
ihdr[11] = 0; // filter
ihdr[12] = 0; // interlace
const rows = [];
for (let y = 0; y < height; y++) {
const row = Buffer.alloc(1 + width * 3);
row[0] = 0; // filter: none
for (let x = 0; x < width; x++) {
const i = 1 + x * 3;
const left = x < width / 2;
row[i + 0] = left ? 255 : 0;
row[i + 1] = left ? 0 : 255;
row[i + 2] = 0;
}
rows.push(row);
}
const raw = Buffer.concat(rows);
const idat = zlib.deflateSync(raw, { level: 9 });
const png = Buffer.concat([
sig,
chunk("IHDR", ihdr),
chunk("IDAT", idat),
chunk("IEND", Buffer.alloc(0)),
]);
fs.writeFileSync(out, png);
NODE
}
run_agent_turn( ) {
local profile = " $1 "
local session_id = " $2 "
local prompt = " $3 "
local out_json = " $4 "
2026-04-05 07:13:59 +01:00
# Installer E2E validates install + onboard + embedded agent tooling. It does
# not need a paired Gateway control-plane hop, which is flaky/non-deterministic
# in the isolated container and already covered by gateway-specific lanes.
2026-01-30 03:15:10 +01:00
openclaw --profile " $profile " agent \
2026-04-05 07:13:59 +01:00
--local \
2026-01-11 10:20:50 +00:00
--session-id " $session_id " \
--message " $prompt " \
--thinking off \
2026-04-05 07:13:59 +01:00
--json >" $out_json " 2>& 1
node - <<'NODE' "$out_json"
const fs = require("node:fs");
const path = process.argv[2];
const raw = fs.readFileSync(path, "utf8");
function extractTrailingJsonObject(input) {
const trimmed = input.trim();
if (!trimmed) {
throw new Error("agent output was empty");
}
try {
return JSON.parse(trimmed);
} catch {
// Some local runs emit stderr diagnostics before the final JSON payload.
// Walk backward and keep the last parseable top-level object.
for (let index = trimmed.lastIndexOf("{"); index >= 0; index = trimmed.lastIndexOf("{", index - 1)) {
const candidate = trimmed.slice(index);
try {
return JSON.parse(candidate);
} catch {
// keep scanning
}
}
throw new Error(`could not extract JSON payload from agent output:\n${trimmed}`);
}
}
const parsed = extractTrailingJsonObject(raw);
fs.writeFileSync(path, `${JSON.stringify(parsed, null, 2)}\n`, "utf8");
NODE
2026-01-11 10:20:50 +00:00
}
assert_agent_json_has_text( ) {
local path = " $1 "
node - <<'NODE' "$path"
const fs = require("node:fs");
const p = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
const payloads =
Array.isArray(p?.result?.payloads) ? p.result.payloads :
Array.isArray(p?.payloads) ? p.payloads :
[];
const texts = payloads.map((x) => String(x?.text ?? "").trim()).filter(Boolean);
if (texts.length === 0) process.exit(1);
NODE
}
assert_agent_json_ok( ) {
local json_path = " $1 "
local expect_provider = " $2 "
node - <<'NODE' "$json_path" "$expect_provider"
const fs = require("node:fs");
const jsonPath = process.argv[2];
const expectProvider = process.argv[3];
const p = JSON.parse(fs.readFileSync(jsonPath, "utf8"));
if (typeof p?.status === "string" && p.status !== "ok" && p.status !== "accepted") {
console.error(`ERROR: gateway status=${p.status}`);
process.exit(1);
}
const result = p?.result ?? p;
const payloads = Array.isArray(result?.payloads) ? result.payloads : [];
const anyError = payloads.some((pl) => pl && pl.isError === true);
const combinedText = payloads.map((pl) => String(pl?.text ?? "")).filter(Boolean).join("\n").trim();
if (anyError) {
console.error(`ERROR: agent returned error payload: ${combinedText}`);
process.exit(1);
}
if (/rate_limit_error/i.test(combinedText) || /^429\\b/.test(combinedText)) {
console.error(`ERROR: agent rate limited: ${combinedText}`);
process.exit(1);
}
const meta = result?.meta;
const provider =
(typeof meta?.agentMeta?.provider === "string" && meta.agentMeta.provider.trim()) ||
(typeof meta?.provider === "string" && meta.provider.trim()) ||
"";
if (expectProvider && provider && provider !== expectProvider) {
console.error(`ERROR: expected provider=${expectProvider}, got provider=${provider}`);
process.exit(1);
}
NODE
}
2026-01-18 08:33:45 +00:00
extract_matching_text( ) {
2026-01-11 10:20:50 +00:00
local path = " $1 "
2026-01-18 08:33:45 +00:00
local expected = " $2 "
node - <<'NODE' "$path" "$expected"
2026-01-11 10:20:50 +00:00
const fs = require("node:fs");
const p = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
2026-01-18 08:33:45 +00:00
const expected = String(process.argv[3] ?? "");
2026-01-11 10:20:50 +00:00
const payloads =
Array.isArray(p?.result?.payloads) ? p.result.payloads :
Array.isArray(p?.payloads) ? p.payloads :
[];
2026-01-18 08:33:45 +00:00
const texts = payloads.map((x) => String(x?.text ?? "").trim()).filter(Boolean);
const match = texts.find((text) => text === expected);
process.stdout.write(match ?? texts[0] ?? "");
2026-01-11 10:20:50 +00:00
NODE
}
assert_session_used_tools( ) {
local jsonl = " $1 "
shift
node - <<'NODE' "$jsonl" "$@"
const fs = require("node:fs");
const jsonl = process.argv[2];
const required = new Set(process.argv.slice(3));
const raw = fs.readFileSync(jsonl, "utf8");
const lines = raw.split("\n").map((l) => l.trim()).filter(Boolean);
const seen = new Set();
const toolTypes = new Set([
"tool_use",
"tool_result",
"tool",
"tool-call",
"tool_call",
"tooluse",
"tool-use",
"toolresult",
"tool-result",
]);
function walk(node, parent) {
if (!node) return;
if (Array.isArray(node)) {
for (const item of node) walk(item, node);
return;
}
if (typeof node !== "object") return;
const obj = node;
const t = typeof obj.type === "string" ? obj.type : null;
if (t && (toolTypes.has(t) || /tool/i.test(t))) {
const name =
typeof obj.name === "string" ? obj.name :
typeof obj.toolName === "string" ? obj.toolName :
typeof obj.tool_name === "string" ? obj.tool_name :
(obj.tool && typeof obj.tool.name === "string") ? obj.tool.name :
null;
if (name) seen.add(name);
}
if (typeof obj.name === "string" && typeof obj.input === "object" && obj.input) {
2026-01-12 02:49:55 +00:00
// Many tool-use blocks look like { type: "...", name: "exec", input: {...} }
2026-01-11 10:20:50 +00:00
// but some transcripts omit/rename type.
seen.add(obj.name);
}
// OpenAI-ish tool call shapes.
if (Array.isArray(obj.tool_calls)) {
for (const c of obj.tool_calls) {
const fn = c?.function;
if (fn && typeof fn.name === "string") seen.add(fn.name);
}
}
if (obj.function && typeof obj.function.name === "string") seen.add(obj.function.name);
for (const v of Object.values(obj)) walk(v, obj);
}
for (const line of lines) {
try {
const entry = JSON.parse(line);
walk(entry, null);
} catch {
// ignore unparsable lines
}
}
const missing = [...required].filter((t) => !seen.has(t));
if (missing.length > 0) {
console.error(`Missing tools in transcript: ${missing.join(", ")}`);
console.error(`Seen tools: ${[...seen].sort().join(", ")}`);
console.error("Transcript head:");
console.error(lines.slice(0, 5).join("\n"));
process.exit(1);
}
NODE
}
run_profile( ) {
local profile = " $1 "
local port = " $2 "
local workspace = " $3 "
local agent_model_provider = " $4 " # "openai"|"anthropic"
2026-01-16 11:46:56 +00:00
echo " ==> Onboard ( $profile ) "
if [ [ " $agent_model_provider " = = "openai" ] ] ; then
2026-01-30 03:15:10 +01:00
openclaw --profile " $profile " onboard \
2026-01-16 11:46:56 +00:00
--non-interactive \
--accept-risk \
--flow quickstart \
--auth-choice openai-api-key \
--openai-api-key " $OPENAI_API_KEY " \
--gateway-port " $port " \
--gateway-bind loopback \
2026-01-11 10:20:50 +00:00
--gateway-auth token \
--workspace " $workspace " \
2026-04-05 07:13:59 +01:00
--skip-health
elif [ [ -n " $ANTHROPIC_API_KEY " ] ] ; then
openclaw --profile " $profile " onboard \
--non-interactive \
--accept-risk \
--flow quickstart \
--auth-choice apiKey \
--anthropic-api-key " $ANTHROPIC_API_KEY " \
--gateway-port " $port " \
--gateway-bind loopback \
--gateway-auth token \
--workspace " $workspace " \
2026-01-11 10:20:50 +00:00
--skip-health
2026-01-16 11:46:56 +00:00
elif [ [ -n " $ANTHROPIC_API_TOKEN " ] ] ; then
2026-01-30 03:15:10 +01:00
openclaw --profile " $profile " onboard \
2026-01-16 11:46:56 +00:00
--non-interactive \
--accept-risk \
--flow quickstart \
--auth-choice token \
--token-provider anthropic \
--token " $ANTHROPIC_API_TOKEN " \
--gateway-port " $port " \
2026-01-14 00:56:34 +00:00
--gateway-bind loopback \
--gateway-auth token \
--workspace " $workspace " \
--skip-health
2026-01-16 11:46:56 +00:00
else
2026-01-30 03:15:10 +01:00
openclaw --profile " $profile " onboard \
2026-01-16 11:46:56 +00:00
--non-interactive \
--accept-risk \
--flow quickstart \
--auth-choice apiKey \
--anthropic-api-key " $ANTHROPIC_API_KEY " \
--gateway-port " $port " \
--gateway-bind loopback \
2026-01-11 10:20:50 +00:00
--gateway-auth token \
--workspace " $workspace " \
--skip-health
fi
echo " ==> Verify workspace identity files ( $profile ) "
test -f " $workspace /AGENTS.md "
test -f " $workspace /IDENTITY.md "
test -f " $workspace /USER.md "
test -f " $workspace /SOUL.md "
test -f " $workspace /TOOLS.md "
echo " ==> Configure models ( $profile ) "
local agent_model
local image_model
if [ [ " $agent_model_provider " = = "openai" ] ] ; then
agent_model = " $( set_agent_model " $profile " \
"openai/gpt-4.1-mini" \
"openai/gpt-4.1" \
"openai/gpt-4o-mini" \
"openai/gpt-4o" ) "
image_model = " $( set_image_model " $profile " \
"openai/gpt-4.1" \
"openai/gpt-4o-mini" \
"openai/gpt-4o" \
"openai/gpt-4.1-mini" ) "
else
agent_model = " $( set_agent_model " $profile " \
2026-02-05 16:54:44 -05:00
"anthropic/claude-opus-4-6" \
2026-03-31 16:40:58 +01:00
"claude-opus-4-6" ) "
2026-01-11 10:20:50 +00:00
image_model = " $( set_image_model " $profile " \
2026-02-05 16:54:44 -05:00
"anthropic/claude-opus-4-6" \
2026-03-31 16:40:58 +01:00
"claude-opus-4-6" ) "
2026-01-11 10:20:50 +00:00
fi
echo " model= $agent_model "
echo " imageModel= $image_model "
echo " ==> Prepare tool fixtures ( $profile ) "
PROOF_TXT = " $workspace /proof.txt "
PROOF_COPY = " $workspace /copy.txt "
HOSTNAME_TXT = " $workspace /hostname.txt "
IMAGE_PNG = " $workspace /proof.png "
IMAGE_TXT = " $workspace /image.txt "
SESSION_ID = " e2e-tools- ${ profile } "
2026-04-05 07:13:59 +01:00
SESSION_JSONL = " $HOME /.openclaw- ${ profile } /agents/main/sessions/ ${ SESSION_ID } .jsonl "
2026-01-11 10:20:50 +00:00
PROOF_VALUE = " $( node -e 'console.log(require("node:crypto").randomBytes(16).toString("hex"))' ) "
echo -n " $PROOF_VALUE " >" $PROOF_TXT "
write_png_lr_rg " $IMAGE_PNG "
EXPECTED_HOSTNAME = " $( cat /etc/hostname | tr -d '\r\n' ) "
echo " ==> Start gateway ( $profile ) "
GATEWAY_LOG = " $workspace /gateway.log "
2026-01-30 03:15:10 +01:00
openclaw --profile " $profile " gateway --port " $port " --bind loopback >" $GATEWAY_LOG " 2>& 1 &
2026-01-11 10:20:50 +00:00
GATEWAY_PID = " $! "
cleanup_profile( ) {
if kill -0 " $GATEWAY_PID " 2>/dev/null; then
kill " $GATEWAY_PID " 2>/dev/null || true
wait " $GATEWAY_PID " 2>/dev/null || true
fi
}
trap cleanup_profile EXIT
echo " ==> Wait for health ( $profile ) "
for _ in $( seq 1 60) ; do
2026-01-30 03:15:10 +01:00
if openclaw --profile " $profile " health --timeout 2000 --json >/dev/null 2>& 1; then
2026-01-11 10:20:50 +00:00
break
fi
sleep 0.25
done
2026-01-30 03:15:10 +01:00
openclaw --profile " $profile " health --timeout 10000 --json >/dev/null
2026-01-11 10:20:50 +00:00
echo " ==> Agent turns ( $profile ) "
TURN1_JSON = " /tmp/agent- ${ profile } -1.json "
TURN2_JSON = " /tmp/agent- ${ profile } -2.json "
2026-04-05 07:13:59 +01:00
TURN2B_JSON = " /tmp/agent- ${ profile } -2b.json "
2026-01-11 10:20:50 +00:00
TURN3_JSON = " /tmp/agent- ${ profile } -3.json "
2026-04-05 07:13:59 +01:00
TURN3B_JSON = " /tmp/agent- ${ profile } -3b.json "
2026-01-11 10:20:50 +00:00
TURN4_JSON = " /tmp/agent- ${ profile } -4.json "
run_agent_turn " $profile " " $SESSION_ID " \
2026-04-05 07:13:59 +01:00
" Use the read tool (not exec) to read ${ PROOF_TXT } . Reply with the exact contents only (no extra whitespace). " \
2026-01-11 10:20:50 +00:00
" $TURN1_JSON "
assert_agent_json_has_text " $TURN1_JSON "
assert_agent_json_ok " $TURN1_JSON " " $agent_model_provider "
local reply1
2026-01-18 08:33:45 +00:00
reply1 = " $( extract_matching_text " $TURN1_JSON " " $PROOF_VALUE " | tr -d '\r\n' ) "
2026-01-11 10:20:50 +00:00
if [ [ " $reply1 " != " $PROOF_VALUE " ] ] ; then
echo " ERROR: agent did not read proof.txt correctly ( $profile ): $reply1 " >& 2
exit 1
fi
local prompt2
2026-04-05 07:13:59 +01:00
prompt2 = $'Use the write tool (not exec) to write exactly this string into ' " ${ PROOF_COPY } " $':\n' " ${ reply1 } " $'\nReply with exactly: WROTE'
2026-01-11 10:20:50 +00:00
run_agent_turn " $profile " " $SESSION_ID " " $prompt2 " " $TURN2_JSON "
assert_agent_json_has_text " $TURN2_JSON "
assert_agent_json_ok " $TURN2_JSON " " $agent_model_provider "
local copy_value
copy_value = " $( cat " $PROOF_COPY " 2>/dev/null | tr -d '\r\n' || true ) "
if [ [ " $copy_value " != " $PROOF_VALUE " ] ] ; then
echo " ERROR: copy.txt did not match proof.txt ( $profile ) " >& 2
exit 1
fi
2026-04-05 07:13:59 +01:00
run_agent_turn " $profile " " $SESSION_ID " \
" Use the read tool (not exec) to read ${ PROOF_COPY } . Reply with the exact contents only (no extra whitespace). " \
" $TURN2B_JSON "
assert_agent_json_has_text " $TURN2B_JSON "
assert_agent_json_ok " $TURN2B_JSON " " $agent_model_provider "
2026-01-11 10:20:50 +00:00
local reply2
2026-04-05 07:13:59 +01:00
reply2 = " $( extract_matching_text " $TURN2B_JSON " " $PROOF_VALUE " | tr -d '\r\n' ) "
2026-01-11 10:20:50 +00:00
if [ [ " $reply2 " != " $PROOF_VALUE " ] ] ; then
echo " ERROR: agent did not read copy.txt correctly ( $profile ): $reply2 " >& 2
exit 1
fi
2026-04-05 07:13:59 +01:00
run_agent_turn " $profile " " $SESSION_ID " \
"Use the exec tool to run: cat /etc/hostname. Reply with the exact stdout only (trim trailing newline)." \
" $TURN3_JSON "
2026-01-11 10:20:50 +00:00
assert_agent_json_has_text " $TURN3_JSON "
assert_agent_json_ok " $TURN3_JSON " " $agent_model_provider "
2026-04-05 07:13:59 +01:00
local reply3
reply3 = " $( extract_matching_text " $TURN3_JSON " " $EXPECTED_HOSTNAME " | tr -d '\r\n' ) "
if [ [ " $reply3 " != " $EXPECTED_HOSTNAME " ] ] ; then
echo " ERROR: agent did not read /etc/hostname correctly ( $profile ): $reply3 " >& 2
exit 1
fi
local prompt3b
prompt3b = $'Use the write tool to write exactly this string into ' " ${ HOSTNAME_TXT } " $':\n' " ${ reply3 } " $'\nReply with exactly: WROTE'
run_agent_turn " $profile " " $SESSION_ID " " $prompt3b " " $TURN3B_JSON "
assert_agent_json_has_text " $TURN3B_JSON "
assert_agent_json_ok " $TURN3B_JSON " " $agent_model_provider "
2026-01-11 10:20:50 +00:00
if [ [ " $( cat " $HOSTNAME_TXT " 2>/dev/null | tr -d '\r\n' || true ) " != " $EXPECTED_HOSTNAME " ] ] ; then
echo " ERROR: hostname.txt did not match /etc/hostname ( $profile ) " >& 2
exit 1
fi
run_agent_turn " $profile " " $SESSION_ID " \
2026-04-05 07:13:59 +01:00
" Use the image tool on ${ IMAGE_PNG } . Determine which color is on the left half and which is on the right half. Then use the write tool to write exactly: LEFT=RED RIGHT=GREEN into ${ IMAGE_TXT } . Reply with exactly: LEFT=RED RIGHT=GREEN " \
2026-01-11 10:20:50 +00:00
" $TURN4_JSON "
assert_agent_json_has_text " $TURN4_JSON "
assert_agent_json_ok " $TURN4_JSON " " $agent_model_provider "
if [ [ " $( cat " $IMAGE_TXT " 2>/dev/null | tr -d '\r\n' || true ) " != "LEFT=RED RIGHT=GREEN" ] ] ; then
echo " ERROR: image.txt did not contain expected marker ( $profile ) " >& 2
exit 1
fi
local reply4
2026-01-18 08:33:45 +00:00
reply4 = " $( extract_matching_text " $TURN4_JSON " "LEFT=RED RIGHT=GREEN" ) "
2026-01-11 10:20:50 +00:00
if [ [ " $reply4 " != "LEFT=RED RIGHT=GREEN" ] ] ; then
echo " ERROR: agent reply did not contain expected marker ( $profile ): $reply4 " >& 2
exit 1
fi
echo " ==> Verify tool usage via session transcript ( $profile ) "
# Give the gateway a moment to flush transcripts.
sleep 1
if [ [ ! -f " $SESSION_JSONL " ] ] ; then
echo " ERROR: missing session transcript ( $profile ): $SESSION_JSONL " >& 2
2026-04-05 07:13:59 +01:00
ls -la " $HOME /.openclaw- ${ profile } /agents/main/sessions " >& 2 || true
2026-01-11 10:20:50 +00:00
exit 1
fi
2026-01-12 02:49:55 +00:00
assert_session_used_tools " $SESSION_JSONL " read write exec image
2026-01-11 10:20:50 +00:00
cleanup_profile
trap - EXIT
}
if [ [ " $MODELS_MODE " = = "openai" || " $MODELS_MODE " = = "both" ] ] ; then
2026-01-30 03:15:10 +01:00
run_profile "e2e-openai" "18789" "/tmp/openclaw-e2e-openai" "openai"
2026-01-11 10:20:50 +00:00
fi
if [ [ " $MODELS_MODE " = = "anthropic" || " $MODELS_MODE " = = "both" ] ] ; then
2026-01-30 03:15:10 +01:00
run_profile "e2e-anthropic" "18799" "/tmp/openclaw-e2e-anthropic" "anthropic"
2026-01-11 10:20:50 +00:00
fi
echo "OK"