148 lines
7.2 KiB
TypeScript
148 lines
7.2 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { chmod, readFile, writeFile } from "node:fs/promises";
|
|
import { spawn } from "node:child_process";
|
|
import path from "node:path";
|
|
import { ManagerClient } from "../../mgr/client.js";
|
|
import { startManagerServer } from "../../mgr/server.js";
|
|
import { MemoryAgentRunStore } from "../../mgr/store.js";
|
|
import type { JsonRecord } from "../../common/types.js";
|
|
import { assertNoSecretLeak, type SelfTestCase } from "../harness.js";
|
|
|
|
const privateKeyHeader = ["-----BEGIN OPENSSH", "PRIVATE KEY-----"].join(" ");
|
|
const privateKeyFooter = ["-----END OPENSSH", "PRIVATE KEY-----"].join(" ");
|
|
const privateKey = `${privateKeyHeader}
|
|
selftest-private-key-material
|
|
${privateKeyFooter}
|
|
`;
|
|
const knownHosts = "github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIselftestKnownHostsKey\n";
|
|
const sshConfig = "Host github.com\n HostName ssh.github.com\n User git\n Port 443\n IdentityFile /home/agentrun/.ssh/id_ed25519\n UserKnownHostsFile /home/agentrun/.ssh/known_hosts\n";
|
|
|
|
const selfTest: SelfTestCase = async (context) => {
|
|
const fakeKubectl = path.join(context.tmp, "fake-tool-kubectl.js");
|
|
const stateDir = path.join(context.tmp, "tool-secret-state");
|
|
const createdSecretPath = path.join(context.tmp, "tool-secret-create.json");
|
|
await writeFile(fakeKubectl, `#!/usr/bin/env bun
|
|
import { mkdirSync } from "node:fs";
|
|
const args = Bun.argv.slice(2);
|
|
const stateDir = ${JSON.stringify(stateDir)};
|
|
const statePath = (name) => stateDir + "/" + name + ".json";
|
|
mkdirSync(stateDir, { recursive: true });
|
|
const readStdin = async () => {
|
|
const chunks = [];
|
|
for await (const chunk of Bun.stdin.stream()) chunks.push(Buffer.from(chunk));
|
|
return Buffer.concat(chunks).toString("utf8");
|
|
};
|
|
if (args[0] === "get" && args[1] === "secret") {
|
|
const name = args[2];
|
|
const file = Bun.file(statePath(name));
|
|
if (!(await file.exists())) {
|
|
console.error('Error from server (NotFound): secrets "' + name + '" not found');
|
|
process.exit(1);
|
|
}
|
|
console.log(await file.text());
|
|
process.exit(0);
|
|
}
|
|
if (args[0] === "replace") {
|
|
const text = await readStdin();
|
|
const manifest = JSON.parse(text);
|
|
const file = Bun.file(statePath(manifest.metadata.name));
|
|
if (!(await file.exists())) {
|
|
console.error('Error from server (NotFound): secrets "' + manifest.metadata.name + '" not found');
|
|
process.exit(1);
|
|
}
|
|
const next = { ...manifest, metadata: { ...(manifest.metadata ?? {}), resourceVersion: "rv-replaced" } };
|
|
await Bun.write(statePath(manifest.metadata.name), JSON.stringify(next));
|
|
console.log(JSON.stringify(next));
|
|
process.exit(0);
|
|
}
|
|
if (args[0] === "create") {
|
|
const text = await readStdin();
|
|
const manifest = JSON.parse(text);
|
|
await Bun.write(${JSON.stringify(createdSecretPath)}, JSON.stringify({ args, manifest }, null, 2));
|
|
const next = { ...manifest, metadata: { ...(manifest.metadata ?? {}), resourceVersion: "rv-created" } };
|
|
await Bun.write(statePath(manifest.metadata.name), JSON.stringify(next));
|
|
console.log(JSON.stringify(next));
|
|
process.exit(0);
|
|
}
|
|
console.error("unsupported fake kubectl args: " + JSON.stringify(args));
|
|
process.exit(1);
|
|
`);
|
|
await chmod(fakeKubectl, 0o755);
|
|
|
|
const server = await startManagerServer({
|
|
port: 0,
|
|
host: "127.0.0.1",
|
|
sourceCommit: "self-test",
|
|
store: new MemoryAgentRunStore(),
|
|
toolCredentialOptions: { namespace: "agentrun-v01", kubectlCommand: fakeKubectl },
|
|
});
|
|
try {
|
|
const client = new ManagerClient(server.baseUrl);
|
|
const missing = await client.get("/api/v1/tool-credentials") as JsonRecord;
|
|
assert.equal(missing.action, "tool-credential-list");
|
|
assert.equal(missing.count, 2);
|
|
assert.equal(JSON.stringify(missing).includes(privateKey), false);
|
|
const githubMissing = ((missing.items as JsonRecord[]) ?? []).find((item) => item.name === "github-ssh") as JsonRecord | undefined;
|
|
assert.equal(githubMissing?.configured, false);
|
|
assert.equal(githubMissing?.failureKind, "secret-unavailable");
|
|
|
|
const updated = await client.put("/api/v1/tool-credentials/github-ssh/credential", { privateKey, knownHosts, config: sshConfig }) as JsonRecord;
|
|
assert.equal(updated.action, "tool-credential-github-ssh-updated");
|
|
assert.equal(updated.configured, true);
|
|
assert.equal(updated.resourceVersion, "rv-created");
|
|
assertNoCredentialLeak(updated);
|
|
assertNoSecretLeak(updated);
|
|
const created = JSON.parse(await readFile(createdSecretPath, "utf8")) as JsonRecord;
|
|
const manifest = created.manifest as JsonRecord;
|
|
assert.equal(((manifest.metadata as JsonRecord).name), "agentrun-v01-tool-github-ssh");
|
|
const data = manifest.data as JsonRecord;
|
|
assert.equal(Buffer.from(String(data.id_ed25519), "base64").toString("utf8"), privateKey);
|
|
assert.equal(Buffer.from(String(data.known_hosts), "base64").toString("utf8"), knownHosts);
|
|
assert.equal(Buffer.from(String(data.config), "base64").toString("utf8"), sshConfig);
|
|
|
|
const shown = await client.get("/api/v1/tool-credentials/github-ssh") as JsonRecord;
|
|
assert.equal(shown.configured, true);
|
|
assert.deepEqual((shown.keyPresence as JsonRecord), { id_ed25519: true, known_hosts: true, config: true });
|
|
assertNoCredentialLeak(shown);
|
|
|
|
const privateKeyFile = path.join(context.tmp, "id_ed25519");
|
|
const knownHostsFile = path.join(context.tmp, "known_hosts");
|
|
await writeFile(privateKeyFile, privateKey);
|
|
await writeFile(knownHostsFile, knownHosts);
|
|
const dryRun = await runCliJson(context, ["tool-credentials", "set-github-ssh", "--private-key-file", privateKeyFile, "--known-hosts-file", knownHostsFile, "--dry-run"]);
|
|
assert.equal(dryRun.ok, true);
|
|
assert.equal(((dryRun.data as JsonRecord).action), "tool-credential-github-ssh-plan");
|
|
assertNoCredentialLeak(dryRun);
|
|
return { name: "tool-credentials", tests: ["tool-credential-list-missing", "tool-credential-github-ssh-upsert-redacted", "tool-credential-cli-dry-run"] };
|
|
} finally {
|
|
await new Promise<void>((resolve) => server.server.close(() => resolve()));
|
|
}
|
|
};
|
|
|
|
function assertNoCredentialLeak(value: unknown): void {
|
|
const text = JSON.stringify(value);
|
|
assert.equal(text.includes("selftest-private-key-material"), false);
|
|
assert.equal(text.includes(privateKeyHeader), false);
|
|
assert.equal(text.includes("AAAAC3NzaC1lZDI1NTE5AAAAIselftestKnownHostsKey"), false);
|
|
assert.equal(text.includes("IdentityFile /home/agentrun/.ssh/id_ed25519"), false);
|
|
}
|
|
|
|
async function runCliJson(context: { root: string }, args: string[]): Promise<JsonRecord> {
|
|
const proc = spawn(process.execPath, [`${context.root}/scripts/agentrun-cli.ts`, ...args], { stdio: ["ignore", "pipe", "pipe"] });
|
|
const [stdout, stderr, code] = await Promise.all([readStream(proc.stdout), readStream(proc.stderr), new Promise<number | null>((resolve) => proc.on("close", resolve))]);
|
|
assert.equal(code, 0, stderr || stdout);
|
|
return JSON.parse(stdout) as JsonRecord;
|
|
}
|
|
|
|
async function readStream(stream: NodeJS.ReadableStream): Promise<string> {
|
|
const chunks: Buffer[] = [];
|
|
stream.on("data", (chunk: Buffer | string) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
|
|
await new Promise<void>((resolve, reject) => {
|
|
stream.on("end", resolve);
|
|
stream.on("error", reject);
|
|
});
|
|
return Buffer.concat(chunks).toString("utf8");
|
|
}
|
|
|
|
export default selfTest;
|