Files
pikasTech-agentrun/src/selftest/cases/46-tool-credentials.ts
T
2026-06-10 21:15:41 +08:00

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;