Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

246 lines
8.8 KiB
TypeScript

#!/usr/bin/env bun
/**
* One-off Docker-disk prune for a single agent node — the immediate staging
* unblock for a node that is OUT OF DISK (`no space left on device` while
* pulling the agent image, breaking dedicated-agent provisioning on it).
*
* It runs the SAME reclamation the provisioning-worker daemon's disk-cleanup
* cycle runs (`docker system prune -af` WITHOUT `--volumes` + clear stuck
* containerd ingest from failed pulls + buildkit prune), over the SAME node-exec
* primitive (`DockerSSHClient`), reading the daemon's configured env — so it
* authenticates with `CONTAINERS_SSH_KEY` exactly like the daemon and never
* touches key material itself. Prints `df` before and after; prints NO secrets.
*
* The recurring daemon cycle PREVENTS recurrence; this script UNBLOCKS NOW.
*
* ── Invocation on the staging CP ──────────────────────────────────────────────
* The daemon runs on the control-plane host with its env at
* `/opt/eliza/cloud/.env.local`. SSH in as the deploy user and run with bun from
* the deployed `/opt/eliza` layout. The admin scripts are imported into that
* layout, where the cloud-shared package resolves as `@elizaos/cloud-shared/...`
* (NOT a relative `packages/cloud/shared/...` path — that only resolves in the
* monorepo checkout). This script already imports via `@elizaos/cloud-shared/*`
* so it resolves in both, but keep that in mind if you adapt it.
*
* ssh deploy@<staging-cp> # 167.233.105.184 (see infra access map)
* cd /opt/eliza/cloud
* set -a; . ./.env.local; set +a # load CONTAINERS_SSH_KEY etc. (no echo)
* bun /opt/eliza/.../packages/scripts/cloud/admin/prune-node-disk.ts \
* --node-id eliza-core-95ea703e
*
* You can target by node-id (looked up in docker_nodes for hostname/port/user) or
* pass --host directly to skip the DB lookup:
*
* bun .../prune-node-disk.ts --host 10.0.0.7 --ssh-user root
*
* Flags (env fallback in parens):
* --node-id <id> docker_nodes node_id to resolve (PRUNE_NODE_ID)
* --host <ip|hostname> SSH target, skips the DB lookup (PRUNE_NODE_HOST)
* --ssh-port <n> SSH port (PRUNE_NODE_SSH_PORT) [default 22]
* --ssh-user <user> SSH user (PRUNE_NODE_SSH_USER) [default root]
* --dry-run Print df + the planned reclamation, reclaim nothing.
*
* Exactly one of --node-id / --host is required.
*/
import * as path from "node:path";
import { fileURLToPath } from "node:url";
async function loadDeps() {
const [{ dockerNodesRepository }, { DockerSSHClient }, diskMgr] =
await Promise.all([
import("@elizaos/cloud-shared/db/repositories/docker-nodes"),
import("@elizaos/cloud-shared/lib/services/docker-ssh"),
import("@elizaos/cloud-shared/lib/services/node-disk-manager"),
]);
return {
dockerNodesRepository,
DockerSSHClient,
buildReclaimCommand: diskMgr.buildReclaimCommand,
parseDfUsedPercent: diskMgr.parseDfUsedPercent,
};
}
// ---------------------------------------------------------------------------
// Pure arg parsing (testable without the DB/SSH stack)
// ---------------------------------------------------------------------------
export interface PruneArgs {
nodeId?: string;
host?: string;
sshPort: number;
sshUser: string;
dryRun: boolean;
}
export function parsePruneArgs(
argv: string[],
env: NodeJS.ProcessEnv,
): PruneArgs {
const flags = new Map<string, string>();
let dryRun = false;
for (let i = 0; i < argv.length; i++) {
const arg = argv[i];
if (arg === undefined) continue;
if (arg === "--dry-run") {
dryRun = true;
continue;
}
if (arg.startsWith("--")) {
const key = arg.slice(2);
const value = argv[i + 1];
if (value === undefined || value.startsWith("--")) {
throw new Error(`Flag --${key} requires a value`);
}
flags.set(key, value);
i++;
}
}
const nodeId = flags.get("node-id") ?? env.PRUNE_NODE_ID;
const host = flags.get("host") ?? env.PRUNE_NODE_HOST;
if (!nodeId && !host) {
throw new Error(
"Provide exactly one of --node-id (resolved via docker_nodes) or --host",
);
}
if (nodeId && host) {
throw new Error("Provide only ONE of --node-id or --host, not both");
}
const sshPort = Number.parseInt(
flags.get("ssh-port") ?? env.PRUNE_NODE_SSH_PORT ?? "22",
10,
);
if (!Number.isInteger(sshPort) || sshPort < 1 || sshPort > 65535) {
throw new Error(`Invalid ssh-port: ${flags.get("ssh-port")}`);
}
const sshUser = flags.get("ssh-user") ?? env.PRUNE_NODE_SSH_USER ?? "root";
return {
...(nodeId ? { nodeId } : {}),
...(host ? { host } : {}),
sshPort,
sshUser,
dryRun,
};
}
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
const DF_TIMEOUT_MS = 15_000;
const RECLAIM_TIMEOUT_MS = 5 * 60_000;
const DF_CMD = "df -P /var/lib/docker 2>/dev/null || df -P /";
async function main(): Promise<void> {
const args = parsePruneArgs(process.argv.slice(2), process.env);
const {
dockerNodesRepository,
DockerSSHClient,
buildReclaimCommand,
parseDfUsedPercent,
} = await loadDeps();
// Resolve SSH target: explicit --host, or the docker_nodes row for --node-id.
let host = args.host;
let sshPort = args.sshPort;
let sshUser = args.sshUser;
let fingerprint: string | undefined;
if (args.nodeId) {
const node = await dockerNodesRepository.findByNodeId(args.nodeId);
if (!node) {
throw new Error(`No docker_nodes row for node-id "${args.nodeId}"`);
}
host = node.hostname;
sshPort = node.ssh_port ?? 22;
sshUser = node.ssh_user ?? "root";
fingerprint = node.host_key_fingerprint ?? undefined;
console.log(
`[prune-node-disk] resolved node "${args.nodeId}" -> ${sshUser}@${host}:${sshPort}`,
);
} else {
console.log(`[prune-node-disk] target ${sshUser}@${host}:${sshPort}`);
}
if (!host) throw new Error("No SSH host resolved");
const ssh = DockerSSHClient.getClient(host, sshPort, fingerprint, sshUser);
try {
await ssh.connect();
const before = await ssh.exec(DF_CMD, DF_TIMEOUT_MS);
console.log("\n[prune-node-disk] df BEFORE:");
console.log(before.trimEnd());
const beforePct = parseDfUsedPercent(before);
if (args.dryRun) {
console.log("\n[prune-node-disk] --dry-run: would run reclamation:");
console.log(` ${buildReclaimCommand()}`);
console.log("[prune-node-disk] --dry-run: no changes made.");
return;
}
console.log(
"\n[prune-node-disk] reclaiming (prune + clear stuck ingest)...",
);
await ssh.exec(buildReclaimCommand(), RECLAIM_TIMEOUT_MS);
const after = await ssh.exec(DF_CMD, DF_TIMEOUT_MS);
console.log("\n[prune-node-disk] df AFTER:");
console.log(after.trimEnd());
const afterPct = parseDfUsedPercent(after);
if (beforePct !== null && afterPct !== null) {
console.log(
`\n[prune-node-disk] done: ${beforePct}% -> ${afterPct}% (reclaimed ${
beforePct - afterPct
} points)`,
);
} else {
console.log("\n[prune-node-disk] done (could not parse df percent).");
}
} finally {
await ssh.disconnect().catch(() => {});
}
}
function isMainModule(): boolean {
const entry = process.argv[1];
return entry ? path.resolve(entry) === fileURLToPath(import.meta.url) : false;
}
if (isMainModule()) {
// Load the daemon's env the same way the daemons do, so CONTAINERS_SSH_KEY and
// DATABASE_URL resolve from /opt/eliza/cloud/.env.local without re-export.
//
// `loadLocalEnv` resolves the project root as ../../.. from the dir of the URL
// it's given. The daemons live in `.../scripts/cloud/admin/daemons/<f>.ts` and
// resolve to `packages/scripts` (where the deployed `.env.local` is loaded
// from). This script lives one dir up (`.../scripts/cloud/admin/`), so handing
// load-env `import.meta.url` directly would resolve to `packages/` — one level
// too high. Point it at a real file INSIDE the daemons dir so the project-root
// resolution is byte-for-byte the same as the daemon's.
import("./daemons/shared/load-env")
.then(({ loadLocalEnv }) => {
loadLocalEnv(
new URL("./daemons/provisioning-worker.ts", import.meta.url).href,
);
})
.catch(() => {
// load-env is best-effort: if the layout differs, env may already be set
// by `set -a; . ./.env.local` per the header invocation.
})
.finally(() => {
main().catch((error) => {
console.error(
"[prune-node-disk] failed:",
error instanceof Error ? error.message : String(error),
);
process.exit(1);
});
});
}