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
346 lines
10 KiB
JavaScript
346 lines
10 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* End-to-end self-test of the certification gate plumbing, used by the
|
|
* `certification-verify-selftest` CI job and runnable locally. It exercises
|
|
* the REAL CLIs (`certify:keygen` → `bundle:create` → `certify:rollup` →
|
|
* `certify:sign` → `certify:verify`) against a throwaway keypair and a
|
|
* throwaway fixture repo — no reimplementation of any verification logic —
|
|
* and asserts:
|
|
*
|
|
* 1. a freshly signed certification verifies green (exit 0, ok:true),
|
|
* 2. a tampered payload fails `bad-signature`,
|
|
* 3. a missing committed bundle fails `bundle-tampered`,
|
|
* 4. a tampered bundle artifact fails `bundle-tampered`,
|
|
* 5. verification against a different trusted key fails `wrong-key`,
|
|
* 6. an out-of-window certification fails `stale`,
|
|
* 7. the commit-drift helper passes on match and rejects source drift,
|
|
* 8. the summary renderer produces markdown from the real report.
|
|
*
|
|
* The throwaway private key exists only in this process's memory and child
|
|
* env (ELIZA_CERT_SIGNING_KEY) — never on disk — matching the custody rules
|
|
* in .github/certification/README.md.
|
|
*/
|
|
|
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
import {
|
|
appendFileSync,
|
|
mkdirSync,
|
|
mkdtempSync,
|
|
readdirSync,
|
|
readFileSync,
|
|
rmSync,
|
|
writeFileSync,
|
|
} from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { dirname, join, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
const EVIDENCE_PKG = join(REPO_ROOT, "packages", "evidence");
|
|
|
|
const failures = [];
|
|
function step(name, ok, detail) {
|
|
const marker = ok ? "ok " : "FAIL";
|
|
console.log(`[selftest] ${marker} ${name}${detail ? ` — ${detail}` : ""}`);
|
|
if (!ok) failures.push(name);
|
|
}
|
|
|
|
function runCli(scriptName, args, { env = {}, expectExit = 0 } = {}) {
|
|
const result = spawnSync(
|
|
"bun",
|
|
["run", "--cwd", EVIDENCE_PKG, scriptName, "--", ...args],
|
|
{ encoding: "utf8", env: { ...process.env, ...env } },
|
|
);
|
|
if (result.error) throw result.error;
|
|
const label = `${scriptName} ${args.join(" ")}`;
|
|
console.log(`[selftest] $ bun run --cwd packages/evidence ${label}`);
|
|
if (result.status !== expectExit) {
|
|
console.log(result.stdout);
|
|
console.error(result.stderr);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function failureCodes(stdout) {
|
|
return JSON.parse(stdout).failures.map((failure) => failure.code);
|
|
}
|
|
|
|
function git(cwd, args) {
|
|
return execFileSync("git", args, {
|
|
cwd,
|
|
encoding: "utf8",
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
}).trim();
|
|
}
|
|
|
|
const work = mkdtempSync(join(tmpdir(), "certification-selftest-"));
|
|
try {
|
|
// -- keygen: throwaway trusted keypair + a second (wrong) keypair ---------
|
|
const pubkeyPath = join(work, "trusted-public-key.pem");
|
|
const keygen = runCli("certify:keygen", [
|
|
"--print-private-key",
|
|
"--pubkey-out",
|
|
pubkeyPath,
|
|
]);
|
|
const privatePem = keygen.stdout.slice(
|
|
keygen.stdout.indexOf("-----BEGIN PRIVATE KEY-----"),
|
|
);
|
|
step("keygen produced a private key", privatePem.includes("PRIVATE KEY"));
|
|
const signingEnv = {
|
|
ELIZA_CERT_SIGNING_KEY: Buffer.from(privatePem).toString("base64"),
|
|
};
|
|
const wrongPubkeyPath = join(work, "wrong-public-key.pem");
|
|
runCli("certify:keygen", ["--pubkey-out", wrongPubkeyPath]);
|
|
|
|
// -- fixture repo: one lane result the e2e-recordings silo will ingest ----
|
|
const fixtureRepo = join(work, "fixture-repo");
|
|
mkdirSync(join(fixtureRepo, "e2e-recordings"), { recursive: true });
|
|
writeFileSync(
|
|
join(fixtureRepo, "e2e-recordings", "result.json"),
|
|
JSON.stringify({ passed: 3, failed: 0, skipped: 0 }),
|
|
);
|
|
writeFileSync(join(fixtureRepo, "src.ts"), "export {};\n");
|
|
git(fixtureRepo, ["init", "-b", "main"]);
|
|
git(fixtureRepo, ["config", "user.email", "selftest@example.invalid"]);
|
|
git(fixtureRepo, ["config", "user.name", "certification selftest"]);
|
|
git(fixtureRepo, ["add", "-A"]);
|
|
git(fixtureRepo, ["commit", "-m", "fixture tree"]);
|
|
const fixtureCommit = git(fixtureRepo, ["rev-parse", "HEAD"]);
|
|
|
|
// -- bundle + rollup + sign ------------------------------------------------
|
|
const runsDir = join(work, "runs");
|
|
const create = runCli("bundle:create", [
|
|
"--tier",
|
|
"full",
|
|
"--repo-root",
|
|
fixtureRepo,
|
|
"--out",
|
|
runsDir,
|
|
]);
|
|
step("bundle:create succeeded", create.status === 0);
|
|
const bundleDir = join(runsDir, readdirSync(runsDir)[0]);
|
|
|
|
const verdictsPath = join(work, "verdicts.json");
|
|
const rollup = runCli("certify:rollup", [
|
|
"--bundle",
|
|
bundleDir,
|
|
"--out",
|
|
verdictsPath,
|
|
]);
|
|
step("certify:rollup succeeded", rollup.status === 0);
|
|
|
|
const sign = runCli(
|
|
"certify:sign",
|
|
[
|
|
"--bundle",
|
|
bundleDir,
|
|
"--verdicts",
|
|
verdictsPath,
|
|
"--reviewer-id",
|
|
"certification-selftest",
|
|
"--reviewer-kind",
|
|
"agent",
|
|
],
|
|
{ env: signingEnv },
|
|
);
|
|
step("certify:sign succeeded", sign.status === 0);
|
|
const certPath = join(bundleDir, "certification.json");
|
|
|
|
const baseVerifyArgs = [
|
|
"--cert",
|
|
certPath,
|
|
"--pubkey",
|
|
pubkeyPath,
|
|
"--expected-commit",
|
|
fixtureCommit,
|
|
"--max-age-hours",
|
|
"72",
|
|
"--required-tier",
|
|
"full",
|
|
"--bundle",
|
|
bundleDir,
|
|
"--json",
|
|
];
|
|
|
|
// -- 1. green path ----------------------------------------------------------
|
|
const green = runCli("certify:verify", baseVerifyArgs);
|
|
step("valid certification verifies (exit 0)", green.status === 0);
|
|
step("report.ok is true", JSON.parse(green.stdout).ok === true);
|
|
const greenReportPath = join(work, "verify-report.json");
|
|
writeFileSync(greenReportPath, green.stdout);
|
|
|
|
// -- 2. tampered payload → bad-signature ------------------------------------
|
|
const tamperedCertPath = join(work, "tampered-certification.json");
|
|
const cert = JSON.parse(readFileSync(certPath, "utf8"));
|
|
writeFileSync(
|
|
tamperedCertPath,
|
|
JSON.stringify({ ...cert, branch: "attacker-branch" }),
|
|
);
|
|
const tampered = runCli(
|
|
"certify:verify",
|
|
["--cert", tamperedCertPath, "--pubkey", pubkeyPath, "--json"],
|
|
{ expectExit: 1 },
|
|
);
|
|
step("tampered payload exits 1", tampered.status === 1);
|
|
step(
|
|
"tampered payload reports bad-signature",
|
|
failureCodes(tampered.stdout).includes("bad-signature"),
|
|
failureCodes(tampered.stdout).join(","),
|
|
);
|
|
|
|
// -- 3. missing committed bundle → bundle-tampered ---------------------------
|
|
const missingBundle = runCli(
|
|
"certify:verify",
|
|
[
|
|
"--cert",
|
|
certPath,
|
|
"--pubkey",
|
|
pubkeyPath,
|
|
"--expected-commit",
|
|
fixtureCommit,
|
|
"--max-age-hours",
|
|
"72",
|
|
"--required-tier",
|
|
"full",
|
|
"--bundle",
|
|
join(work, "missing-bundle"),
|
|
"--json",
|
|
],
|
|
{ expectExit: 1 },
|
|
);
|
|
step("missing committed bundle exits 1", missingBundle.status === 1);
|
|
step(
|
|
"missing committed bundle reports bundle-tampered",
|
|
failureCodes(missingBundle.stdout).includes("bundle-tampered"),
|
|
failureCodes(missingBundle.stdout).join(","),
|
|
);
|
|
|
|
// -- 4. tampered bundle artifact → bundle-tampered ---------------------------
|
|
appendFileSync(join(bundleDir, "lanes", "e2e", "result.json"), " ");
|
|
const bundleTampered = runCli("certify:verify", baseVerifyArgs, {
|
|
expectExit: 1,
|
|
});
|
|
step("tampered bundle exits 1", bundleTampered.status === 1);
|
|
step(
|
|
"tampered bundle reports bundle-tampered",
|
|
failureCodes(bundleTampered.stdout).includes("bundle-tampered"),
|
|
failureCodes(bundleTampered.stdout).join(","),
|
|
);
|
|
|
|
// -- 5. different trusted key → wrong-key ------------------------------------
|
|
const wrongKey = runCli(
|
|
"certify:verify",
|
|
["--cert", certPath, "--pubkey", wrongPubkeyPath, "--json"],
|
|
{ expectExit: 1 },
|
|
);
|
|
step("wrong trusted key exits 1", wrongKey.status === 1);
|
|
step(
|
|
"wrong trusted key reports wrong-key",
|
|
failureCodes(wrongKey.stdout).includes("wrong-key"),
|
|
failureCodes(wrongKey.stdout).join(","),
|
|
);
|
|
|
|
// -- 6. out-of-window certification → stale ----------------------------------
|
|
await new Promise((resolveSleep) => setTimeout(resolveSleep, 1500));
|
|
const stale = runCli(
|
|
"certify:verify",
|
|
[
|
|
"--cert",
|
|
certPath,
|
|
"--pubkey",
|
|
pubkeyPath,
|
|
// ~0.36s window: the 1.5s sleep above guarantees the cert is outside it.
|
|
"--max-age-hours",
|
|
"0.0001",
|
|
"--json",
|
|
],
|
|
{ expectExit: 1 },
|
|
);
|
|
step("out-of-window certification exits 1", stale.status === 1);
|
|
step(
|
|
"out-of-window certification reports stale",
|
|
failureCodes(stale.stdout).includes("stale"),
|
|
failureCodes(stale.stdout).join(","),
|
|
);
|
|
|
|
// -- 7. drift helper: match passes, source drift fails ------------------------
|
|
const driftScript = join(
|
|
REPO_ROOT,
|
|
"scripts",
|
|
"certification",
|
|
"check-commit-drift.mjs",
|
|
);
|
|
const driftMatch = spawnSync(
|
|
"node",
|
|
[
|
|
driftScript,
|
|
"--cert",
|
|
certPath,
|
|
"--head",
|
|
fixtureCommit,
|
|
"--repo",
|
|
fixtureRepo,
|
|
],
|
|
{ encoding: "utf8" },
|
|
);
|
|
step("drift helper passes on commit match", driftMatch.status === 0);
|
|
|
|
writeFileSync(join(fixtureRepo, "src.ts"), "export const drift = 1;\n");
|
|
git(fixtureRepo, ["add", "-A"]);
|
|
git(fixtureRepo, ["commit", "-m", "source drift"]);
|
|
const driftedHead = git(fixtureRepo, ["rev-parse", "HEAD"]);
|
|
const driftBad = spawnSync(
|
|
"node",
|
|
[
|
|
driftScript,
|
|
"--cert",
|
|
certPath,
|
|
"--head",
|
|
driftedHead,
|
|
"--repo",
|
|
fixtureRepo,
|
|
],
|
|
{ encoding: "utf8" },
|
|
);
|
|
step("drift helper rejects source drift", driftBad.status === 1);
|
|
|
|
// -- 8. renderer produces markdown from the real report -----------------------
|
|
const renderScript = join(
|
|
REPO_ROOT,
|
|
"scripts",
|
|
"certification",
|
|
"render-check-summary.mjs",
|
|
);
|
|
const summary = spawnSync(
|
|
"node",
|
|
[
|
|
renderScript,
|
|
"--mode",
|
|
"summary",
|
|
"--report",
|
|
greenReportPath,
|
|
"--pubkey",
|
|
pubkeyPath,
|
|
"--cert-path",
|
|
"selftest certification.json",
|
|
],
|
|
{ encoding: "utf8" },
|
|
);
|
|
step(
|
|
"renderer emits the verdict table",
|
|
summary.status === 0 &&
|
|
summary.stdout.includes("certification verified") &&
|
|
summary.stdout.includes("lane:e2e"),
|
|
);
|
|
console.log("\n[selftest] rendered summary preview:\n");
|
|
console.log(summary.stdout);
|
|
} finally {
|
|
rmSync(work, { recursive: true, force: true });
|
|
}
|
|
|
|
if (failures.length > 0) {
|
|
console.error(`\n[selftest] FAILED: ${failures.join("; ")}`);
|
|
process.exit(1);
|
|
}
|
|
console.log("\n[selftest] all certification gate plumbing checks passed");
|