export const DEFAULT_AGENTRUN_NAMESPACE = "agentrun-v01"; export const DEFAULT_AGENTRUN_MANAGER_URL = "http://agentrun-mgr.agentrun-v01.svc.cluster.local:8080"; export const DEFAULT_HWLAB_AGENTRUN_PROJECT_ID = "pikasTech/HWLAB"; export const DEFAULT_HWLAB_AGENTRUN_PROVIDER_ID = "G14"; export const DEFAULT_HWLAB_AGENTRUN_REPO_URL = "http://git-mirror-http.devops-infra.svc.cluster.local/pikasTech/HWLAB.git"; export const DEFAULT_HWLAB_AGENTRUN_BRANCH = "v0.2"; export const DEFAULT_HWLAB_AGENTRUN_BACKEND_PROFILE = "deepseek"; export const DEFAULT_UNIDESK_MAIN_SERVER_ENV = "UNIDESK_MAIN_SERVER_IP"; export const DEFAULT_HWLAB_AGENTRUN_GIT_BUNDLES = Object.freeze([ { name: "hwlab-tools", subpath: "tools", target_path: "tools" }, { name: "hwlab-agent-skills", subpath: "skills", target_path: ".agents/skills" } ]); export const DEFAULT_HWLAB_AGENTRUN_PROMPT_REFS = Object.freeze([ { name: "hwlab-v02-runtime", path: "internal/agent/prompts/hwlab-v02-runtime.md", inject: "thread-start", required: true } ]); export function createHwlabAgentRunAssemblySummary(options = {}) { const env = options.env ?? process.env; const bundles = normalizeResourceGitBundles(options.bundles ?? DEFAULT_HWLAB_AGENTRUN_GIT_BUNDLES); const promptRefs = normalizeResourcePromptRefs(options.promptRefs ?? DEFAULT_HWLAB_AGENTRUN_PROMPT_REFS); const promptAssembly = createPromptAssemblySummary(promptRefs); const repoUrl = optionalString(options.repoUrl ?? env.HWLAB_CODE_AGENT_AGENTRUN_REPO_URL ?? env.AGENTRUN_REPO_URL) ?? DEFAULT_HWLAB_AGENTRUN_REPO_URL; const branch = optionalString(options.branch ?? env.HWLAB_BOOT_REF ?? env.HWLAB_CODE_AGENT_AGENTRUN_BRANCH ?? env.AGENTRUN_BRANCH) ?? DEFAULT_HWLAB_AGENTRUN_BRANCH; const commitId = optionalString(options.commitId ?? env.HWLAB_COMMIT_ID ?? env.HWLAB_GIT_SHA ?? env.HWLAB_REVISION); const namespace = optionalString(options.namespace ?? env.AGENTRUN_RUNTIME_NAMESPACE ?? env.HWLAB_CODE_AGENT_AGENTRUN_NAMESPACE) ?? DEFAULT_AGENTRUN_NAMESPACE; const managerUrl = optionalString(options.managerUrl ?? env.HWLAB_CODE_AGENT_AGENTRUN_MGR_URL ?? env.AGENTRUN_MANAGER_URL) ?? DEFAULT_AGENTRUN_MANAGER_URL; return { status: bundles.length > 0 ? "ready" : "degraded", provider: "agentrun-v01", namespace, managerUrl, resourceBundle: { kind: "gitbundle", repoUrl, branch, commitId, skillsDir: ".agents/skills" }, counts: { bundles: bundles.length, promptRefs: promptRefs.length }, bundles: bundles.map((item) => ({ ...item })), promptRefs: promptRefs.map((item) => ({ ...item })), promptAssembly, runtimeEnv: { assembledRuntime: "HWLAB_CODE_AGENT_ASSEMBLED_RUNTIME", skillsDirs: "HWLAB_CODE_AGENT_SKILLS_DIRS" }, valuesPrinted: false }; } function createPromptAssemblySummary(promptRefs) { const refs = promptRefs.map((item) => ({ name: item.name, path: item.path, inject: item.inject, stage: promptInjectStageLabel(item.inject), required: item.required !== false, source: "ResourceBundleRef.promptRefs" })); return { status: refs.length > 0 ? "ready" : "missing", count: refs.length, requiredCount: refs.filter((item) => item.required).length, injectStages: [...new Set(refs.map((item) => item.inject))], refs, valuesPrinted: false }; } function promptInjectStageLabel(inject) { if (inject === "thread-start") return "thread-start initial prompt"; return inject; } export const AGENTRUN_REUSABLE_CREDENTIAL_ENV_NAMES = Object.freeze([ "AUTH_PASSWORD", "CODEX_API_KEY", "GH_TOKEN", "GITHUB_TOKEN", "OPENAI_API_KEY", "PROVIDER_TOKEN", "UNIDESK_AUTH_PASSWORD", "UNIDESK_PROVIDER_TOKEN", "UNIDESK_SSH_CLIENT_TOKEN" ]); const backendProfileAliases = Object.freeze({ "codex-api": "codex", codex: "codex" }); const backendProfileIdPattern = /^[a-z0-9][a-z0-9-]{0,63}$/u; const providerSecretKeys = Object.freeze(["auth.json", "config.toml"]); const reusableCredentialEnvNameSet = new Set(AGENTRUN_REUSABLE_CREDENTIAL_ENV_NAMES); export function createGithubToolCredential({ namespace = DEFAULT_AGENTRUN_NAMESPACE, secretName = "agentrun-v01-tool-github-pr", secretKey = "GH_TOKEN" } = {}) { return { tool: "github", purpose: "pull-request", secretRef: { namespace, name: secretName, keys: [secretKey] }, projection: { kind: "env", envName: secretKey, secretKey } }; } export function createUnideskSshToolCredential({ namespace = DEFAULT_AGENTRUN_NAMESPACE, secretName = "agentrun-v01-tool-unidesk-ssh" } = {}) { return { tool: "unidesk-ssh", purpose: "ssh-passthrough", secretRef: { namespace, name: secretName, keys: ["UNIDESK_SSH_CLIENT_TOKEN"] }, projection: { kind: "env", envName: "UNIDESK_SSH_CLIENT_TOKEN", secretKey: "UNIDESK_SSH_CLIENT_TOKEN" } }; } export function createProviderCredential({ backendProfile = DEFAULT_HWLAB_AGENTRUN_BACKEND_PROFILE, namespace = DEFAULT_AGENTRUN_NAMESPACE, secretName = null, mountPath = "~/.codex" } = {}) { const profile = normalizeBackendProfile(backendProfile); return { profile, secretRef: { namespace, name: secretName || `agentrun-v01-provider-${profile}`, keys: providerSecretKeys.slice(), mountPath } }; } export function createHwlabAgentRunDispatchAssembly(options = {}) { const env = options.env ?? process.env; const traceId = nonEmptyString(options.traceId, "traceId"); const prompt = nonEmptyString(options.prompt, "prompt"); const commitId = fullCommitSha(options.commitId); // Concept boundary (#792): HWLAB adapter fixes AgentRun policy fields regardless of caller. // Business-side projectId belongs in metadata/workspaceRef, NOT AgentRun projectId. const backendProfile = normalizeBackendProfile(DEFAULT_HWLAB_AGENTRUN_BACKEND_PROFILE); const includeGithubToolCredential = options.includeGithubToolCredential !== false; const includeUnideskSshToolCredential = options.includeUnideskSshToolCredential !== false; const namespace = nonEmptyString(options.namespace ?? DEFAULT_AGENTRUN_NAMESPACE, "namespace"); const managerUrl = trimTrailingSlash(nonEmptyString(options.managerUrl ?? DEFAULT_AGENTRUN_MANAGER_URL, "managerUrl")); const projectId = DEFAULT_HWLAB_AGENTRUN_PROJECT_ID; const providerId = DEFAULT_HWLAB_AGENTRUN_PROVIDER_ID; const repoUrl = nonEmptyString(options.repoUrl ?? DEFAULT_HWLAB_AGENTRUN_REPO_URL, "repoUrl"); const branch = nonEmptyString(options.branch ?? DEFAULT_HWLAB_AGENTRUN_BRANCH, "branch"); const timeoutMs = positiveInteger(options.timeoutMs ?? 600000, "timeoutMs"); const unideskMainServerIp = optionalString(options.unideskMainServerIp ?? env.UNIDESK_MAIN_SERVER_IP ?? env.UNIDESK_MAIN_SERVER_HOST); const bundles = normalizeResourceGitBundles(options.bundles ?? DEFAULT_HWLAB_AGENTRUN_GIT_BUNDLES); const promptRefs = normalizeResourcePromptRefs(options.promptRefs ?? DEFAULT_HWLAB_AGENTRUN_PROMPT_REFS); if (includeUnideskSshToolCredential && !unideskMainServerIp) { throw new Error("unideskMainServerIp is required when UniDesk SSH passthrough is enabled"); } const toolCredentials = [ ...(includeGithubToolCredential ? [createGithubToolCredential({ namespace })] : []), ...(includeUnideskSshToolCredential ? [createUnideskSshToolCredential({ namespace })] : []), ...normalizeToolCredentials(options.toolCredentials ?? []) ]; const transientEnv = normalizeTransientEnv(transientEnvWithUnideskMainServer(options.transientEnv ?? [], unideskMainServerIp)); const sessionRef = createSessionRef(options); const resourceBundleRef = { kind: "gitbundle", repoUrl, commitId, bundles, ...(promptRefs.length > 0 ? { promptRefs } : {}), ...(options.resourceCredentialRef ? { credentialRef: options.resourceCredentialRef } : {}) }; const runPayload = { tenantId: "hwlab", projectId, workspaceRef: { kind: "git-worktree", repo: repoUrl, branch }, ...(sessionRef ? { sessionRef } : { sessionRef: null }), resourceBundleRef, providerId, backendProfile, executionPolicy: { sandbox: options.sandbox ?? "workspace-write", approval: options.approval ?? "never", timeoutMs, network: options.network ?? "default", secretScope: { allowCredentialEcho: false, providerCredentials: [createProviderCredential({ backendProfile, namespace })], ...(toolCredentials.length > 0 ? { toolCredentials } : {}) } }, traceSink: options.traceSink ?? null }; const commandPayload = { type: "turn", payload: { prompt, traceId, projectId: DEFAULT_HWLAB_AGENTRUN_PROJECT_ID, ...(options.conversationId ? { conversationId: nonEmptyString(options.conversationId, "conversationId") } : {}), ...(options.sessionId ? { sessionId: nonEmptyString(options.sessionId, "sessionId") } : {}), ...(options.threadId ? { threadId: nonEmptyString(options.threadId, "threadId") } : {}), providerProfile: backendProfile, dispatchSource: "hwlab-code-agent" }, idempotencyKey: options.commandIdempotencyKey ?? `hwlab:${traceId}:turn` }; const runnerJobPayload = { idempotencyKey: options.runnerJobIdempotencyKey ?? `hwlab:${traceId}:runner-job`, ...(options.attemptId ? { attemptId: nonEmptyString(options.attemptId, "attemptId") } : {}), transientEnv }; return { managerUrl, runPayload, commandPayload, runnerJobPayload, boundaries: { valuesPrinted: false, githubCredentialSource: includeGithubToolCredential ? "toolCredentials" : null, unideskSshCredentialSource: includeUnideskSshToolCredential ? "toolCredentials" : null, unideskMainServerSource: unideskMainServerIp ? "transientEnv" : null, bundles: bundles.map((item) => item.name), promptRefs: promptRefs.map((item) => item.name), transientEnvNames: transientEnv.map((item) => item.name), forbiddenTransientEnvNames: AGENTRUN_REUSABLE_CREDENTIAL_ENV_NAMES.slice() } }; } export async function dispatchHwlabAgentRun(options = {}) { const fetchImpl = options.fetchImpl ?? globalThis.fetch; if (typeof fetchImpl !== "function") throw new Error("dispatchHwlabAgentRun requires fetchImpl or global fetch"); const assembly = options.assembly ?? createHwlabAgentRunDispatchAssembly(options); const managerUrl = trimTrailingSlash(options.managerUrl ?? assembly.managerUrl); const run = await postJson(fetchImpl, managerUrl, "/api/v1/runs", assembly.runPayload); const runId = nonEmptyString(run.id, "run.id"); const command = await postJson(fetchImpl, managerUrl, `/api/v1/runs/${encodeURIComponent(runId)}/commands`, assembly.commandPayload); const commandId = nonEmptyString(command.id, "command.id"); const runnerJobPayload = { ...assembly.runnerJobPayload, commandId }; const runnerJob = await postJson(fetchImpl, managerUrl, `/api/v1/runs/${encodeURIComponent(runId)}/runner-jobs`, runnerJobPayload); return { managerUrl, run, command, runnerJob, requests: { runPayload: assembly.runPayload, commandPayload: assembly.commandPayload, runnerJobPayload }, boundaries: assembly.boundaries }; } function createSessionRef(options) { const sessionId = optionalString(options.sessionId); const conversationId = optionalString(options.conversationId); const threadId = optionalString(options.threadId); if (!sessionId && !conversationId && !threadId) return null; return { sessionId: sessionId || conversationId || threadId, ...(conversationId ? { conversationId } : {}), ...(threadId ? { threadId } : {}), metadata: { source: "hwlab-code-agent", traceId: optionalString(options.traceId) ?? null } }; } function normalizeTransientEnv(items) { const seen = new Set(); return items.filter(Boolean).map((item, index) => { const name = nonEmptyString(item.name, `transientEnv[${index}].name`); if (!/^[A-Z_][A-Z0-9_]{0,63}$/u.test(name)) throw new Error(`transientEnv[${index}].name must be an uppercase env name`); if (reusableCredentialEnvNameSet.has(name)) throw new Error(`transientEnv ${name} must use AgentRun tool/provider credential assembly instead`); if (seen.has(name)) throw new Error(`transientEnv ${name} is duplicated`); seen.add(name); return { name, value: nonEmptyString(item.value, `transientEnv[${index}].value`), sensitive: item.sensitive !== false }; }); } function transientEnvWithUnideskMainServer(items, unideskMainServerIp) { if (!unideskMainServerIp) return items; if (items.some((item) => item?.name === DEFAULT_UNIDESK_MAIN_SERVER_ENV)) return items; return [...items, { name: DEFAULT_UNIDESK_MAIN_SERVER_ENV, value: unideskMainServerIp, sensitive: false }]; } function normalizeToolCredentials(items) { return items.map((item, index) => { if (!item || typeof item !== "object") throw new Error(`toolCredentials[${index}] must be an object`); return item; }); } function normalizeResourceGitBundles(items) { if (!Array.isArray(items)) throw new Error("bundles must be an array"); const seen = new Set(); return items.map((item, index) => { if (!item || typeof item !== "object") throw new Error(`bundles[${index}] must be an object`); const name = resourceName(item.name, `bundles[${index}].name`); if (seen.has(name)) throw new Error(`bundles name ${name} is duplicated`); seen.add(name); return { name, subpath: resourcePath(item.subpath, `bundles[${index}].subpath`), target_path: workspaceTargetPath(item.target_path ?? item.targetPath, `bundles[${index}].target_path`) }; }); } function normalizeResourcePromptRefs(items) { if (!Array.isArray(items)) throw new Error("promptRefs must be an array"); const seen = new Set(); return items.map((item, index) => { if (!item || typeof item !== "object") throw new Error(`promptRefs[${index}] must be an object`); const name = resourceName(item.name, `promptRefs[${index}].name`); if (seen.has(name)) throw new Error(`promptRefs name ${name} is duplicated`); seen.add(name); const refPath = resourcePath(item.path, `promptRefs[${index}].path`); const inject = optionalString(item.inject) ?? "thread-start"; if (inject !== "thread-start") throw new Error(`promptRefs[${index}].inject must be thread-start`); return { name, path: refPath, inject: "thread-start", required: item.required !== false }; }); } function resourceName(value, fieldName) { const name = nonEmptyString(value, fieldName); if (!/^[a-z][a-z0-9._-]{0,62}$/u.test(name)) throw new Error(`${fieldName} must be a lowercase resource name`); return name; } function resourcePath(value, fieldName) { const refPath = nonEmptyString(value, fieldName); if (refPath.startsWith("/") || refPath.includes("..")) throw new Error(`${fieldName} must stay within the checkout`); return refPath; } function workspaceTargetPath(value, fieldName) { const targetPath = nonEmptyString(value, fieldName); if (targetPath === "." || targetPath.startsWith("/") || targetPath.includes("..")) throw new Error(`${fieldName} must stay within the workspace`); return targetPath; } async function postJson(fetchImpl, managerUrl, path, payload) { const response = await fetchImpl(`${managerUrl}${path}`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(payload) }); const text = await response.text(); const body = text ? JSON.parse(text) : null; if (!response.ok) throw new Error(`AgentRun POST ${path} failed: status=${response.status} body=${JSON.stringify(body).slice(0, 500)}`); return unwrapAgentRunEnvelope(path, body); } function unwrapAgentRunEnvelope(path, body) { if (!body || typeof body !== "object" || !("ok" in body)) return body; if (body.ok === true) return body.data ?? null; throw new Error(`AgentRun POST ${path} failed: envelope=${JSON.stringify(body).slice(0, 500)}`); } function normalizeBackendProfile(value) { const profile = nonEmptyString(value, "backendProfile").toLowerCase(); if (profile === "runtime-default") throw new Error(`unsupported AgentRun backendProfile ${JSON.stringify(profile)}`); const normalized = backendProfileAliases[profile] || profile; if (!backendProfileIdPattern.test(normalized)) throw new Error(`unsupported AgentRun backendProfile ${JSON.stringify(profile)}`); return normalized; } function fullCommitSha(value) { const commitId = nonEmptyString(value, "commitId"); if (!/^[0-9a-f]{40}$/u.test(commitId)) throw new Error("commitId must be a full 40-character lowercase git commit sha"); return commitId; } function positiveInteger(value, fieldName) { const parsed = Number(value); if (!Number.isSafeInteger(parsed) || parsed <= 0) throw new Error(`${fieldName} must be a positive integer`); return parsed; } function nonEmptyString(value, fieldName) { const text = optionalString(value); if (!text) throw new Error(`${fieldName} is required`); return text; } function optionalString(value) { return typeof value === "string" && value.trim() ? value.trim() : null; } function trimTrailingSlash(value) { return nonEmptyString(value, "url").replace(/\/+$/u, ""); }