Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 517d8d2b54 | |||
| 56beca2a57 |
@@ -276,6 +276,7 @@ function runnerEnv(options: RunnerJobRenderOptions, context: { namespace: string
|
||||
const selectedSecret = context.secretRefs.find((item) => item.profile === options.run.backendProfile);
|
||||
const codexHome = runnerCodexHome(options.run.backendProfile, selectedSecret, context.sessionPvc);
|
||||
const bootRepoUrl = optionalString(options.bootRepoUrl) ?? defaultBootRepoUrl;
|
||||
const resourceBundleRepositoriesJson = optionalString(process.env.AGENTRUN_RESOURCE_BUNDLE_REPOSITORIES_JSON);
|
||||
const env = dedupeEnvVars([
|
||||
{ name: "AGENTRUN_MGR_URL", value: options.managerUrl },
|
||||
{ name: "AGENTRUN_API_KEY", valueFrom: { secretKeyRef: context.runnerApiKeySecretRef } },
|
||||
@@ -297,7 +298,7 @@ function runnerEnv(options: RunnerJobRenderOptions, context: { namespace: string
|
||||
{ name: "AGENTRUN_BOOT_MODE", value: "runner" },
|
||||
{ name: "AGENTRUN_APP_ROOT", value: "/home/agentrun/agentrun-source" },
|
||||
{ name: "AGENTRUN_RUNTIME_NAMESPACE", value: context.namespace },
|
||||
...(optionalString(process.env.AGENTRUN_GIT_MIRROR_BASE_URL) ? [{ name: "AGENTRUN_GIT_MIRROR_BASE_URL", value: optionalString(process.env.AGENTRUN_GIT_MIRROR_BASE_URL) }] : []),
|
||||
...(resourceBundleRepositoriesJson ? [{ name: "AGENTRUN_RESOURCE_BUNDLE_REPOSITORIES_JSON", value: resourceBundleRepositoriesJson }] : []),
|
||||
{ name: "AGENTRUN_K8S_JOB_NAME", value: context.jobName },
|
||||
{ name: "AGENTRUN_LOG_PATH", value: "/tmp/agentrun-runner.jsonl" },
|
||||
{ name: "AGENTRUN_WORK_READY_VERSION", value: String(staticWorkReadyCapabilitySummary().version) },
|
||||
|
||||
+122
-32
@@ -58,15 +58,17 @@ interface GitCheckout {
|
||||
branch: string | null;
|
||||
}
|
||||
|
||||
interface GitMirrorConfig {
|
||||
baseUrl: string;
|
||||
interface ResourceBundleRepositoryMapping {
|
||||
repository: string;
|
||||
normalizedRepository: string;
|
||||
readUrl: string;
|
||||
}
|
||||
|
||||
interface GitBundleSource {
|
||||
repoUrl: string;
|
||||
commitId?: string;
|
||||
ref?: string;
|
||||
gitMirror?: GitMirrorConfig;
|
||||
repositoryMappings?: readonly ResourceBundleRepositoryMapping[];
|
||||
credentialRefPresent: boolean;
|
||||
credentialEnv?: NodeJS.ProcessEnv;
|
||||
}
|
||||
@@ -145,7 +147,6 @@ export async function materializeResourceBundle(resourceBundleRef: ResourceBundl
|
||||
const assemblyContractHash = primaryAssemblyContractHash(resourceBundleRef, workspaceRef);
|
||||
const bundleContractHash = stableHash(resourceBundleRef.bundles);
|
||||
const initialState = await inspectPrimaryWorkspace(workspacePath, declaredSource, assemblyContractHash, bundleContractHash);
|
||||
const gitMirror = gitMirrorConfig(resourceBundleRef, env);
|
||||
let provenance: PrimaryWorkspaceProvenance;
|
||||
let materializedBundles: MaterializedGitBundle[];
|
||||
let checkoutPath: string | null = null;
|
||||
@@ -159,7 +160,8 @@ export async function materializeResourceBundle(resourceBundleRef: ResourceBundl
|
||||
await rm(checkoutRoot, { recursive: true, force: true });
|
||||
await mkdir(checkoutRoot, { recursive: true });
|
||||
const credentialEnv = await resourceCredentialEnv(resourceBundleRef, env);
|
||||
const defaultSource = defaultGitBundleSource(resourceBundleRef, env, gitMirror, credentialEnv);
|
||||
const repositoryMappings = resourceBundleRepositoryMappings(resourceBundleRef, env);
|
||||
const defaultSource = defaultGitBundleSource(resourceBundleRef, repositoryMappings, credentialEnv);
|
||||
const checkoutCache = new Map<string, Promise<GitCheckout>>();
|
||||
const checkoutFor = (source: GitBundleSource, sourceContext: GitBundleSourceContext) => {
|
||||
const key = stableHash(gitSourceIdentity(source));
|
||||
@@ -214,7 +216,7 @@ export async function materializeResourceBundle(resourceBundleRef: ResourceBundl
|
||||
fetchRepoUrl: resolvedSource.fetchRepoUrl,
|
||||
mirrorUsed: resolvedSource.mirrorUsed,
|
||||
mirrorBaseUrl: resolvedSource.mirrorBaseUrl,
|
||||
gitMirror: resolvedSource.mirrorUsed ? { enabled: true, baseUrl: resolvedSource.mirrorBaseUrl, valuesPrinted: false } : { enabled: false, baseUrl: null, valuesPrinted: false },
|
||||
repositoryAuthority: resolvedSource.mirrorUsed ? { repository: githubRepositoryIdentity(resourceBundleRef.repoUrl), mode: "exact-read-url", valuesPrinted: false } : null,
|
||||
commitId: resolvedSource.commitId,
|
||||
requestedCommitId: resourceBundleRef.commitId ?? null,
|
||||
requestedRef: resourceBundleRef.ref ?? null,
|
||||
@@ -254,33 +256,22 @@ function isSameOrChildPath(candidate: string, parent: string): boolean {
|
||||
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
||||
}
|
||||
|
||||
function gitMirrorConfig(resourceBundleRef: ResourceBundleRef, env: NodeJS.ProcessEnv): GitMirrorConfig | undefined {
|
||||
void resourceBundleRef;
|
||||
return defaultGitMirrorConfig(env);
|
||||
}
|
||||
|
||||
function defaultGitMirrorConfig(env: NodeJS.ProcessEnv): GitMirrorConfig {
|
||||
const baseUrl = optionalNonEmpty(env.AGENTRUN_GIT_MIRROR_BASE_URL) ?? "http://git-mirror-http.devops-infra.svc.cluster.local";
|
||||
return { baseUrl: normalizeMirrorBaseUrl(baseUrl) };
|
||||
}
|
||||
|
||||
function defaultGitBundleSource(resourceBundleRef: ResourceBundleRef, env: NodeJS.ProcessEnv, gitMirror?: GitMirrorConfig, credentialEnv?: NodeJS.ProcessEnv): GitBundleSource {
|
||||
void env;
|
||||
function defaultGitBundleSource(resourceBundleRef: ResourceBundleRef, repositoryMappings?: readonly ResourceBundleRepositoryMapping[], credentialEnv?: NodeJS.ProcessEnv): GitBundleSource {
|
||||
const ref = optionalNonEmpty(resourceBundleRef.ref);
|
||||
const commitId = optionalNonEmpty(resourceBundleRef.commitId);
|
||||
const credentialRefPresent = resourceBundleRef.credentialRef !== undefined;
|
||||
if (!ref && !commitId) throw new AgentRunError("schema-invalid", "gitbundle primary source must declare ref or commitId", { httpStatus: 400 });
|
||||
return { repoUrl: resourceBundleRef.repoUrl, ...(ref ? { ref } : {}), ...(commitId ? { commitId } : {}), credentialRefPresent, ...(gitMirror ? { gitMirror } : {}), ...(credentialEnv ? { credentialEnv } : {}) };
|
||||
return { repoUrl: resourceBundleRef.repoUrl, ...(ref ? { ref } : {}), ...(commitId ? { commitId } : {}), credentialRefPresent, ...(repositoryMappings ? { repositoryMappings } : {}), ...(credentialEnv ? { credentialEnv } : {}) };
|
||||
}
|
||||
|
||||
function bundleGitSource(bundle: ResourceBundleRef["bundles"][number], resourceBundleRef: ResourceBundleRef, defaultSource: GitBundleSource): GitBundleSource {
|
||||
const repoUrl = bundle.repoUrl ?? resourceBundleRef.repoUrl;
|
||||
const ref = optionalNonEmpty(bundle.ref);
|
||||
const mirror = defaultSource.gitMirror;
|
||||
const repositoryMappings = defaultSource.repositoryMappings;
|
||||
const credentialRefPresent = defaultSource.credentialRefPresent;
|
||||
const credentialEnv = defaultSource.credentialEnv;
|
||||
const commitId = optionalNonEmpty(bundle.commitId);
|
||||
if (ref || commitId) return { repoUrl, ...(ref ? { ref } : {}), ...(commitId ? { commitId } : {}), credentialRefPresent, ...(mirror ? { gitMirror: mirror } : {}), ...(credentialEnv ? { credentialEnv } : {}) };
|
||||
if (ref || commitId) return { repoUrl, ...(ref ? { ref } : {}), ...(commitId ? { commitId } : {}), credentialRefPresent, ...(repositoryMappings ? { repositoryMappings } : {}), ...(credentialEnv ? { credentialEnv } : {}) };
|
||||
if (repoUrl === defaultSource.repoUrl) return defaultSource;
|
||||
throw new AgentRunError("schema-invalid", "cross-repository bundle must declare its own ref or commitId", { httpStatus: 400 });
|
||||
}
|
||||
@@ -327,7 +318,12 @@ async function checkoutGitSource(checkoutRoot: string, source: GitBundleSource,
|
||||
}
|
||||
|
||||
function gitSourceIdentity(source: GitBundleSource): JsonRecord {
|
||||
return { repoUrl: source.repoUrl, commitId: source.commitId ?? null, ref: source.ref ?? null, gitMirror: source.gitMirror ? { baseUrl: source.gitMirror.baseUrl } : null };
|
||||
const repositoryAuthority = source.repositoryMappings?.map((mapping) => ({
|
||||
repository: mapping.repository,
|
||||
normalizedRepository: mapping.normalizedRepository,
|
||||
readUrl: mapping.readUrl,
|
||||
})) ?? null;
|
||||
return { repoUrl: source.repoUrl, commitId: source.commitId ?? null, ref: source.ref ?? null, repositoryAuthorityHash: repositoryAuthority ? stableHash(repositoryAuthority) : null };
|
||||
}
|
||||
|
||||
async function resourceCredentialEnv(resourceBundleRef: ResourceBundleRef, env: NodeJS.ProcessEnv): Promise<NodeJS.ProcessEnv | undefined> {
|
||||
@@ -744,19 +740,113 @@ async function verifyPrimaryWorkspace(workspacePath: string, workspaceRef: Works
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveGitBundleFetchSource(repoUrl: string, gitMirror?: GitMirrorConfig, env: NodeJS.ProcessEnv = process.env): { fetchRepoUrl: string; mirrorUsed: boolean; mirrorBaseUrl?: string } {
|
||||
const mirror = gitMirror ? { baseUrl: normalizeMirrorBaseUrl(gitMirror.baseUrl) } : defaultGitMirrorConfig(env);
|
||||
const githubPath = githubRepoPath(repoUrl);
|
||||
if (!githubPath) return { fetchRepoUrl: repoUrl, mirrorUsed: false };
|
||||
return { fetchRepoUrl: `${mirror.baseUrl}/${githubPath}.git`, mirrorUsed: true, mirrorBaseUrl: mirror.baseUrl };
|
||||
}
|
||||
|
||||
function normalizeMirrorBaseUrl(baseUrl: string): string {
|
||||
return baseUrl.replace(/\/+$/u, "");
|
||||
export function resolveGitBundleFetchSource(repoUrl: string, env: NodeJS.ProcessEnv = process.env): { fetchRepoUrl: string; mirrorUsed: boolean; mirrorBaseUrl?: string } {
|
||||
const repository = githubRepositoryIdentity(repoUrl);
|
||||
if (!repository) return { fetchRepoUrl: repoUrl, mirrorUsed: false };
|
||||
return resolveGitBundleFetchSourceFromMappings(repository, parseResourceBundleRepositoryMappings(env));
|
||||
}
|
||||
|
||||
function gitFetchSource(source: GitBundleSource): { fetchRepoUrl: string; mirrorUsed: boolean; mirrorBaseUrl?: string } {
|
||||
return resolveGitBundleFetchSource(source.repoUrl, source.gitMirror);
|
||||
const repository = githubRepositoryIdentity(source.repoUrl);
|
||||
if (!repository) return { fetchRepoUrl: source.repoUrl, mirrorUsed: false };
|
||||
return resolveGitBundleFetchSourceFromMappings(repository, source.repositoryMappings ?? []);
|
||||
}
|
||||
|
||||
function resourceBundleRepositoryMappings(resourceBundleRef: ResourceBundleRef, env: NodeJS.ProcessEnv): readonly ResourceBundleRepositoryMapping[] | undefined {
|
||||
const repoUrls = [resourceBundleRef.repoUrl, ...resourceBundleRef.bundles.map((bundle) => bundle.repoUrl ?? resourceBundleRef.repoUrl)];
|
||||
if (!repoUrls.some((repoUrl) => githubRepositoryIdentity(repoUrl) !== null)) return undefined;
|
||||
return parseResourceBundleRepositoryMappings(env);
|
||||
}
|
||||
|
||||
function parseResourceBundleRepositoryMappings(env: NodeJS.ProcessEnv): readonly ResourceBundleRepositoryMapping[] {
|
||||
const envName = "AGENTRUN_RESOURCE_BUNDLE_REPOSITORIES_JSON";
|
||||
const raw = optionalNonEmpty(env[envName]);
|
||||
if (!raw) return [];
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(raw);
|
||||
} catch (error) {
|
||||
throw repositoryMappingError("resource-bundle-repositories-invalid-json", `${envName} is not valid JSON`, {
|
||||
error: fileErrorSummary(error),
|
||||
});
|
||||
}
|
||||
if (!Array.isArray(parsed)) {
|
||||
throw repositoryMappingError("resource-bundle-repositories-invalid-shape", `${envName} must be an array`, {
|
||||
actualType: parsed === null ? "null" : typeof parsed,
|
||||
});
|
||||
}
|
||||
return parsed.map((entry, index) => {
|
||||
if (!isJsonObject(entry)) {
|
||||
throw repositoryMappingError("resource-bundle-repository-invalid-entry", `${envName}[${index}] must be an object`, { index });
|
||||
}
|
||||
const repository = optionalNonEmpty(entry.repository);
|
||||
const normalizedRepository = repository ? normalizeRepositoryIdentity(repository) : null;
|
||||
if (!repository || !normalizedRepository) {
|
||||
throw repositoryMappingError("resource-bundle-repository-invalid-entry", `${envName}[${index}].repository must be an owner/repo identity`, { index });
|
||||
}
|
||||
const readUrl = optionalNonEmpty(entry.readUrl);
|
||||
if (!readUrl || !isSupportedRepositoryReadUrl(readUrl)) {
|
||||
throw repositoryMappingError("resource-bundle-repository-invalid-entry", `${envName}[${index}].readUrl must be a credential-free Git URL`, { index, repository: normalizedRepository });
|
||||
}
|
||||
return { repository, normalizedRepository, readUrl };
|
||||
});
|
||||
}
|
||||
|
||||
function resolveGitBundleFetchSourceFromMappings(repository: string, mappings: readonly ResourceBundleRepositoryMapping[]): { fetchRepoUrl: string; mirrorUsed: boolean; mirrorBaseUrl?: string } {
|
||||
const matches = mappings.filter((mapping) => mapping.normalizedRepository === repository);
|
||||
if (matches.length === 0) {
|
||||
throw repositoryMappingError("resource-bundle-repository-zero-match", `resource bundle repository ${repository} has no declared read URL`, {
|
||||
repository,
|
||||
mappingCount: mappings.length,
|
||||
matchCount: 0,
|
||||
});
|
||||
}
|
||||
if (matches.length !== 1) {
|
||||
throw repositoryMappingError("resource-bundle-repository-multiple-matches", `resource bundle repository ${repository} has multiple declared read URLs`, {
|
||||
repository,
|
||||
mappingCount: mappings.length,
|
||||
matchCount: matches.length,
|
||||
});
|
||||
}
|
||||
return { fetchRepoUrl: matches[0]!.readUrl, mirrorUsed: true };
|
||||
}
|
||||
|
||||
function repositoryMappingError(reason: string, message: string, details: JsonRecord): AgentRunError {
|
||||
return new AgentRunError("schema-invalid", message, {
|
||||
httpStatus: 400,
|
||||
details: {
|
||||
reason,
|
||||
environment: "AGENTRUN_RESOURCE_BUNDLE_REPOSITORIES_JSON",
|
||||
...details,
|
||||
valuesPrinted: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function isSupportedRepositoryReadUrl(value: string): boolean {
|
||||
if (/\s/u.test(value)) return false;
|
||||
if (/^git@[^:/\s]+:.+/u.test(value)) return true;
|
||||
try {
|
||||
const parsed = new URL(value);
|
||||
if (!parsed.hostname || parsed.password) return false;
|
||||
if (parsed.username && !(parsed.protocol === "ssh:" && parsed.username === "git")) return false;
|
||||
return ["http:", "https:", "ssh:", "git:"].includes(parsed.protocol);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function githubRepositoryIdentity(repoUrl: string): string | null {
|
||||
const githubPath = githubRepoPath(repoUrl);
|
||||
return githubPath ? normalizeRepositoryIdentity(githubPath) : null;
|
||||
}
|
||||
|
||||
function normalizeRepositoryIdentity(value: string): string | null {
|
||||
const match = /^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+?)(?:\.git)?$/u.exec(value.trim());
|
||||
if (!match) return null;
|
||||
const owner = match[1]?.toLowerCase();
|
||||
const repo = match[2]?.toLowerCase();
|
||||
return owner && repo ? `${owner}/${repo}` : null;
|
||||
}
|
||||
|
||||
function githubRepoPath(repoUrl: string): string | null {
|
||||
|
||||
@@ -37,7 +37,11 @@ const selfTest: SelfTestCase = async (context) => {
|
||||
const combinedToolCredentials = [...githubToolCredentials, ...unideskSshToolCredentials, ...githubSshToolCredentials];
|
||||
const item = await createRunWithCommand(client, { ...context, toolCredentials: combinedToolCredentials }, "job smoke", "selftest-job-render", 15_000);
|
||||
const baseRun = await client.get(`/api/v1/runs/${item.runId}`) as RunRecord;
|
||||
const rendered = renderRunnerJobDryRun({
|
||||
const repositoryMappings = JSON.stringify([{ repository: "pikasTech/unidesk", readUrl: "http://gitea.example.test/mirrors/pikasTech-unidesk.git" }]);
|
||||
const rendered = withProcessEnv({
|
||||
AGENTRUN_RESOURCE_BUNDLE_REPOSITORIES_JSON: repositoryMappings,
|
||||
AGENTRUN_GIT_MIRROR_BASE_URL: "http://legacy-base.example.test",
|
||||
}, () => renderRunnerJobDryRun({
|
||||
run: baseRun,
|
||||
commandId: item.commandId,
|
||||
managerUrl: server.baseUrl,
|
||||
@@ -46,7 +50,7 @@ const selfTest: SelfTestCase = async (context) => {
|
||||
attemptId: "attempt_selftest",
|
||||
sourceCommit: "self-test",
|
||||
transientEnv: [{ name: "HWLAB_API_KEY", value: "hwl_live_selftest", sensitive: true }],
|
||||
});
|
||||
}));
|
||||
assert.equal(rendered.dryRun, true);
|
||||
assert.equal(rendered.mutation, false);
|
||||
assert.equal(((rendered.retention as JsonRecord).ttlSecondsAfterFinished), 604_800);
|
||||
@@ -60,6 +64,8 @@ const selfTest: SelfTestCase = async (context) => {
|
||||
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, "AGENTRUN_RESOURCE_BUNDLE_REPOSITORIES_JSON"), repositoryMappings);
|
||||
assert.equal(runnerEnvValue(rendered.manifest as JsonRecord, "AGENTRUN_GIT_MIRROR_BASE_URL"), undefined);
|
||||
assert.deepEqual(runnerEnvSecretKeyRef(rendered.manifest as JsonRecord, "AGENTRUN_API_KEY"), runnerApiKeySecretRef);
|
||||
assert.deepEqual(rendered.runnerApiKeySecretRef, { ...runnerApiKeySecretRef, valuesPrinted: false });
|
||||
assert.equal(runnerEnvValue(rendered.manifest as JsonRecord, "HWLAB_API_KEY"), "REDACTED");
|
||||
@@ -581,7 +587,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/.agentrun-sessions/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-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-resource-credential-volume", "runner-job-unidesk-ssh-endpoint-auto-env", "runner-job-unidesk-ssh-transient-env-denied", "runner-job-pre-create-retention", "runner-job-pre-create-retention-protects-stale-active", "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-resource-bundle-repositories-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-resource-credential-volume", "runner-job-unidesk-ssh-endpoint-auto-env", "runner-job-unidesk-ssh-transient-env-denied", "runner-job-pre-create-retention", "runner-job-pre-create-retention-protects-stale-active", "runner-k8s-job-session-pvc-volume-and-env"] };
|
||||
} finally {
|
||||
await new Promise<void>((resolve) => server.server.close(() => resolve()));
|
||||
}
|
||||
@@ -589,6 +595,19 @@ process.exit(1);
|
||||
|
||||
export default selfTest;
|
||||
|
||||
function withProcessEnv<T>(values: Record<string, string>, operation: () => T): T {
|
||||
const previous = new Map(Object.keys(values).map((name) => [name, process.env[name]]));
|
||||
for (const [name, value] of Object.entries(values)) process.env[name] = value;
|
||||
try {
|
||||
return operation();
|
||||
} finally {
|
||||
for (const [name, value] of previous) {
|
||||
if (value === undefined) delete process.env[name];
|
||||
else process.env[name] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function runnerEnvEntry(manifest: JsonRecord, name: string): JsonRecord | undefined {
|
||||
const spec = manifest.spec as JsonRecord;
|
||||
const template = spec.template as JsonRecord;
|
||||
|
||||
@@ -300,13 +300,14 @@ const selfTest: SelfTestCase = async (context) => {
|
||||
(((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");
|
||||
const repositoryMappings = JSON.stringify([{ repository: "pikasTech/unidesk", readUrl: "http://mirror.example.test/mirrors/pikasTech-unidesk.git" }]);
|
||||
const mirrored = resolveGitBundleFetchSource("git@github.com:pikasTech/unidesk.git", { AGENTRUN_RESOURCE_BUNDLE_REPOSITORIES_JSON: repositoryMappings });
|
||||
assert.equal(mirrored.fetchRepoUrl, "http://mirror.example.test/mirrors/pikasTech-unidesk.git");
|
||||
assert.equal(mirrored.mirrorUsed, true);
|
||||
const defaultMirror = resolveGitBundleFetchSource("https://github.com/pikasTech/unidesk.git", undefined, { AGENTRUN_GIT_MIRROR_BASE_URL: "http://mirror.example.test/base" });
|
||||
assert.equal(defaultMirror.fetchRepoUrl, "http://mirror.example.test/base/pikasTech/unidesk.git");
|
||||
const defaultMirror = resolveGitBundleFetchSource("https://github.com/pikasTech/unidesk.git", { AGENTRUN_RESOURCE_BUNDLE_REPOSITORIES_JSON: repositoryMappings });
|
||||
assert.equal(defaultMirror.fetchRepoUrl, "http://mirror.example.test/mirrors/pikasTech-unidesk.git");
|
||||
assert.equal(defaultMirror.mirrorUsed, true);
|
||||
const nonGithub = resolveGitBundleFetchSource("ssh://git@example.test/repo.git", { baseUrl: "http://mirror.example.test" }, {});
|
||||
const nonGithub = resolveGitBundleFetchSource("ssh://git@example.test/repo.git", { AGENTRUN_RESOURCE_BUNDLE_REPOSITORIES_JSON: "invalid-json-is-ignored-for-non-github" });
|
||||
assert.equal(nonGithub.fetchRepoUrl, "ssh://git@example.test/repo.git");
|
||||
assert.equal(nonGithub.mirrorUsed, false);
|
||||
assert.equal(classifyGitStderr("fatal: repository 'http://mirror.example.test/pikasTech/agent_skills.git/' not found\n"), "repository-not-found");
|
||||
@@ -415,7 +416,7 @@ const selfTest: SelfTestCase = async (context) => {
|
||||
assert.equal((storedTask.payload as JsonRecord).model, "gpt-5.6-sol");
|
||||
assert.equal((storedTask.payload as JsonRecord).reasoningEffort, "medium");
|
||||
assert.deepEqual((storedTask.payload as JsonRecord).modelConfig, taskPayload.modelConfig);
|
||||
return { name: "aipod-spec", tests: ["aipod-spec-yaml-parser-runtime-compatible", "aipod-spec-artificer-image-ref-render", "aipod-spec-artificer-release-runtime-authority", "aipod-spec-multi-upstream-secret-summary", "aipod-spec-legacy-single-upstream-compatible", "aipod-spec-model-defaults", "aipod-spec-model-reasoning-overrides", "aipod-spec-model-default-fallback", "aipod-spec-grok-exact-route", "aipod-spec-selected-provider-only", "aipod-spec-invalid-route-fail-closed", "aipod-spec-control-override-fail-closed", "aipod-spec-payload-control-bypass-fail-closed", "aipod-spec-durable-task-model-snapshot", "aipod-spec-primary-workspace-contract", "aipod-spec-artificer-github-url-render", "aipod-spec-artificer-github-ssh-required-keys", "aipod-spec-artificer-private-repository-override", "aipod-spec-artificer-private-repository-override-runner-projection", "aipod-spec-git-mirror-url", "git-fetch-stderr-classification", "resource-bundle-source-authority-validation", "resource-bundle-target-conflict-validation", "queue-submit-aipod-dry-run", "session-send-aipod-model-dry-run", "session-send-aipod-grok-dry-run", "session-send-aipod-cancelled-durable-identity", "session-send-aipod-boundary-conflict-fail-closed", "aipod-cli-help"] };
|
||||
return { name: "aipod-spec", tests: ["aipod-spec-yaml-parser-runtime-compatible", "aipod-spec-artificer-image-ref-render", "aipod-spec-artificer-release-runtime-authority", "aipod-spec-multi-upstream-secret-summary", "aipod-spec-legacy-single-upstream-compatible", "aipod-spec-model-defaults", "aipod-spec-model-reasoning-overrides", "aipod-spec-model-default-fallback", "aipod-spec-grok-exact-route", "aipod-spec-selected-provider-only", "aipod-spec-invalid-route-fail-closed", "aipod-spec-control-override-fail-closed", "aipod-spec-payload-control-bypass-fail-closed", "aipod-spec-durable-task-model-snapshot", "aipod-spec-primary-workspace-contract", "aipod-spec-artificer-github-url-render", "aipod-spec-artificer-github-ssh-required-keys", "aipod-spec-artificer-private-repository-override", "aipod-spec-artificer-private-repository-override-runner-projection", "aipod-spec-resource-bundle-repository-map", "git-fetch-stderr-classification", "resource-bundle-source-authority-validation", "resource-bundle-target-conflict-validation", "queue-submit-aipod-dry-run", "session-send-aipod-model-dry-run", "session-send-aipod-grok-dry-run", "session-send-aipod-cancelled-durable-identity", "session-send-aipod-boundary-conflict-fail-closed", "aipod-cli-help"] };
|
||||
} finally {
|
||||
await new Promise<void>((resolve) => server.server.close(() => resolve()));
|
||||
}
|
||||
|
||||
@@ -91,7 +91,10 @@ async function runInternalHttpMirrorMaterializationCase(context: Parameters<type
|
||||
};
|
||||
const workspaceRef: WorkspaceRef = { kind: "opaque", path: "." };
|
||||
const materialized = await withGenericRunnerGitEnvironment(async () => await materializeResourceBundle(resourceBundleRef, workspaceRef, {
|
||||
AGENTRUN_GIT_MIRROR_BASE_URL: mirrorBaseUrl,
|
||||
AGENTRUN_RESOURCE_BUNDLE_REPOSITORIES_JSON: JSON.stringify([{
|
||||
repository: "pikasTech/agentrun-mirror-probe",
|
||||
readUrl: `${mirrorBaseUrl}/pikasTech/agentrun-mirror-probe.git`,
|
||||
}]),
|
||||
AGENTRUN_WORKSPACE_ROOT: path.join(context.tmp, "github-ssh-http-mirror-assembly"),
|
||||
AGENTRUN_WORKSPACE_PATH: workspacePath,
|
||||
AGENTRUN_RUN_ID: "run_github_ssh_http_mirror_selftest",
|
||||
@@ -99,8 +102,9 @@ async function runInternalHttpMirrorMaterializationCase(context: Parameters<type
|
||||
assert.ok(materialized);
|
||||
assert.equal(materialized.event.mirrorUsed, true);
|
||||
assert.equal(String(materialized.event.fetchRepoUrl).startsWith(`${mirrorBaseUrl}/`), true);
|
||||
assert.deepEqual(materialized.event.repositoryAuthority, { repository: "pikastech/agentrun-mirror-probe", mode: "exact-read-url", valuesPrinted: false });
|
||||
assert.equal(((materialized.event.gitTransport as JsonRecord).scope), "resource-bundle");
|
||||
assert.equal(await readFile(path.join(workspacePath, "mirror-probe.txt"), "utf8"), "internal http mirror remains available\n");
|
||||
assert.equal(await readFile(path.join(workspacePath, "probe.txt"), "utf8"), "internal http mirror remains available\n");
|
||||
} finally {
|
||||
await new Promise<void>((resolve) => mirrorServer.close(() => resolve()));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { AgentRunError } from "../../common/errors.js";
|
||||
import { resolveGitBundleFetchSource } from "../../runner/resource-bundle.js";
|
||||
import type { SelfTestCase } from "../harness.js";
|
||||
|
||||
const repositoriesEnvName = "AGENTRUN_RESOURCE_BUNDLE_REPOSITORIES_JSON";
|
||||
const exactReadUrl = "http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/pikasTech-unidesk.git";
|
||||
|
||||
const selfTest: SelfTestCase = async () => {
|
||||
const exactMapping = JSON.stringify([
|
||||
{ repository: "pikasTech/unidesk", readUrl: exactReadUrl },
|
||||
{ repository: "pikasTech/agentrun", readUrl: "http://gitea.example.test/mirrors/pikasTech-agentrun.git" },
|
||||
]);
|
||||
const exact = resolveGitBundleFetchSource("git@github.com:pikasTech/unidesk.git", { [repositoriesEnvName]: exactMapping });
|
||||
assert.deepEqual(exact, { fetchRepoUrl: exactReadUrl, mirrorUsed: true });
|
||||
|
||||
const normalized = resolveGitBundleFetchSource("https://github.com/PIKASTECH/UNIDESK.git", { [repositoriesEnvName]: exactMapping });
|
||||
assert.deepEqual(normalized, { fetchRepoUrl: exactReadUrl, mirrorUsed: true });
|
||||
|
||||
assertMappingFailure(
|
||||
() => resolveGitBundleFetchSource("git@github.com:pikasTech/missing.git", { [repositoriesEnvName]: exactMapping }),
|
||||
"resource-bundle-repository-zero-match",
|
||||
);
|
||||
|
||||
const duplicateMapping = JSON.stringify([
|
||||
{ repository: "pikasTech/unidesk", readUrl: exactReadUrl },
|
||||
{ repository: "PIKASTECH/UNIDESK", readUrl: "http://gitea.example.test/mirrors/duplicate.git" },
|
||||
]);
|
||||
assertMappingFailure(
|
||||
() => resolveGitBundleFetchSource("git@github.com:pikasTech/unidesk.git", { [repositoriesEnvName]: duplicateMapping }),
|
||||
"resource-bundle-repository-multiple-matches",
|
||||
);
|
||||
|
||||
assertMappingFailure(
|
||||
() => resolveGitBundleFetchSource("git@github.com:pikasTech/unidesk.git", { [repositoriesEnvName]: "{" }),
|
||||
"resource-bundle-repositories-invalid-json",
|
||||
);
|
||||
assertMappingFailure(
|
||||
() => resolveGitBundleFetchSource("git@github.com:pikasTech/unidesk.git", { [repositoriesEnvName]: JSON.stringify([{ repository: "pikasTech/unidesk", readUrl: "https://user:password@gitea.example.test/repo.git" }]) }),
|
||||
"resource-bundle-repository-invalid-entry",
|
||||
);
|
||||
|
||||
const nonGithubUrl = "ssh://git@gitea.example.test/mirrors/pikasTech-unidesk.git";
|
||||
assert.deepEqual(
|
||||
resolveGitBundleFetchSource(nonGithubUrl, { [repositoriesEnvName]: "invalid-json-is-not-consumed" }),
|
||||
{ fetchRepoUrl: nonGithubUrl, mirrorUsed: false },
|
||||
);
|
||||
|
||||
return {
|
||||
name: "resource-bundle-repository-map",
|
||||
tests: [
|
||||
"resource-bundle-repository-exact-read-url",
|
||||
"resource-bundle-repository-normalized-github-identity",
|
||||
"resource-bundle-repository-zero-match-typed-failure",
|
||||
"resource-bundle-repository-multiple-match-typed-failure",
|
||||
"resource-bundle-repositories-invalid-json-typed-failure",
|
||||
"resource-bundle-repository-invalid-entry-typed-failure",
|
||||
"resource-bundle-non-github-url-preserved",
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
function assertMappingFailure(operation: () => unknown, reason: string): void {
|
||||
assert.throws(operation, (error: unknown) => {
|
||||
assert.ok(error instanceof AgentRunError);
|
||||
assert.equal(error.failureKind, "schema-invalid");
|
||||
assert.equal(error.details?.reason, reason);
|
||||
assert.equal(error.details?.environment, repositoriesEnvName);
|
||||
assert.equal(error.details?.valuesPrinted, false);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
export default selfTest;
|
||||
Reference in New Issue
Block a user