305 lines
13 KiB
JavaScript
305 lines
13 KiB
JavaScript
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 = "G14";
|
|
export const DEFAULT_HWLAB_AGENTRUN_BACKEND_PROFILE = "deepseek";
|
|
export const DEFAULT_UNIDESK_MAIN_SERVER_ENV = "UNIDESK_MAIN_SERVER_IP";
|
|
export const DEFAULT_HWLAB_AGENTRUN_TOOL_ALIASES = Object.freeze([
|
|
{ name: "unidesk-ssh", path: "tools/unidesk-ssh.mjs", kind: "bun-script" }
|
|
]);
|
|
|
|
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 backendProfiles = Object.freeze(["codex", "deepseek", "minimax-m3"]);
|
|
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);
|
|
const backendProfile = normalizeBackendProfile(options.backendProfile ?? 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 = nonEmptyString(options.projectId ?? DEFAULT_HWLAB_AGENTRUN_PROJECT_ID, "projectId");
|
|
const providerId = nonEmptyString(options.providerId ?? DEFAULT_HWLAB_AGENTRUN_PROVIDER_ID, "providerId");
|
|
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 toolAliases = normalizeResourceToolAliases(options.toolAliases ?? DEFAULT_HWLAB_AGENTRUN_TOOL_ALIASES);
|
|
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: "git",
|
|
repoUrl,
|
|
commitId,
|
|
...(options.subdir ? { subdir: nonEmptyString(options.subdir, "subdir") } : {}),
|
|
...(Array.isArray(options.sparsePaths) ? { sparsePaths: options.sparsePaths.map((item, index) => nonEmptyString(item, `sparsePaths[${index}]`)) } : {}),
|
|
...(toolAliases.length > 0 ? { toolAliases } : {}),
|
|
...(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,
|
|
...(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,
|
|
toolAliases: toolAliases.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 normalizeResourceToolAliases(items) {
|
|
if (!Array.isArray(items)) throw new Error("toolAliases must be an array");
|
|
const allowedKinds = new Set(["node-script", "bun-script", "sh-script", "executable"]);
|
|
const seen = new Set();
|
|
return items.map((item, index) => {
|
|
if (!item || typeof item !== "object") throw new Error(`toolAliases[${index}] must be an object`);
|
|
const name = nonEmptyString(item.name, `toolAliases[${index}].name`);
|
|
const aliasPath = nonEmptyString(item.path, `toolAliases[${index}].path`);
|
|
const kind = nonEmptyString(item.kind, `toolAliases[${index}].kind`);
|
|
if (!/^[a-z][a-z0-9._-]{0,62}$/u.test(name)) throw new Error(`toolAliases[${index}].name must be a lowercase command name`);
|
|
if (seen.has(name)) throw new Error(`toolAliases name ${name} is duplicated`);
|
|
if (aliasPath.startsWith("/") || aliasPath.includes("..")) throw new Error(`toolAliases[${index}].path must stay within the checkout`);
|
|
if (!allowedKinds.has(kind)) throw new Error(`toolAliases[${index}].kind is not supported`);
|
|
seen.add(name);
|
|
return { name, path: aliasPath, kind };
|
|
});
|
|
}
|
|
|
|
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 body;
|
|
}
|
|
|
|
function normalizeBackendProfile(value) {
|
|
const profile = nonEmptyString(value, "backendProfile");
|
|
if (!backendProfiles.includes(profile)) throw new Error(`unsupported AgentRun backendProfile ${JSON.stringify(profile)}`);
|
|
return profile;
|
|
}
|
|
|
|
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, "");
|
|
}
|