feat: add aipod spec Artificer assembly

This commit is contained in:
Codex
2026-06-10 17:46:45 +08:00
parent 45df61bd02
commit 6989dc18ef
22 changed files with 2103 additions and 56 deletions
+39 -2
View File
@@ -24,7 +24,13 @@ const selfTest: SelfTestCase = async (context) => {
secretRef: { name: "agentrun-v01-tool-unidesk-ssh", keys: ["UNIDESK_SSH_CLIENT_TOKEN"] },
projection: { kind: "env", envName: "UNIDESK_SSH_CLIENT_TOKEN", secretKey: "UNIDESK_SSH_CLIENT_TOKEN" },
}];
const combinedToolCredentials = [...githubToolCredentials, ...unideskSshToolCredentials];
const githubSshToolCredentials = [{
tool: "github",
purpose: "github-ssh",
secretRef: { name: "agentrun-v01-tool-github-ssh", keys: ["id_ed25519", "known_hosts", "config"] },
projection: { kind: "volume", mountPath: "/home/agentrun/.ssh" },
}];
const combinedToolCredentials = [...githubToolCredentials, ...unideskSshToolCredentials, ...githubSshToolCredentials];
const item = await createRunWithCommand(client, { ...context, toolCredentials: combinedToolCredentials }, "job smoke", "selftest-job-render", 15_000);
const rendered = renderRunnerJobDryRun({
run: await client.get(`/api/v1/runs/${item.runId}`) as RunRecord,
@@ -42,6 +48,7 @@ const selfTest: SelfTestCase = async (context) => {
assertRunnerJobUsesWritableCodexHome(rendered.manifest as JsonRecord, context.codexHome, "codex-0", "/var/run/agentrun/secrets/codex-0");
assertRunnerJobUsesToolCredential(rendered, "GH_TOKEN", "agentrun-v01-tool-github-pr", "GH_TOKEN");
assertRunnerJobUsesToolCredential(rendered, "UNIDESK_SSH_CLIENT_TOKEN", "agentrun-v01-tool-unidesk-ssh", "UNIDESK_SSH_CLIENT_TOKEN");
assertRunnerJobUsesToolCredentialVolume(rendered, "agentrun-v01-tool-github-ssh", "/home/agentrun/.ssh", ["id_ed25519", "known_hosts", "config"]);
assertRunnerJobUsesG14EgressProxy(rendered.manifest as JsonRecord);
assert.equal(runnerEnvValue(rendered.manifest as JsonRecord, "AGENTRUN_CODEX_SHELL_SANDBOX"), "danger-full-access");
assert.equal(runnerEnvValue(rendered.manifest as JsonRecord, "HWLAB_API_KEY"), "REDACTED");
@@ -211,6 +218,7 @@ process.exit(1);
assertRunnerJobUsesTransientEnvSecret(manifest, "UNIDESK_MAIN_SERVER_IP", String(transientEnvSecret.name));
assertRunnerJobUsesToolCredential({ manifest, toolCredentials: (created as JsonRecord).toolCredentials } as JsonRecord, "GH_TOKEN", "agentrun-v01-tool-github-pr", "GH_TOKEN");
assertRunnerJobUsesToolCredential({ manifest, toolCredentials: (created as JsonRecord).toolCredentials } as JsonRecord, "UNIDESK_SSH_CLIENT_TOKEN", "agentrun-v01-tool-unidesk-ssh", "UNIDESK_SSH_CLIENT_TOKEN");
assertRunnerJobUsesToolCredentialVolume({ manifest, toolCredentials: (created as JsonRecord).toolCredentials } as JsonRecord, "agentrun-v01-tool-github-ssh", "/home/agentrun/.ssh", ["id_ed25519", "known_hosts", "config"]);
assertNoSecretLeak(created);
const defaultEndpointJobItem = await createRunWithCommand(jobClient, { ...context, toolCredentials: unideskSshToolCredentials }, "job create unidesk ssh default endpoint", "selftest-job-create-unidesk-ssh-default-endpoint", 15_000);
const defaultEndpointCreated = await jobClient.post(`/api/v1/runs/${defaultEndpointJobItem.runId}/runner-jobs`, {
@@ -277,7 +285,7 @@ process.exit(1);
assert.equal(envMap.get("AGENTRUN_SESSION_PVC_NAMESPACE"), "agentrun-v01");
assert.equal(envMap.get("AGENTRUN_SESSION_PVC_MOUNT_PATH"), "/home/agentrun/.codex-codex/sessions");
assert.equal(envMap.get("AGENTRUN_CODEX_ROLLOUT_SUBDIR"), "sessions");
return { name: "runner-k8s-job", tests: ["runner-k8s-job-dry-run", "runner-k8s-job-codex-shell-sandbox-env", "runner-k8s-job-g14-egress-proxy-env", "runner-k8s-job-deepseek-profile-dry-run", "runner-k8s-job-minimax-m3-profile-dry-run", "runner-k8s-job-dsflash-go-profile-dry-run", "runner-k8s-job-dsflash-go-legacy-secretref-normalized", "runner-k8s-job-create-api", "runner-k8s-job-retention-ttl", "runner-job-transient-env", "runner-job-transient-env-secretref", "runner-job-tool-credential-env", "runner-job-unidesk-ssh-tool-credential-env", "runner-job-unidesk-ssh-endpoint-auto-env", "runner-job-unidesk-ssh-transient-env-denied", "runner-k8s-job-session-pvc-volume-and-env"] };
return { name: "runner-k8s-job", tests: ["runner-k8s-job-dry-run", "runner-k8s-job-codex-shell-sandbox-env", "runner-k8s-job-g14-egress-proxy-env", "runner-k8s-job-deepseek-profile-dry-run", "runner-k8s-job-minimax-m3-profile-dry-run", "runner-k8s-job-dsflash-go-profile-dry-run", "runner-k8s-job-dsflash-go-legacy-secretref-normalized", "runner-k8s-job-create-api", "runner-k8s-job-retention-ttl", "runner-job-transient-env", "runner-job-transient-env-secretref", "runner-job-tool-credential-env", "runner-job-unidesk-ssh-tool-credential-env", "runner-job-tool-credential-volume", "runner-job-unidesk-ssh-endpoint-auto-env", "runner-job-unidesk-ssh-transient-env-denied", "runner-k8s-job-session-pvc-volume-and-env"] };
} finally {
await new Promise<void>((resolve) => server.server.close(() => resolve()));
}
@@ -353,6 +361,35 @@ function assertRunnerJobUsesToolCredential(rendered: JsonRecord, envName: string
assert.equal(summaryEntry.valuesPrinted, false);
}
function assertRunnerJobUsesToolCredentialVolume(rendered: JsonRecord, secretName: string, mountPath: string, secretKeys: string[]): void {
const manifest = rendered.manifest as JsonRecord;
const spec = manifest.spec as JsonRecord;
const template = spec.template as JsonRecord;
const podSpec = template.spec as JsonRecord;
const containers = podSpec.containers as JsonRecord[];
const runner = containers[0] as JsonRecord;
const mounts = runner.volumeMounts as JsonRecord[];
const mount = mounts.find((item) => item.mountPath === mountPath) as JsonRecord | undefined;
assert.ok(mount, `${mountPath} tool credential volume mount should be present`);
assert.equal(mount.readOnly, true);
const volumes = podSpec.volumes as JsonRecord[];
const volume = volumes.find((item) => item.name === mount.name) as JsonRecord | undefined;
assert.ok(volume, `${mountPath} tool credential volume should be present`);
const secret = volume.secret as JsonRecord;
assert.equal(secret.secretName, secretName);
assert.deepEqual((secret.items as JsonRecord[]).map((item) => item.key), secretKeys);
const summary = rendered.toolCredentials as JsonRecord;
assert.equal(summary.valuesPrinted, false);
const items = summary.items as JsonRecord[];
const summaryEntry = items.find((item) => {
const projection = item.projection as JsonRecord;
return item.name === secretName && projection.kind === "volume" && projection.mountPath === mountPath;
});
assert.ok(summaryEntry, `${mountPath} tool credential summary should include its SecretRef and volume projection`);
assert.equal(summaryEntry.valuesPrinted, false);
}
function assertRunnerJobUsesWritableCodexHome(manifest: JsonRecord, expectedCodexHome: string, volumeName: string, projectionPath: string): void {
const spec = manifest.spec as JsonRecord;
const template = spec.template as JsonRecord;
+147
View File
@@ -0,0 +1,147 @@
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 ~/.ssh/id_ed25519\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 ~/.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;
+94
View File
@@ -0,0 +1,94 @@
import assert from "node:assert/strict";
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 { resolveGitBundleFetchSource } from "../../runner/resource-bundle.js";
import { assertNoSecretLeak, type SelfTestCase } from "../harness.js";
const selfTest: SelfTestCase = async (context) => {
const server = await startManagerServer({ port: 0, host: "127.0.0.1", sourceCommit: "self-test", store: new MemoryAgentRunStore(), aipodSpecDir: path.join(context.root, "config", "aipods") });
try {
const client = new ManagerClient(server.baseUrl);
const listed = await client.get("/api/v1/aipod-specs") as JsonRecord;
assert.equal(listed.action, "aipod-spec-list");
assert.equal((listed.items as JsonRecord[]).some((item) => item.name === "Artificer"), true);
const shown = await client.get("/api/v1/aipod-specs/Artificer") as JsonRecord;
const shownItem = shown.item as JsonRecord;
assert.equal(shownItem.backendProfile, "sub2api");
assert.equal(((shownItem.model as JsonRecord).model), "gpt-5.5");
assert.equal(((shownItem.resourceBundleRef as JsonRecord).gitMirror as JsonRecord).enabled, true);
const rendered = await client.post("/api/v1/aipod-specs/Artificer/render", { prompt: "处理 pikasTech/unidesk#245", idempotencyKey: "selftest-aipod-artificer" }) as JsonRecord;
assert.equal(rendered.action, "aipod-spec-render");
const task = rendered.queueTask as JsonRecord;
assert.equal(task.backendProfile, "sub2api");
assert.equal(task.providerId, "G14");
assert.equal(task.idempotencyKey, "selftest-aipod-artificer");
assert.equal(((task.payload as JsonRecord).model), "gpt-5.5");
assert.equal((((task.payload as JsonRecord).modelConfig as JsonRecord).reasoningEffort), "xhigh");
const policy = task.executionPolicy as JsonRecord;
const secretScope = policy.secretScope as JsonRecord;
const providerCredentials = secretScope.providerCredentials as JsonRecord[];
assert.equal(providerCredentials.some((item) => item.profile === "sub2api" && ((item.secretRef as JsonRecord).name) === "agentrun-v01-provider-sub2api"), true);
const toolCredentials = secretScope.toolCredentials as JsonRecord[];
assert.equal(toolCredentials.some((item) => item.tool === "unidesk-ssh" && ((item.projection as JsonRecord).envName) === "UNIDESK_SSH_CLIENT_TOKEN"), true);
assert.equal(toolCredentials.some((item) => item.tool === "github" && ((item.projection as JsonRecord).kind) === "volume" && ((item.projection as JsonRecord).mountPath) === "/home/agentrun/.ssh"), true);
const bundle = task.resourceBundleRef as JsonRecord;
assert.equal(((bundle.gitMirror as JsonRecord).enabled), true);
const bundles = bundle.bundles as JsonRecord[];
const toolBundle = bundles.find((item) => item.name === "agentrun-runner-tools") as JsonRecord | undefined;
assert.equal(toolBundle?.repoUrl, "git@github.com:pikasTech/agentrun.git");
assert.equal(toolBundle?.ref, "v0.1");
assert.equal(toolBundle?.subpath, "tools");
assert.equal(toolBundle?.targetPath, "tools");
assert.equal((bundle.requiredSkills as JsonRecord[]).some((item) => item.name === "dad-dev"), true);
assert.equal((bundle.requiredSkills as JsonRecord[]).some((item) => item.name === "unidesk-gh"), true);
assertNoSecretLeak(rendered);
const mirrored = resolveGitBundleFetchSource("git@github.com:pikasTech/unidesk.git", { enabled: true, baseUrl: "http://mirror.example.test/root/" }, {});
assert.equal(mirrored.fetchRepoUrl, "http://mirror.example.test/root/pikasTech/unidesk.git");
assert.equal(mirrored.mirrorUsed, true);
const nonGithub = resolveGitBundleFetchSource("ssh://git@example.test/repo.git", { enabled: true, baseUrl: "http://mirror.example.test" }, {});
assert.equal(nonGithub.fetchRepoUrl, "ssh://git@example.test/repo.git");
assert.equal(nonGithub.mirrorUsed, false);
const submitPlan = await runCliJson(context, server.baseUrl, ["queue", "submit", "--aipod", "Artificer", "--prompt", "处理 pikasTech/unidesk#245", "--idempotency-key", "selftest-aipod-cli", "--dry-run"]);
assert.equal(submitPlan.ok, true);
assert.equal(((submitPlan.data as JsonRecord).action), "queue-submit-plan");
assert.equal((((submitPlan.data as JsonRecord).jsonInput as JsonRecord).source), "aipod-spec");
const request = ((submitPlan.data as JsonRecord).request as JsonRecord);
assert.equal(((request.body as JsonRecord).idempotencyKey), "selftest-aipod-cli");
const help = await runCliJson(context, server.baseUrl, ["help"]);
const commands = ((help.data as JsonRecord).commands as string[]) ?? [];
assert.equal(commands.some((item) => item.includes("aipod-specs render <name>")), true);
assert.equal(commands.some((item) => item.includes("queue submit --aipod <name>")), true);
assertNoSecretLeak(submitPlan);
return { name: "aipod-spec", tests: ["aipod-spec-artificer-yaml-render", "aipod-spec-git-mirror-url", "queue-submit-aipod-dry-run", "aipod-cli-help"] };
} finally {
await new Promise<void>((resolve) => server.server.close(() => resolve()));
}
};
export default selfTest;
async function runCliJson(context: { root: string }, managerUrl: string, args: string[]): Promise<JsonRecord> {
const proc = spawn(process.execPath, [`${context.root}/scripts/agentrun-cli.ts`, "--manager-url", managerUrl, ...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");
}
+9 -2
View File
@@ -13,6 +13,7 @@ const selfTest: SelfTestCase = async (context) => {
const apkPackages = installedApkPackages(containerfile);
const tran = await readFile(path.join(context.root, "tools/tran"), "utf8");
const trans = await readFile(path.join(context.root, "tools/trans"), "utf8");
const applyPatch = await readFile(path.join(context.root, "tools/apply_patch"), "utf8");
for (const packageName of requiredRunnerPackages) {
assert.equal(apkPackages.has(packageName), true, `runner image must install ${packageName}`);
@@ -20,16 +21,22 @@ const selfTest: SelfTestCase = async (context) => {
assert.equal(tran.startsWith("#!/usr/bin/env bun\n"), true, "tools/tran must be a shebang executable discovered by gitbundle tools");
assert.equal(trans.startsWith("#!/bin/sh\n"), true, "tools/trans must be a shebang executable discovered by gitbundle tools");
assert.equal(applyPatch.startsWith("#!/bin/sh\n"), true, "tools/apply_patch must be a shebang helper copied with runner tools");
assert.equal(tran.includes("UNIDESK_SSH_CLIENT_TOKEN"), true, "tools/tran must require the scoped UniDesk SSH client token");
assert.equal(tran.includes("/ws/ssh"), true, "tools/tran must use the frontend SSH WebSocket path");
assert.equal(tran.includes("apply-patch < patch.diff"), true, "tools/tran help must advertise runner-side apply-patch");
const help = await execFileAsync(path.join(context.root, "tools/tran"), ["--help"], { cwd: context.root, timeout: 10_000 });
const parsed = JSON.parse(help.stdout) as { ok?: boolean; supported?: string[]; valuesPrinted?: boolean };
const parsed = JSON.parse(help.stdout) as { ok?: boolean; supported?: string[]; unsupported?: string[]; valuesPrinted?: boolean };
assert.equal(parsed.ok, true);
assert.equal(parsed.valuesPrinted, false);
assert.equal(parsed.supported?.some((line) => line.includes("script")), true);
assert.equal(parsed.supported?.some((line) => line.includes("apply-patch")), true);
assert.equal(parsed.unsupported?.includes("apply-patch"), false);
const patchHelp = await execFileAsync(path.join(context.root, "tools/apply_patch"), ["--help"], { cwd: context.root, timeout: 10_000 });
assert.equal(patchHelp.stdout.includes("reads *** Begin Patch format"), true);
return { name: "90-runner-image-tools", tests: ["runner image installs required CLI tools", "gitbundle tran tools are executable and documented"] };
return { name: "90-runner-image-tools", tests: ["runner image installs required CLI tools", "gitbundle tran tools are executable and documented", "runner apply-patch helper is bundled"] };
};
function installedApkPackages(containerfile: string): Set<string> {