fix: bound runner git transport (#169)
Co-authored-by: AgentRun Codex <agentrun-codex@users.noreply.github.com>
This commit is contained in:
@@ -53,6 +53,7 @@ const selfTest: SelfTestCase = async (context) => {
|
||||
assertRunnerJobUsesToolCredentialVolume(rendered, "agentrun-v01-tool-github-ssh", "/home/agentrun/.ssh", ["id_ed25519", "known_hosts", "config"]);
|
||||
assertRunnerJobUsesGithubSshCommand(rendered.manifest as JsonRecord, "/home/agentrun/.ssh");
|
||||
assertRunnerJobUsesG14EgressProxy(rendered.manifest as JsonRecord);
|
||||
assertRunnerJobUsesBoundedGitTransport(rendered);
|
||||
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");
|
||||
assert.deepEqual((((rendered.transientEnv as JsonRecord).names) as string[]), ["HWLAB_API_KEY"]);
|
||||
@@ -230,6 +231,7 @@ process.exit(1);
|
||||
const manifest = JSON.parse(await readFile(createdManifest, "utf8")) as JsonRecord;
|
||||
assert.equal((manifest.spec as JsonRecord).ttlSecondsAfterFinished, 86_400);
|
||||
assertRunnerJobUsesG14EgressProxy(manifest);
|
||||
assertRunnerJobUsesBoundedGitTransport({ manifest, gitTransport: (created as JsonRecord).gitTransport } as JsonRecord);
|
||||
assertRunnerJobUsesTransientEnvSecret(manifest, "HWLAB_API_KEY", String(transientEnvSecret.name));
|
||||
assertRunnerJobUsesTransientEnvSecret(manifest, "HWLAB_RUNTIME_API_URL", String(transientEnvSecret.name));
|
||||
assertRunnerJobUsesTransientEnvSecret(manifest, "HWLAB_CODE_AGENT_ASSEMBLED_RUNTIME", String(transientEnvSecret.name));
|
||||
@@ -249,6 +251,7 @@ process.exit(1);
|
||||
const defaultEndpointSecret = defaultEndpointCreated.transientEnvSecret as JsonRecord;
|
||||
assertRunnerJobUsesTransientEnvSecret(defaultEndpointManifest, "UNIDESK_MAIN_SERVER_IP", String(defaultEndpointSecret.name));
|
||||
assertRunnerJobUsesG14EgressProxy(defaultEndpointManifest);
|
||||
assertRunnerJobUsesBoundedGitTransport({ manifest: defaultEndpointManifest, gitTransport: defaultEndpointCreated.gitTransport } as JsonRecord);
|
||||
assertRunnerJobUsesToolCredential({ manifest: defaultEndpointManifest, toolCredentials: defaultEndpointCreated.toolCredentials } as JsonRecord, "UNIDESK_SSH_CLIENT_TOKEN", "agentrun-v01-tool-unidesk-ssh", "UNIDESK_SSH_CLIENT_TOKEN");
|
||||
assertNoSecretLeak(defaultEndpointCreated);
|
||||
await assert.rejects(
|
||||
@@ -304,7 +307,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-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"] };
|
||||
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-bounded-git-transport-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()));
|
||||
}
|
||||
@@ -364,6 +367,25 @@ function assertRunnerJobUsesG14EgressProxy(manifest: JsonRecord): void {
|
||||
assert.ok(noProxy.includes(".svc"), "NO_PROXY must include Kubernetes Service domains");
|
||||
}
|
||||
|
||||
function assertRunnerJobUsesBoundedGitTransport(rendered: JsonRecord): void {
|
||||
const manifest = rendered.manifest as JsonRecord;
|
||||
assert.equal(runnerEnvValue(manifest, "GIT_TERMINAL_PROMPT"), "0");
|
||||
assert.equal(runnerEnvValue(manifest, "GIT_HTTP_VERSION"), "HTTP/1.1");
|
||||
assert.equal(runnerEnvValue(manifest, "GIT_HTTP_LOW_SPEED_LIMIT"), "1024");
|
||||
assert.equal(runnerEnvValue(manifest, "GIT_HTTP_LOW_SPEED_TIME"), "15");
|
||||
assert.equal(runnerEnvValue(manifest, "AGENTRUN_GIT_DEFAULT_TIMEOUT_MS"), "60000");
|
||||
assert.equal(runnerEnvValue(manifest, "AGENTRUN_GIT_CREDENTIAL_HELPER"), "gh-auth-setup-git");
|
||||
const directHosts = String(runnerEnvValue(manifest, "AGENTRUN_GIT_DIRECT_HOSTS"));
|
||||
assert.ok(directHosts.includes("github.com"), "GitHub HTTPS transport should be eligible for direct fallback");
|
||||
assert.ok(directHosts.includes("codeload.github.com"), "codeload downloads should be eligible for direct fallback");
|
||||
const summary = rendered.gitTransport as JsonRecord;
|
||||
assert.equal(summary.valuesPrinted, false);
|
||||
assert.equal(summary.terminalPrompt, false);
|
||||
assert.equal(summary.defaultTimeoutMs, 60_000);
|
||||
assert.equal(summary.httpVersion, "HTTP/1.1");
|
||||
assert.equal(summary.credentialHelper, "gh-auth-setup-git");
|
||||
}
|
||||
|
||||
function assertRunnerJobUsesGithubSshCommand(manifest: JsonRecord, mountPath: string): void {
|
||||
const value = String(runnerEnvValue(manifest, "GIT_SSH_COMMAND"));
|
||||
assert.ok(value.includes(`-F ${mountPath}/config`), "GIT_SSH_COMMAND must use the mounted SSH config");
|
||||
|
||||
@@ -56,6 +56,10 @@ if (args[0] === "get" && args[1] === "secret") {
|
||||
console.log(JSON.stringify(fixture));
|
||||
process.exit(0);
|
||||
}
|
||||
if (args[0] === "get" && args[1] === "pvc") {
|
||||
console.error('Error from server (NotFound): persistentvolumeclaims "' + args[2] + '" not found');
|
||||
process.exit(1);
|
||||
}
|
||||
if (args[0] === "get" && args[1] === "secrets") {
|
||||
const items = [];
|
||||
if (!(await Bun.file(deletedMarkerPath("agentrun-v01-provider-codex")).exists())) items.push(fixtureSecret("agentrun-v01-provider-codex"));
|
||||
@@ -105,6 +109,10 @@ if (args[0] === "replace") {
|
||||
if (args[0] === "create") {
|
||||
const text = await readStdin();
|
||||
const manifest = JSON.parse(text);
|
||||
if (manifest.kind === "PersistentVolumeClaim") {
|
||||
console.log(JSON.stringify({ apiVersion: manifest.apiVersion, kind: manifest.kind, metadata: { name: manifest.metadata.name, namespace: manifest.metadata.namespace, resourceVersion: "rv-pvc" }, status: { phase: "Bound" } }));
|
||||
process.exit(0);
|
||||
}
|
||||
if (manifest.kind === "Secret") {
|
||||
try { rmSync(deletedMarkerPath(manifest.metadata.name)); } catch {}
|
||||
const annotations = manifest.metadata?.annotations ?? {};
|
||||
|
||||
@@ -22,12 +22,29 @@ const selfTest: SelfTestCase = async (context) => {
|
||||
const fakeKubectl = path.join(context.tmp, "fake-kubectl-hwlab.js");
|
||||
const createdManifest = path.join(context.tmp, "created-hwlab-runner-job.json");
|
||||
await writeFile(fakeKubectl, `#!/usr/bin/env bun
|
||||
const chunks = [];
|
||||
for await (const chunk of Bun.stdin.stream()) chunks.push(chunk);
|
||||
const text = Buffer.concat(chunks.map((chunk) => Buffer.from(chunk))).toString("utf8");
|
||||
await Bun.write(${JSON.stringify(createdManifest)}, text);
|
||||
const manifest = JSON.parse(text);
|
||||
console.log(JSON.stringify({ apiVersion: manifest.apiVersion, kind: manifest.kind, metadata: { uid: "job-uid-hwlab", resourceVersion: "1", name: manifest.metadata.name, namespace: manifest.metadata.namespace } }));
|
||||
const args = Bun.argv.slice(2);
|
||||
async function readStdin() {
|
||||
const chunks = [];
|
||||
for await (const chunk of Bun.stdin.stream()) chunks.push(chunk);
|
||||
return Buffer.concat(chunks.map((chunk) => Buffer.from(chunk))).toString("utf8");
|
||||
}
|
||||
if (args[0] === "get" && args[1] === "pvc") {
|
||||
console.error('Error from server (NotFound): persistentvolumeclaims "' + args[2] + '" not found');
|
||||
process.exit(1);
|
||||
}
|
||||
if (args[0] === "create") {
|
||||
const text = await readStdin();
|
||||
const manifest = JSON.parse(text);
|
||||
if (manifest.kind === "PersistentVolumeClaim") {
|
||||
console.log(JSON.stringify({ apiVersion: manifest.apiVersion, kind: manifest.kind, metadata: { uid: "pvc-uid-hwlab", resourceVersion: "1", name: manifest.metadata.name, namespace: manifest.metadata.namespace }, status: { phase: "Bound" } }));
|
||||
process.exit(0);
|
||||
}
|
||||
await Bun.write(${JSON.stringify(createdManifest)}, text);
|
||||
console.log(JSON.stringify({ apiVersion: manifest.apiVersion, kind: manifest.kind, metadata: { uid: "job-uid-hwlab", resourceVersion: "1", name: manifest.metadata.name, namespace: manifest.metadata.namespace } }));
|
||||
process.exit(0);
|
||||
}
|
||||
console.error("unsupported fake kubectl args: " + args.join(" "));
|
||||
process.exit(1);
|
||||
`);
|
||||
await chmod(fakeKubectl, 0o755);
|
||||
const store = new MemoryAgentRunStore();
|
||||
@@ -97,7 +114,7 @@ console.log(JSON.stringify({ apiVersion: manifest.apiVersion, kind: manifest.kin
|
||||
const resultBundleTargets = (((resultEnvelope.resourceBundleRef as JsonRecord).bundles as JsonRecord).items as JsonRecord[]).map((item) => item.targetPath);
|
||||
assert.deepEqual(resultBundleTargets, ["tools", ".agents/skills"]);
|
||||
const materialized = ((resultEnvelope.resourceBundleRef as JsonRecord).materialized as JsonRecord);
|
||||
assert.deepEqual(((materialized.tools as JsonRecord).names), ["apply_patch", "hwpod", "tran", "trans"]);
|
||||
assert.deepEqual(((materialized.tools as JsonRecord).names), ["agentrun-git", "apply_patch", "hwpod", "tran", "trans"]);
|
||||
assert.equal(((materialized.tools as JsonRecord).installed), true);
|
||||
assert.deepEqual(((materialized.skillDirs as JsonRecord).names), ["dad-dev", "hwpod-cli", "hwpod-ctl"]);
|
||||
const requiredSkillItems = ((materialized.requiredSkills as JsonRecord).items as JsonRecord[]);
|
||||
@@ -278,6 +295,7 @@ async function createLocalGitBundle(context: SelfTestContext, repoName = "bundle
|
||||
await writeFile(path.join(repo, "tools", "tran"), "#!/usr/bin/env sh\necho tran-selftest\n", "utf8");
|
||||
await writeFile(path.join(repo, "tools", "trans"), "#!/usr/bin/env sh\necho trans-selftest\n", "utf8");
|
||||
await writeFile(path.join(repo, "tools", "apply_patch"), "#!/usr/bin/env sh\necho apply-patch-selftest\n", "utf8");
|
||||
await writeFile(path.join(repo, "tools", "agentrun-git"), "#!/usr/bin/env sh\necho agentrun-git-selftest\n", "utf8");
|
||||
await writeFile(path.join(repo, "tools", "hwpod-cli.ts"), "import { hwpodSelftestName } from './src/hwpod-harness-lib.ts';\nconsole.log(JSON.stringify({ ok: true, cli: hwpodSelftestName(), argv: process.argv.slice(2) }));\n", "utf8");
|
||||
await writeFile(path.join(repo, "tools", "src", "hwpod-harness-lib.ts"), "export function hwpodSelftestName() { return 'hwpod-selftest'; }\n", "utf8");
|
||||
await writeFile(path.join(repo, "tools", "hwpod-node.test.ts"), "console.log('test-only source file without shebang');\n", "utf8");
|
||||
@@ -318,7 +336,7 @@ async function createLocalGitBundle(context: SelfTestContext, repoName = "bundle
|
||||
"Use hwpod-ctl for HWPOD runtime inspection and control-plane state.",
|
||||
].join("\n"), "utf8");
|
||||
await writeFile(path.join(repo, "skills", "hwpod-ctl", "scripts", "hwpod-ctl.mjs"), "console.log(JSON.stringify({ ok: true, cli: 'hwpod-ctl-skill-selftest' }));\n", "utf8");
|
||||
await execFile("git", ["add", "README.md", "tools/hwpod", "tools/tran", "tools/trans", "tools/apply_patch", "tools/hwpod-cli.ts", "tools/src/hwpod-harness-lib.ts", "tools/hwpod-node.test.ts", "internal/agent/prompts/hwlab-v02-runtime.md", "skills/dad-dev/SKILL.md", "skills/hwpod-cli/SKILL.md", "skills/hwpod-cli/scripts/hwpod-cli.mjs", "skills/hwpod-ctl/SKILL.md", "skills/hwpod-ctl/scripts/hwpod-ctl.mjs"], { cwd: repo });
|
||||
await execFile("git", ["add", "README.md", "tools/hwpod", "tools/tran", "tools/trans", "tools/apply_patch", "tools/agentrun-git", "tools/hwpod-cli.ts", "tools/src/hwpod-harness-lib.ts", "tools/hwpod-node.test.ts", "internal/agent/prompts/hwlab-v02-runtime.md", "skills/dad-dev/SKILL.md", "skills/hwpod-cli/SKILL.md", "skills/hwpod-cli/scripts/hwpod-cli.mjs", "skills/hwpod-ctl/SKILL.md", "skills/hwpod-ctl/scripts/hwpod-ctl.mjs"], { cwd: repo });
|
||||
await execFile("git", ["-c", "user.email=selftest@example.invalid", "-c", "user.name=AgentRun SelfTest", "commit", "-m", "bundle selftest"], { cwd: repo });
|
||||
const { stdout } = await execFile("git", ["rev-parse", "HEAD"], { cwd: repo });
|
||||
return { repoUrl: repo, commitId: stdout.trim(), requiredSkills: [{ name: "dad-dev" }] };
|
||||
|
||||
@@ -17,6 +17,7 @@ const selfTest: SelfTestCase = async (context) => {
|
||||
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");
|
||||
const agentrunGit = await readFile(path.join(context.root, "tools/agentrun-git"), "utf8");
|
||||
|
||||
for (const packageName of requiredRunnerPackages) {
|
||||
assert.equal(apkPackages.has(packageName), true, `runner image must install ${packageName}`);
|
||||
@@ -28,9 +29,12 @@ 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(agentrunGit.startsWith("#!/usr/bin/env bun\n"), true, "tools/agentrun-git must be a shebang executable discovered by gitbundle 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");
|
||||
assert.equal(agentrunGit.includes("GIT_TERMINAL_PROMPT"), true, "tools/agentrun-git must disable interactive git prompts");
|
||||
assert.equal(agentrunGit.includes("AGENTRUN_GIT_DIRECT_HOSTS"), true, "tools/agentrun-git must expose direct host policy");
|
||||
|
||||
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[]; unsupported?: string[]; valuesPrinted?: boolean };
|
||||
@@ -41,10 +45,18 @@ const selfTest: SelfTestCase = async (context) => {
|
||||
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);
|
||||
const agentrunGitHelp = await execFileAsync(path.join(context.root, "tools/agentrun-git"), ["--help"], { cwd: context.root, timeout: 10_000 });
|
||||
const parsedGitHelp = JSON.parse(agentrunGitHelp.stdout) as { ok?: boolean; commands?: string[]; defaults?: { valuesPrinted?: boolean }; valuesPrinted?: boolean };
|
||||
assert.equal(parsedGitHelp.ok, true);
|
||||
assert.equal(parsedGitHelp.valuesPrinted, false);
|
||||
assert.equal(parsedGitHelp.defaults?.valuesPrinted, false);
|
||||
assert.equal(parsedGitHelp.commands?.includes("ls-remote"), true);
|
||||
assert.equal(parsedGitHelp.commands?.includes("download"), true);
|
||||
const summary = staticWorkReadyCapabilitySummary();
|
||||
assert.equal(summary.valuesPrinted, false);
|
||||
assert.ok((summary.requiredImageTools as string[]).includes("npm"), "work-ready capability must include npm");
|
||||
assert.ok((summary.requiredImageTools as string[]).includes("gh"), "work-ready capability must include gh");
|
||||
assert.ok((summary.requiredBundledTools as string[]).includes("agentrun-git"), "work-ready capability must include agentrun-git");
|
||||
assert.equal(((summary.dependencyStrategy as { projectDependencies?: unknown }).projectDependencies), "not-installed-by-default");
|
||||
const fakeBin = await createFakeToolBin(path.join(context.tmp, "work-ready-bin"));
|
||||
const smokeEnv = { PATH: fakeBin, AGENTRUN_RESOURCE_BIN_PATH: path.join(context.root, "tools") };
|
||||
@@ -62,7 +74,7 @@ const selfTest: SelfTestCase = async (context) => {
|
||||
assert.equal(isDigestPinnedImage("127.0.0.1:5000/agentrun/agentrun-mgr@sha256:1111111111111111111111111111111111111111111111111111111111111111"), true);
|
||||
assert.equal(isDigestPinnedImage("127.0.0.1:5000/agentrun/agentrun-mgr:self-test"), false);
|
||||
|
||||
return { name: "90-runner-image-tools", tests: ["runner image installs required CLI tools", "runner image build verifies work-ready tools", "gitbundle tran tools are executable and documented", "runner apply-patch helper is bundled", "work-ready smoke runs without printing secrets", "aipod imageRef validates env image source identity"] };
|
||||
return { name: "90-runner-image-tools", tests: ["runner image installs required CLI tools", "runner image build verifies work-ready tools", "gitbundle tran tools are executable and documented", "runner apply-patch helper is bundled", "runner agentrun-git helper is bundled", "work-ready smoke runs without printing secrets", "aipod imageRef validates env image source identity"] };
|
||||
};
|
||||
|
||||
async function createFakeToolBin(dir: string): Promise<string> {
|
||||
|
||||
Reference in New Issue
Block a user