fix: 为 Artificer runner 注入 Git identity

This commit is contained in:
Codex
2026-07-12 21:12:18 +02:00
parent 242d919277
commit e275879c25
10 changed files with 81 additions and 0 deletions
+3
View File
@@ -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:
+3
View File
@@ -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,
+4
View File
@@ -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;
+15
View File
@@ -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<ExecutionPolicy["secretScope"]["providerCredentials"]>;
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<ExecutionPolicy["gitIdentity"]> {
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<ExecutionPolicy["secretScope"]["toolCredentials"]> {
if (value === undefined) return [];
if (!Array.isArray(value)) throw new AgentRunError("schema-invalid", "toolCredentials must be an array", { httpStatus: 400 });
+3
View File
@@ -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,
+2
View File
@@ -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 } : {}),
+19
View File
@@ -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");
+20
View File
@@ -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<typeof renderRunnerJobDryRun>[0] = {
run: {
...baseRun,
@@ -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: [
+10
View File
@@ -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");