feat: 支持 runner tool credential 装配

This commit is contained in:
Codex
2026-06-02 00:22:38 +08:00
parent ac14a06436
commit 159b99e763
10 changed files with 163 additions and 14 deletions
+32 -3
View File
@@ -12,7 +12,13 @@ const selfTest: SelfTestCase = async (context) => {
const server = await startManagerServer({ port: 0, host: "127.0.0.1", sourceCommit: "self-test", store: new MemoryAgentRunStore() });
try {
const client = new ManagerClient(server.baseUrl);
const item = await createRunWithCommand(client, context, "job smoke", "selftest-job-render", 15_000);
const githubToolCredentials = [{
tool: "github",
purpose: "pull-request",
secretRef: { name: "agentrun-v01-tool-github-pr", keys: ["GH_TOKEN"] },
projection: { kind: "env", envName: "GH_TOKEN", secretKey: "GH_TOKEN" },
}];
const item = await createRunWithCommand(client, { ...context, toolCredentials: githubToolCredentials }, "job smoke", "selftest-job-render", 15_000);
const rendered = renderRunnerJobDryRun({
run: await client.get(`/api/v1/runs/${item.runId}`) as RunRecord,
commandId: item.commandId,
@@ -27,6 +33,7 @@ const selfTest: SelfTestCase = async (context) => {
assert.equal(((rendered.retention as JsonRecord).ttlSecondsAfterFinished), 86_400);
assert.equal((rendered.jobIdentity as { serviceAccountName?: string }).serviceAccountName, "agentrun-v01-runner");
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");
assert.equal(runnerEnvValue(rendered.manifest as JsonRecord, "HWLAB_DEVICE_POD_SESSION_TOKEN"), "REDACTED");
assert.deepEqual((((rendered.transientEnv as JsonRecord).names) as string[]), ["HWLAB_DEVICE_POD_SESSION_TOKEN"]);
assertNoSecretLeak(rendered);
@@ -70,7 +77,7 @@ console.log(JSON.stringify({ apiVersion: manifest.apiVersion, kind: manifest.kin
});
try {
const jobClient = new ManagerClient(serverWithKubectl.baseUrl);
const jobItem = await createRunWithCommand(jobClient, context, "job create smoke", "selftest-job-create", 15_000);
const jobItem = await createRunWithCommand(jobClient, { ...context, toolCredentials: githubToolCredentials }, "job create smoke", "selftest-job-create", 15_000);
const created = await jobClient.post(`/api/v1/runs/${jobItem.runId}/runner-jobs`, {
commandId: jobItem.commandId,
attemptId: "attempt_selftest_create",
@@ -86,11 +93,12 @@ console.log(JSON.stringify({ apiVersion: manifest.apiVersion, kind: manifest.kin
assert.equal((manifest.spec as JsonRecord).ttlSecondsAfterFinished, 86_400);
assert.equal(runnerEnvValue(manifest, "HWLAB_DEVICE_POD_SESSION_TOKEN"), "test-token-material");
assert.equal(runnerEnvValue(manifest, "HWLAB_CLOUD_API_URL"), "http://cloud.test");
assertRunnerJobUsesToolCredential({ manifest, toolCredentials: (created as JsonRecord).toolCredentials } as JsonRecord, "GH_TOKEN", "agentrun-v01-tool-github-pr", "GH_TOKEN");
assertNoSecretLeak(created);
} finally {
await new Promise<void>((resolve) => serverWithKubectl.server.close(() => resolve()));
}
return { name: "runner-k8s-job", tests: ["runner-k8s-job-dry-run", "runner-k8s-job-deepseek-profile-dry-run", "runner-k8s-job-create-api", "runner-k8s-job-retention-ttl", "runner-job-transient-env"] };
return { name: "runner-k8s-job", tests: ["runner-k8s-job-dry-run", "runner-k8s-job-deepseek-profile-dry-run", "runner-k8s-job-create-api", "runner-k8s-job-retention-ttl", "runner-job-transient-env", "runner-job-tool-credential-env"] };
} finally {
await new Promise<void>((resolve) => server.server.close(() => resolve()));
}
@@ -108,6 +116,27 @@ function runnerEnvValue(manifest: JsonRecord, name: string): unknown {
return env.find((item) => item.name === name)?.value;
}
function assertRunnerJobUsesToolCredential(rendered: JsonRecord, envName: string, secretName: string, secretKey: 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 env = runner.env as JsonRecord[];
const entry = env.find((item) => item.name === envName) as JsonRecord | undefined;
assert.ok(entry, `${envName} env should be projected from a SecretRef`);
assert.equal(entry.value, undefined);
const valueFrom = entry.valueFrom as JsonRecord;
const secretKeyRef = valueFrom.secretKeyRef as JsonRecord;
assert.equal(secretKeyRef.name, secretName);
assert.equal(secretKeyRef.key, secretKey);
const summary = rendered.toolCredentials as JsonRecord;
assert.equal(summary.valuesPrinted, false);
assert.equal(summary.count, 1);
}
function assertRunnerJobUsesWritableCodexHome(manifest: JsonRecord, expectedCodexHome: string, volumeName: string, projectionPath: string): void {
const spec = manifest.spec as JsonRecord;
const template = spec.template as JsonRecord;
+8 -2
View File
@@ -25,6 +25,8 @@ export interface SelfTestResult {
export type SelfTestCase = (context: SelfTestContext) => Promise<SelfTestResult> | SelfTestResult;
type SelfTestRunContext = Pick<SelfTestContext, "workspace" | "codexHome"> & Partial<Pick<SelfTestContext, "deepseekHome">> & { backendProfile?: BackendProfile; includeOnlyProfile?: BackendProfile; toolCredentials?: JsonRecord[] };
export async function createSelfTestContext(root: string): Promise<SelfTestContext> {
const tmp = await mkdtemp(path.join(os.tmpdir(), "agentrun-selftest-"));
const codexHome = path.join(tmp, "codex-home");
@@ -52,7 +54,7 @@ export async function createSelfTestContext(root: string): Promise<SelfTestConte
};
}
export async function createRunWithCommand(client: ManagerClient, context: Pick<SelfTestContext, "workspace" | "codexHome"> & Partial<Pick<SelfTestContext, "deepseekHome">> & { backendProfile?: BackendProfile; includeOnlyProfile?: BackendProfile }, prompt: string, idempotencyKey: string, timeoutMs: number): Promise<{ runId: string; commandId: string }> {
export async function createRunWithCommand(client: ManagerClient, context: SelfTestRunContext, prompt: string, idempotencyKey: string, timeoutMs: number): Promise<{ runId: string; commandId: string }> {
const backendProfile = context.backendProfile ?? "codex";
const run = await client.post("/api/v1/runs", {
tenantId: "unidesk",
@@ -65,7 +67,7 @@ export async function createRunWithCommand(client: ManagerClient, context: Pick<
approval: "never",
timeoutMs,
network: "default",
secretScope: { allowCredentialEcho: false, providerCredentials: providerCredentials(context, backendProfile) },
secretScope: { allowCredentialEcho: false, providerCredentials: providerCredentials(context, backendProfile), ...toolCredentialScope(context) },
},
traceSink: null,
}) as { id: string };
@@ -75,6 +77,10 @@ export async function createRunWithCommand(client: ManagerClient, context: Pick<
return { runId: run.id, commandId: command.id };
}
function toolCredentialScope(context: { toolCredentials?: JsonRecord[] }): JsonRecord {
return context.toolCredentials ? { toolCredentials: context.toolCredentials } : {};
}
function providerCredentials(context: Pick<SelfTestContext, "codexHome"> & Partial<Pick<SelfTestContext, "deepseekHome">> & { includeOnlyProfile?: BackendProfile }, backendProfile: BackendProfile): JsonRecord[] {
const profiles: BackendProfile[] = context.includeOnlyProfile ? [context.includeOnlyProfile] : [backendProfile];
return profiles.map((profile) => ({