From e275879c25b4961ad6f9e040516b45cb0d0030d6 Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 12 Jul 2026 21:12:18 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=B8=BA=20Artificer=20runner=20?= =?UTF-8?q?=E6=B3=A8=E5=85=A5=20Git=20identity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/aipods/artificer.yaml | 3 +++ src/common/aipod-specs.ts | 3 +++ src/common/types.ts | 4 ++++ src/common/validation.ts | 15 ++++++++++++++ src/mgr/kubernetes-runner-job.ts | 3 +++ src/mgr/server.ts | 2 ++ src/runner/k8s-job.ts | 19 ++++++++++++++++++ src/selftest/cases/20-runner-k8s-job.ts | 20 +++++++++++++++++++ .../cases/66-session-turn-admission.ts | 2 ++ src/selftest/cases/76-aipod-spec.ts | 10 ++++++++++ 10 files changed, 81 insertions(+) diff --git a/config/aipods/artificer.yaml b/config/aipods/artificer.yaml index 43fe5be..ecca087 100644 --- a/config/aipods/artificer.yaml +++ b/config/aipods/artificer.yaml @@ -31,6 +31,9 @@ spec: approval: never timeoutMs: 1800000 network: enabled + gitIdentity: + name: AgentRun Codex + email: agentrun-codex@users.noreply.github.com secretScope: allowCredentialEcho: false providerCredentials: diff --git a/src/common/aipod-specs.ts b/src/common/aipod-specs.ts index 0db02fd..c0e4d64 100644 --- a/src/common/aipod-specs.ts +++ b/src/common/aipod-specs.ts @@ -192,6 +192,9 @@ export function summarizeAipodSpecRecord(record: AipodSpecRecord): JsonRecord { providerId: spec.providerId ?? "G14", providerCredentials: summarizeProviderCredentials(spec.executionPolicy), toolCredentials: summarizeToolCredentials(spec.executionPolicy), + gitIdentity: spec.executionPolicy.gitIdentity + ? { ...spec.executionPolicy.gitIdentity, source: `${record.source}#spec.executionPolicy.gitIdentity`, valuesPrinted: true } + : null, resourceBundleRef: summarizeResourceBundle(spec.resourceBundleRef), dispatchDefaults: summarizeKeys(spec.dispatchDefaults), createdAt: record.createdAt, diff --git a/src/common/types.ts b/src/common/types.ts index e1bdf22..3042385 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -182,6 +182,10 @@ export interface ExecutionPolicy extends JsonRecord { approval: string; timeoutMs: number; network: string; + gitIdentity?: { + name: string; + email: string; + }; secretScope: { providerCredentials?: Array<{ profile: BackendProfile | string; diff --git a/src/common/validation.ts b/src/common/validation.ts index 559f537..fde2ed3 100644 --- a/src/common/validation.ts +++ b/src/common/validation.ts @@ -262,15 +262,30 @@ export function validateExecutionPolicy(record: JsonRecord): ExecutionPolicy { const secretScopeResult: ExecutionPolicy["secretScope"] = { allowCredentialEcho: false }; if (providerCredentials.length > 0) secretScopeResult.providerCredentials = providerCredentials as NonNullable; if (toolCredentials.length > 0) secretScopeResult.toolCredentials = toolCredentials; + const gitIdentity = record.gitIdentity === undefined ? undefined : validateGitIdentity(record.gitIdentity); return { sandbox: requiredString(record, "sandbox"), approval: requiredString(record, "approval"), timeoutMs: timeout, network: requiredString(record, "network"), + ...(gitIdentity ? { gitIdentity } : {}), secretScope: secretScopeResult, }; } +function validateGitIdentity(value: unknown): NonNullable { + const record = asRecord(value, "executionPolicy.gitIdentity"); + const name = requiredString(record, "name"); + const email = requiredString(record, "email"); + if (/[\r\n]/u.test(name)) { + throw new AgentRunError("schema-invalid", "executionPolicy.gitIdentity.name must be a single line", { httpStatus: 400 }); + } + if (/[\r\n<>]/u.test(email) || !/^[^\s@]+@[^\s@]+$/u.test(email)) { + throw new AgentRunError("schema-invalid", "executionPolicy.gitIdentity.email must be a valid single-line email address", { httpStatus: 400 }); + } + return { name, email }; +} + function validateToolCredentials(value: unknown): NonNullable { if (value === undefined) return []; if (!Array.isArray(value)) throw new AgentRunError("schema-invalid", "toolCredentials must be an array", { httpStatus: 400 }); diff --git a/src/mgr/kubernetes-runner-job.ts b/src/mgr/kubernetes-runner-job.ts index e6171a4..b6f4e6c 100644 --- a/src/mgr/kubernetes-runner-job.ts +++ b/src/mgr/kubernetes-runner-job.ts @@ -315,6 +315,9 @@ async function createKubernetesRunnerJobUnderFence(options: { store: AgentRunSto toolCredentials: summarizeToolCredentials(render.toolCredentials, render.namespace), gitTransport: gitTransportSummary({ scope: "runner" }), backendGitTransport: hasGithubSshCredential(run.executionPolicy) ? gitTransportSummary({ sshOnly: true, scope: "backend-command" }) : null, + gitIdentity: run.executionPolicy.gitIdentity + ? { ...run.executionPolicy.gitIdentity, source: "executionPolicy.gitIdentity", author: true, committer: true, valuesPrinted: true } + : null, transientEnv: summarizeTransientEnv(transientEnv), transientEnvSecret: transientEnvSecretResponse, sessionPvc: sessionPvcSummary, diff --git a/src/mgr/server.ts b/src/mgr/server.ts index 569b391..68dc569 100644 --- a/src/mgr/server.ts +++ b/src/mgr/server.ts @@ -1279,12 +1279,14 @@ function inheritSessionRunResources(runBody: JsonRecord, resourceRun: RunRecord const toolCredentials = Array.isArray(secretScope.toolCredentials) && secretScope.toolCredentials.length > 0 ? secretScope.toolCredentials : sourceToolCredentials; + const gitIdentity = asJsonRecord(executionPolicy.gitIdentity) ?? resourceRun.executionPolicy.gitIdentity; return { ...runBody, workspaceRef: resourceRun.workspaceRef, resourceBundleRef: resourceRun.resourceBundleRef, executionPolicy: { ...executionPolicy, + ...(gitIdentity ? { gitIdentity } : {}), secretScope: { ...secretScope, ...(toolCredentials && toolCredentials.length > 0 ? { toolCredentials } : {}), diff --git a/src/runner/k8s-job.ts b/src/runner/k8s-job.ts index 4cef17c..3831286 100644 --- a/src/runner/k8s-job.ts +++ b/src/runner/k8s-job.ts @@ -154,6 +154,7 @@ export function renderRunnerJobDryRun(options: RunnerJobRenderOptions): JsonReco resourceCredential: summarizeResourceCredential(render.resourceCredential, render.namespace), gitTransport: gitTransportSummary({ scope: "runner" }), backendGitTransport: hasGithubSshCredential(render.toolCredentials) ? gitTransportSummary({ sshOnly: true, scope: "backend-command" }) : null, + gitIdentity: summarizeGitIdentity(options.run.executionPolicy), transientEnv: summarizeTransientEnv(options.transientEnv ?? []), workReady: staticWorkReadyCapabilitySummary(), retention: { @@ -308,6 +309,7 @@ function runnerEnv(options: RunnerJobRenderOptions, context: { namespace: string { name: "AGENTRUN_RUNNER_POLL_INTERVAL_MS", value: "250" }, { name: "HOME", value: "/home/agentrun" }, { name: "CODEX_HOME", value: codexHome }, + ...gitIdentityEnvVars(options.run.executionPolicy), { name: "AGENTRUN_CODEX_HOME_STORAGE_KIND", value: context.sessionPvc ? "session-pvc" : "runner-home" }, ...runnerOtelEnvVars(process.env), ...runnerKafkaEnvVars(process.env), @@ -334,6 +336,23 @@ function runnerEnv(options: RunnerJobRenderOptions, context: { namespace: string return env; } +function gitIdentityEnvVars(policy: ExecutionPolicy): Array<{ name: string; value: string }> { + const identity = policy.gitIdentity; + if (!identity) return []; + return [ + { name: "GIT_AUTHOR_NAME", value: identity.name }, + { name: "GIT_AUTHOR_EMAIL", value: identity.email }, + { name: "GIT_COMMITTER_NAME", value: identity.name }, + { name: "GIT_COMMITTER_EMAIL", value: identity.email }, + ]; +} + +function summarizeGitIdentity(policy: ExecutionPolicy): JsonRecord | null { + const identity = policy.gitIdentity; + if (!identity) return null; + return { ...identity, source: "executionPolicy.gitIdentity", author: true, committer: true, valuesPrinted: true }; +} + function normalizeRunnerIdleTimeoutMs(value: number | undefined): number { if (value === undefined) return defaultRunnerIdleTimeoutMs; if (!Number.isInteger(value) || value <= 0) throw new Error("runnerIdleTimeoutMs must be a positive integer"); diff --git a/src/selftest/cases/20-runner-k8s-job.ts b/src/selftest/cases/20-runner-k8s-job.ts index 929a988..2a4002c 100644 --- a/src/selftest/cases/20-runner-k8s-job.ts +++ b/src/selftest/cases/20-runner-k8s-job.ts @@ -66,6 +66,26 @@ const selfTest: SelfTestCase = async (context) => { assert.deepEqual((((rendered.transientEnv as JsonRecord).names) as string[]), ["HWLAB_API_KEY"]); assertNoSecretLeak(rendered); + const gitIdentityRendered = renderRunnerJobDryRun({ + run: { + ...baseRun, + executionPolicy: { + ...baseRun.executionPolicy, + gitIdentity: { name: "AgentRun Codex", email: "agentrun-codex@users.noreply.github.com" }, + }, + }, + commandId: item.commandId, + managerUrl: server.baseUrl, + runnerApiKeySecretRef, + image: "127.0.0.1:5000/agentrun/agentrun-mgr@sha256:1111111111111111111111111111111111111111111111111111111111111111", + }); + const gitIdentityManifest = gitIdentityRendered.manifest as JsonRecord; + assert.equal(runnerEnvValue(gitIdentityManifest, "GIT_AUTHOR_NAME"), "AgentRun Codex"); + assert.equal(runnerEnvValue(gitIdentityManifest, "GIT_AUTHOR_EMAIL"), "agentrun-codex@users.noreply.github.com"); + assert.equal(runnerEnvValue(gitIdentityManifest, "GIT_COMMITTER_NAME"), "AgentRun Codex"); + assert.equal(runnerEnvValue(gitIdentityManifest, "GIT_COMMITTER_EMAIL"), "agentrun-codex@users.noreply.github.com"); + assert.deepEqual(gitIdentityRendered.gitIdentity, { name: "AgentRun Codex", email: "agentrun-codex@users.noreply.github.com", source: "executionPolicy.gitIdentity", author: true, committer: true, valuesPrinted: true }); + const resourceCredentialOptions: Parameters[0] = { run: { ...baseRun, diff --git a/src/selftest/cases/66-session-turn-admission.ts b/src/selftest/cases/66-session-turn-admission.ts index 92ac0ad..3fd2334 100644 --- a/src/selftest/cases/66-session-turn-admission.ts +++ b/src/selftest/cases/66-session-turn-admission.ts @@ -313,6 +313,7 @@ async function assertSessionFollowUpInheritsAipodResources(kubectlCommand: strin assert.deepEqual(run.workspaceRef, initial.workspaceRef); assert.deepEqual(run.resourceBundleRef, initial.resourceBundleRef); assert.deepEqual(run.executionPolicy.secretScope.toolCredentials, initial.executionPolicy.secretScope.toolCredentials); + assert.deepEqual(run.executionPolicy.gitIdentity, initial.executionPolicy.gitIdentity); assert.deepEqual(run.executionPolicy.secretScope.providerCredentials, runInput(sessionId).executionPolicy.secretScope.providerCredentials); } finally { await closeServer(server.server); @@ -411,6 +412,7 @@ function artificerRunInput(sessionId: string): CreateRunInput { }, executionPolicy: { ...input.executionPolicy, + gitIdentity: { name: "AgentRun Codex", email: "agentrun-codex@users.noreply.github.com" }, secretScope: { ...input.executionPolicy.secretScope, toolCredentials: [ diff --git a/src/selftest/cases/76-aipod-spec.ts b/src/selftest/cases/76-aipod-spec.ts index 889597f..1fad5c5 100644 --- a/src/selftest/cases/76-aipod-spec.ts +++ b/src/selftest/cases/76-aipod-spec.ts @@ -51,6 +51,12 @@ const selfTest: SelfTestCase = async (context) => { assert.equal(shownItem.lane, "v0.2"); assert.equal(((shownItem.model as JsonRecord).model), "gpt-5.6-terra"); assert.equal(((shownItem.model as JsonRecord).reasoningEffort), "xhigh"); + assert.deepEqual(shownItem.gitIdentity, { + name: "AgentRun Codex", + email: "agentrun-codex@users.noreply.github.com", + source: `${artificerSpecFile}#spec.executionPolicy.gitIdentity`, + valuesPrinted: true, + }); const shownModelDefaults = shownItem.modelDefaults as JsonRecord; assert.equal(shownModelDefaults.modelSource, "aipod-spec-default"); assert.equal(shownModelDefaults.reasoningEffortSource, "aipod-spec-default"); @@ -98,6 +104,7 @@ const selfTest: SelfTestCase = async (context) => { assert.equal(modelResolution.backendProfileSource, "aipod-spec"); assert.equal(modelResolution.valuesPrinted, false); const policy = task.executionPolicy as JsonRecord; + assert.deepEqual(policy.gitIdentity, { name: "AgentRun Codex", email: "agentrun-codex@users.noreply.github.com" }); const secretScope = policy.secretScope as JsonRecord; const providerCredentials = secretScope.providerCredentials as JsonRecord[]; const providerCredential = providerCredentials.find((item) => item.profile === "gpt-pika") as JsonRecord | undefined; @@ -166,6 +173,9 @@ const selfTest: SelfTestCase = async (context) => { const payloadDefaultsBypassDocument = structuredClone(originalSpecDocument); ((payloadDefaultsBypassDocument.spec as JsonRecord).payloadDefaults as JsonRecord).modelConfig = { model: "bypass" }; assert.throws(() => validateAipodSpec(payloadDefaultsBypassDocument, "selftest-payload-defaults-bypass"), /cannot set Aipod control fields: modelConfig/u); + const invalidGitIdentityDocument = structuredClone(originalSpecDocument); + (((invalidGitIdentityDocument.spec as JsonRecord).executionPolicy as JsonRecord).gitIdentity as JsonRecord).email = "not-an-email"; + assert.throws(() => validateAipodSpec(invalidGitIdentityDocument, "selftest-invalid-git-identity"), /valid single-line email address/u); const mirrored = resolveGitBundleFetchSource("git@github.com:pikasTech/unidesk.git", { baseUrl: "http://mirror.example.test/root/" }, {}); assert.equal(mirrored.fetchRepoUrl, "http://mirror.example.test/root/pikasTech/unidesk.git");