From 10bc33f8e1ec6627a0ca9bd4be57c8c0758560c4 Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 2 Jun 2026 09:12:47 +0800 Subject: [PATCH] fix: expose resource aliases on runner path --- src/runner/k8s-job.ts | 2 ++ src/runner/resource-bundle.ts | 26 ++++++++++++++----- .../cases/50-hwlab-manual-dispatch.ts | 25 +++++++++++++++--- 3 files changed, 43 insertions(+), 10 deletions(-) diff --git a/src/runner/k8s-job.ts b/src/runner/k8s-job.ts index c9a1e31..c8f7c27 100644 --- a/src/runner/k8s-job.ts +++ b/src/runner/k8s-job.ts @@ -3,6 +3,7 @@ import type { BackendProfile, ExecutionPolicy, JsonRecord, JsonValue, RunRecord, import { backendProfileSpec } from "../common/backend-profiles.js"; const defaultBootRepoUrl = "http://git-mirror-http.devops-infra.svc.cluster.local/pikasTech/agentrun.git"; +const defaultResourceBinPath = "/usr/local/bin"; export interface RunnerJobRenderOptions { run: RunRecord; @@ -165,6 +166,7 @@ function runnerEnv(options: RunnerJobRenderOptions, context: { namespace: string { name: "AGENTRUN_SESSION_REF_JSON", value: JSON.stringify(options.run.sessionRef ?? null) }, { name: "AGENTRUN_RESOURCE_BUNDLE_JSON", value: JSON.stringify(options.run.resourceBundleRef ?? null) }, { name: "AGENTRUN_WORKSPACE_ROOT", value: "/home/agentrun/workspaces" }, + { name: "AGENTRUN_RESOURCE_BIN_PATH", value: defaultResourceBinPath }, { name: "AGENTRUN_SOURCE_COMMIT", value: context.sourceCommit }, { name: "AGENTRUN_BOOT_COMMIT", value: context.sourceCommit }, { name: "AGENTRUN_BOOT_REPO_URL", value: defaultBootRepoUrl }, diff --git a/src/runner/resource-bundle.ts b/src/runner/resource-bundle.ts index dc77102..97fe461 100644 --- a/src/runner/resource-bundle.ts +++ b/src/runner/resource-bundle.ts @@ -1,5 +1,5 @@ import { spawn } from "node:child_process"; -import { chmod, mkdir, writeFile } from "node:fs/promises"; +import { chmod, mkdir, readFile, writeFile } from "node:fs/promises"; import path from "node:path"; import { AgentRunError } from "../common/errors.js"; import { redactText } from "../common/redaction.js"; @@ -59,7 +59,9 @@ async function materializeToolAliases(checkoutPath: string, aliases: NonNullable for (const alias of aliases) { const target = resolveBundlePath(checkoutPath, alias.path, `toolAliases.${alias.name}.path`); const wrapper = path.join(binPath, alias.name); - await writeFile(wrapper, aliasWrapper(alias.kind, target), "utf8"); + const content = aliasWrapper(alias.kind, target); + await assertAliasWrapperWritable(wrapper, alias.name); + await writeFile(wrapper, content, "utf8"); await chmod(wrapper, 0o755); names.push(alias.name); } @@ -67,10 +69,22 @@ async function materializeToolAliases(checkoutPath: string, aliases: NonNullable } function aliasWrapper(kind: string, target: string): string { - if (kind === "node-script") return `#!/usr/bin/env sh\nexec node ${shellArg(target)} "$@"\n`; - if (kind === "bun-script") return `#!/usr/bin/env sh\nexec bun ${shellArg(target)} "$@"\n`; - if (kind === "sh-script") return `#!/usr/bin/env sh\nexec sh ${shellArg(target)} "$@"\n`; - return `#!/usr/bin/env sh\nexec ${shellArg(target)} "$@"\n`; + if (kind === "node-script") return `#!/usr/bin/env sh\n# agentrun-resource-alias-wrapper\nexec node ${shellArg(target)} "$@"\n`; + if (kind === "bun-script") return `#!/usr/bin/env sh\n# agentrun-resource-alias-wrapper\nexec bun ${shellArg(target)} "$@"\n`; + if (kind === "sh-script") return `#!/usr/bin/env sh\n# agentrun-resource-alias-wrapper\nexec sh ${shellArg(target)} "$@"\n`; + return `#!/usr/bin/env sh\n# agentrun-resource-alias-wrapper\nexec ${shellArg(target)} "$@"\n`; +} + +async function assertAliasWrapperWritable(wrapper: string, name: string): Promise { + try { + const existing = await readFile(wrapper, "utf8"); + if (existing.includes("agentrun-resource-alias-wrapper")) return; + throw new AgentRunError("schema-invalid", `resource bundle tool alias ${name} would overwrite an existing command`, { httpStatus: 409, details: { wrapper: pathSummary(wrapper) } }); + } catch (error) { + if (error instanceof AgentRunError) throw error; + if (error && typeof error === "object" && "code" in error && (error as { code?: unknown }).code === "ENOENT") return; + throw error; + } } async function git(args: string[], cwd: string, options: { allowFailure?: boolean } = {}): Promise<{ stdout: string; stderr: string }> { diff --git a/src/selftest/cases/50-hwlab-manual-dispatch.ts b/src/selftest/cases/50-hwlab-manual-dispatch.ts index 158543e..2465674 100644 --- a/src/selftest/cases/50-hwlab-manual-dispatch.ts +++ b/src/selftest/cases/50-hwlab-manual-dispatch.ts @@ -7,10 +7,11 @@ import { startManagerServer } from "../../mgr/server.js"; import { ManagerClient } from "../../mgr/client.js"; import { MemoryAgentRunStore } from "../../mgr/store.js"; import { runOnce } from "../../runner/run-once.js"; -import type { JsonRecord } from "../../common/types.js"; +import type { JsonRecord, ResourceBundleRef } from "../../common/types.js"; import { assertNoSecretLeak, type SelfTestCase, type SelfTestContext } from "../harness.js"; const execFile = promisify(execFileCallback); +type LocalBundle = { repoUrl: string; commitId: string; toolAliases?: ResourceBundleRef["toolAliases"] }; const selfTest: SelfTestCase = async (context) => { const containerfile = await readFile(path.join(context.root, "deploy/container/Containerfile"), "utf8"); @@ -56,6 +57,7 @@ console.log(JSON.stringify({ apiVersion: manifest.apiVersion, kind: manifest.kin ); const manifest = JSON.parse(await readFile(createdManifest, "utf8")) as JsonRecord; assert.ok(JSON.stringify(manifest).includes("AGENTRUN_RESOURCE_BUNDLE_JSON")); + assert.equal(runnerEnvValue(manifest, "AGENTRUN_RESOURCE_BIN_PATH"), "/usr/local/bin"); assert.ok(JSON.stringify(manifest).includes("/opt/agentrun/deploy/runtime/boot/agentrun-runner.sh")); assert.ok(JSON.stringify(manifest).includes("AGENTRUN_BOOT_COMMIT")); assertNoSecretLeak(created); @@ -78,6 +80,11 @@ console.log(JSON.stringify({ apiVersion: manifest.apiVersion, kind: manifest.kin assert.equal(runResult.terminalStatus, "completed"); const hwpod = await execFile(path.join(resourceBin, "hwpod"), ["profile", "list"]); assert.match(hwpod.stdout, /"argv":\["profile","list"\]/u); + await writeFile(path.join(resourceBin, "blocked"), "#!/usr/bin/env sh\necho existing\n", "utf8"); + const blockedRun = await createHwlabRun(client, context, { ...bundle, toolAliases: [{ name: "blocked", path: "tools/device-pod-cli.mjs", kind: "node-script" }] }, "hwlab-session-blocked-alias", "blocked alias", "hwlab-command-blocked-alias"); + const blockedResult = await runOnce({ managerUrl: server.baseUrl, runId: blockedRun.runId, codexCommand: context.fakeCodexCommand, codexArgs: context.fakeCodexArgs, codexHome: context.codexHome, env: { CODEX_HOME: context.codexHome, AGENTRUN_WORKSPACE_ROOT: path.join(context.tmp, "workspaces-blocked"), AGENTRUN_RESOURCE_BIN_PATH: resourceBin }, oneShot: true }); + assert.equal(blockedResult.terminalStatus, "blocked"); + assert.equal(blockedResult.failureKind, "schema-invalid"); const session = await store.getSession("hwlab-session-resume"); assert.equal(session?.threadId, "thread_selftest_1"); const resultEnvelope = await client.get(`/api/v1/runs/${sessionRun.runId}/commands/${sessionRun.commandId}/result`) as JsonRecord; @@ -137,7 +144,7 @@ console.log(JSON.stringify({ apiVersion: manifest.apiVersion, kind: manifest.kin } }; -async function createLocalGitBundle(context: SelfTestContext): Promise<{ repoUrl: string; commitId: string }> { +async function createLocalGitBundle(context: SelfTestContext): Promise { const repo = path.join(context.tmp, "bundle-repo"); await mkdir(repo, { recursive: true }); await execFile("git", ["init"], { cwd: repo }); @@ -150,13 +157,14 @@ async function createLocalGitBundle(context: SelfTestContext): Promise<{ repoUrl return { repoUrl: repo, commitId: stdout.trim() }; } -async function createHwlabRun(client: ManagerClient, context: SelfTestContext, bundle: { repoUrl: string; commitId: string }, sessionId: string, prompt: string, idempotencyKey: string, timeoutMs = 15_000): Promise<{ runId: string; commandId: string }> { +async function createHwlabRun(client: ManagerClient, context: SelfTestContext, bundle: LocalBundle, sessionId: string, prompt: string, idempotencyKey: string, timeoutMs = 15_000): Promise<{ runId: string; commandId: string }> { + const toolAliases = bundle.toolAliases ?? [{ name: "hwpod", path: "tools/device-pod-cli.mjs", kind: "node-script" }]; const run = await client.post("/api/v1/runs", { tenantId: "hwlab", projectId: "pikasTech/HWLAB", workspaceRef: { kind: "opaque", repo: "pikasTech/HWLAB" }, sessionRef: { sessionId, conversationId: sessionId }, - resourceBundleRef: { kind: "git", repoUrl: bundle.repoUrl, commitId: bundle.commitId, toolAliases: [{ name: "hwpod", path: "tools/device-pod-cli.mjs", kind: "node-script" }], submodules: false, lfs: false }, + resourceBundleRef: { kind: "git", repoUrl: bundle.repoUrl, commitId: bundle.commitId, toolAliases, submodules: false, lfs: false }, providerId: "G14", backendProfile: "codex", executionPolicy: { @@ -195,4 +203,13 @@ async function readTextIfExists(filePath: string): Promise { } } +function runnerEnvValue(manifest: JsonRecord, name: string): unknown { + 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 env = containers[0]?.env as JsonRecord[]; + return env.find((entry) => entry.name === name)?.value; +} + export default selfTest;