426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
264 lines
7.9 KiB
JavaScript
264 lines
7.9 KiB
JavaScript
#!/usr/bin/env node
|
|
// Cloud route e2e: provision a REAL Eliza Cloud agent (Hetzner-backed) and prove
|
|
// its runtime answers. Fails LOUDLY (non-zero exit + redacted diagnostics) when
|
|
// provisioning fails or the runtime never answers — the user's "if Hetzner
|
|
// fails to provision, we need to know". Programmatic surface only; the on-device
|
|
// cloud surface is driven by the Playwright Android suite (ELIZA_ANDROID_BACKEND
|
|
// =cloud) once a runtime URL + token are known.
|
|
//
|
|
// Usage:
|
|
// ELIZA_CLOUD_AUTH_TOKEN=... node scripts/cloud-provisioning-e2e.mjs
|
|
// [--cloud-api-base https://api.elizacloud.ai] [--agent-id <id>] [--fresh-agent]
|
|
// [--timeout-ms 600000] [--report <path>] [--print-runtime-url]
|
|
import fs from "node:fs";
|
|
|
|
const arg = (name, fb) => {
|
|
const i = process.argv.indexOf(name);
|
|
return i >= 0 ? process.argv[i + 1] : fb;
|
|
};
|
|
const has = (name) => process.argv.includes(name);
|
|
|
|
const token =
|
|
arg("--token") ??
|
|
process.env.ELIZA_CLOUD_AUTH_TOKEN ??
|
|
process.env.ELIZAOS_CLOUD_API_KEY ??
|
|
process.env.ELIZACLOUD_API_KEY;
|
|
const cloudApiBase = (
|
|
arg("--cloud-api-base") ??
|
|
process.env.ELIZA_CLOUD_API_BASE ??
|
|
"https://api.elizacloud.ai"
|
|
).replace(/\/+$/, "");
|
|
const agentIdArg = arg("--agent-id") ?? process.env.ELIZA_CLOUD_AGENT_ID;
|
|
const agentName =
|
|
arg("--agent-name") ??
|
|
process.env.ELIZA_CLOUD_AGENT_NAME ??
|
|
"Eliza Android Cloud E2E";
|
|
const timeoutMs = Number(arg("--timeout-ms") ?? 600_000);
|
|
const pollMs = Number(arg("--poll-ms") ?? 3_000);
|
|
const reportPath = arg("--report");
|
|
|
|
const log = (m) => console.log(`[cloud-e2e] ${m}`);
|
|
const redact = (v) =>
|
|
token ? String(v).replaceAll(token, "<redacted-token>") : String(v);
|
|
const delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
const tryJson = (t) => {
|
|
try {
|
|
return JSON.parse(t);
|
|
} catch {
|
|
return null;
|
|
}
|
|
};
|
|
const rec = (v) =>
|
|
typeof v === "object" && v !== null && !Array.isArray(v) ? v : null;
|
|
const data = (v) => rec(rec(v)?.data) ?? rec(v);
|
|
const str = (...xs) => {
|
|
for (const x of xs) if (typeof x === "string" && x.trim()) return x.trim();
|
|
return null;
|
|
};
|
|
|
|
function requireToken() {
|
|
if (!token?.trim()) {
|
|
throw new Error(
|
|
"Missing Cloud token. Set ELIZA_CLOUD_AUTH_TOKEN (or pass --token).",
|
|
);
|
|
}
|
|
return token.trim();
|
|
}
|
|
|
|
function runtimeUrlFrom(...vals) {
|
|
for (const v of vals) {
|
|
const o = rec(v);
|
|
if (!o) continue;
|
|
const url = str(
|
|
o.bridgeUrl,
|
|
o.bridge_url,
|
|
o.webUiUrl,
|
|
o.web_ui_url,
|
|
o.runtimeUrl,
|
|
o.runtime_url,
|
|
o.containerUrl,
|
|
o.container_url,
|
|
o.apiBase,
|
|
o.api_base,
|
|
);
|
|
if (url) return url.replace(/\/+$/, "");
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function normStatus(v) {
|
|
const s = String(v ?? "")
|
|
.trim()
|
|
.toLowerCase();
|
|
if (["complete", "completed", "success", "succeeded"].includes(s))
|
|
return "completed";
|
|
if (["fail", "failed", "error"].includes(s)) return "failed";
|
|
return s || "unknown";
|
|
}
|
|
|
|
async function cloud(path, init = {}) {
|
|
const url = `${cloudApiBase}${path}`;
|
|
const res = await fetch(url, {
|
|
...init,
|
|
headers: {
|
|
Accept: "application/json",
|
|
"Content-Type": "application/json",
|
|
Authorization: `Bearer ${requireToken()}`,
|
|
...(init.headers ?? {}),
|
|
},
|
|
});
|
|
const text = await res.text();
|
|
const body = tryJson(text) ?? text;
|
|
const ok =
|
|
(res.status >= 200 && res.status < 300) || rec(body)?.success === true;
|
|
if (!ok) {
|
|
const detail =
|
|
str(rec(body)?.error, rec(body)?.message, rec(body)?.reason) ??
|
|
text.slice(0, 300);
|
|
const err = new Error(
|
|
`Cloud request failed (${res.status}) ${url}: ${detail}`,
|
|
);
|
|
err.status = res.status;
|
|
err.body = body;
|
|
throw err;
|
|
}
|
|
return { status: res.status, body, text };
|
|
}
|
|
|
|
async function resolveAgent() {
|
|
if (agentIdArg?.trim()) {
|
|
const res = await cloud(
|
|
`/api/v1/eliza/agents/${encodeURIComponent(agentIdArg.trim())}`,
|
|
);
|
|
const d = data(res.body);
|
|
if (!d) throw new Error(`Cloud agent not found: ${agentIdArg}`);
|
|
return { ...d, id: str(d.id, d.agentId) ?? agentIdArg.trim() };
|
|
}
|
|
if (!has("--fresh-agent")) {
|
|
const res = await cloud("/api/v1/eliza/agents");
|
|
const list = Array.isArray(rec(res.body)?.data)
|
|
? rec(res.body).data
|
|
: Array.isArray(res.body)
|
|
? res.body
|
|
: [];
|
|
const existing = list.map((x) => data(x) ?? x).filter(Boolean);
|
|
const named = existing.find(
|
|
(a) => str(a.agentName, a.name, a.agent_name) === agentName,
|
|
);
|
|
const chosen = named ?? existing[0];
|
|
if (chosen) return chosen;
|
|
}
|
|
const res = await cloud("/api/v1/eliza/agents", {
|
|
method: "POST",
|
|
body: JSON.stringify({ agentName }),
|
|
});
|
|
const d = data(res.body);
|
|
const id = str(d?.id, d?.agentId, d?.agent_id);
|
|
if (!id) throw new Error(`Cloud create returned no agent id: ${res.text}`);
|
|
return { ...d, id };
|
|
}
|
|
|
|
async function provision(agentId) {
|
|
log(`provisioning agent=${agentId}`);
|
|
const res = await cloud(
|
|
`/api/v1/eliza/agents/${encodeURIComponent(agentId)}/provision`,
|
|
{ method: "POST" },
|
|
);
|
|
const d = data(res.body) ?? {};
|
|
return {
|
|
jobId: str(d.jobId, d.job_id, d.id),
|
|
runtimeUrl: runtimeUrlFrom(d, res.body),
|
|
status: normStatus(str(d.status, d.state)),
|
|
};
|
|
}
|
|
|
|
async function waitForRuntimeUrl(agentId, p) {
|
|
if (p.runtimeUrl) return p.runtimeUrl;
|
|
const deadline = Date.now() + timeoutMs;
|
|
while (Date.now() < deadline) {
|
|
if (p.jobId) {
|
|
const job = data(
|
|
(await cloud(`/api/v1/jobs/${encodeURIComponent(p.jobId)}`)).body,
|
|
);
|
|
const url = runtimeUrlFrom(rec(job?.result), job);
|
|
if (url) return url;
|
|
if (normStatus(str(job?.status, job?.state, job?.phase)) === "failed") {
|
|
throw new Error(
|
|
`Cloud provision job failed: ${str(job?.error, job?.message) ?? "unknown error"}`,
|
|
);
|
|
}
|
|
}
|
|
const agent = await cloud(
|
|
`/api/v1/eliza/agents/${encodeURIComponent(agentId)}`,
|
|
).catch(() => null);
|
|
const url = runtimeUrlFrom(data(agent?.body));
|
|
if (url) return url;
|
|
await delay(pollMs);
|
|
}
|
|
throw new Error(
|
|
`Timed out after ${timeoutMs}ms waiting for Cloud runtime URL.`,
|
|
);
|
|
}
|
|
|
|
async function probe(runtimeUrl) {
|
|
const failures = [];
|
|
for (const endpoint of ["/api/status", "/api/health", "/api/auth/me"]) {
|
|
const url = `${runtimeUrl}${endpoint}`;
|
|
try {
|
|
const res = await fetch(url, {
|
|
headers: {
|
|
Accept: "application/json",
|
|
Authorization: `Bearer ${requireToken()}`,
|
|
"X-ElizaOS-Client-Id": "cloud-provisioning-e2e",
|
|
},
|
|
});
|
|
const text = await res.text();
|
|
if (res.ok)
|
|
return {
|
|
ok: true,
|
|
url,
|
|
status: res.status,
|
|
body: tryJson(text) ?? text.slice(0, 300),
|
|
};
|
|
failures.push({ url, status: res.status, body: text.slice(0, 300) });
|
|
} catch (error) {
|
|
failures.push({ url, error: String(error) });
|
|
}
|
|
}
|
|
throw new Error(
|
|
`Provisioned runtime did not answer: ${JSON.stringify(failures)}`,
|
|
);
|
|
}
|
|
|
|
async function main() {
|
|
requireToken();
|
|
const agent = await resolveAgent();
|
|
const agentId = str(agent.id, agent.agentId, agent.agent_id);
|
|
if (!agentId) throw new Error("Selected Cloud agent has no id.");
|
|
const p = await provision(agentId);
|
|
const runtimeUrl = await waitForRuntimeUrl(agentId, p);
|
|
log(`runtime URL: ${runtimeUrl}`);
|
|
const probeResult = await probe(runtimeUrl);
|
|
const report = {
|
|
ok: true,
|
|
agentId,
|
|
runtimeUrl,
|
|
provision: p,
|
|
probe: probeResult,
|
|
};
|
|
if (reportPath)
|
|
fs.writeFileSync(
|
|
reportPath,
|
|
`${redact(JSON.stringify(report, null, 2))}\n`,
|
|
);
|
|
if (has("--print-runtime-url")) console.log(`RUNTIME_URL=${runtimeUrl}`);
|
|
log("CLOUD PROVISIONING OK ✅");
|
|
console.log(redact(JSON.stringify(report, null, 2)));
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(`[cloud-e2e] FAILED: ${redact(error?.message ?? error)}`);
|
|
if (error?.body) console.error(redact(JSON.stringify(error.body, null, 2)));
|
|
process.exit(1);
|
|
});
|