Merge pull request #115 from pikasTech/fix/issue112-unidesk-ssh-endpoint-assembly

fix: 装配 UniDesk SSH endpoint env
This commit is contained in:
Lyon
2026-06-09 21:34:15 +08:00
committed by GitHub
6 changed files with 147 additions and 7 deletions
+1
View File
@@ -3,6 +3,7 @@
"runtimeNamespace": "agentrun-v01",
"gitopsBranch": "v0.1-gitops",
"runtimePath": "deploy/gitops/g14/runtime-v01",
"unideskSshEndpointEnv": { "name": "UNIDESK_MAIN_SERVER_IP", "value": "74.48.78.17" },
"services": [
{
"id": "agentrun-mgr",
+24 -3
View File
@@ -39,7 +39,13 @@ interface CatalogService {
provenance?: JsonRecord;
}
interface EnvVarValue {
name: string;
value: string;
}
const defaultBootRepoUrl = "http://git-mirror-http.devops-infra.svc.cluster.local/pikasTech/agentrun.git";
const unideskSshEndpointEnvNames = new Set(["UNIDESK_MAIN_SERVER_IP", "UNIDESK_MAIN_SERVER_HOST", "UNIDESK_FRONTEND_URL"]);
export async function runGitopsRenderCli(argv: string[]): Promise<void> {
try {
@@ -57,6 +63,7 @@ export async function renderGitops(options: RenderOptions): Promise<JsonRecord>
const runtimeNamespace = stringField(deploy, "runtimeNamespace", "agentrun-v01");
const gitopsBranch = stringField(deploy, "gitopsBranch", "v0.1-gitops");
const runtimePath = stringField(deploy, "runtimePath", "deploy/gitops/g14/runtime-v01");
const unideskSshEndpointEnv = optionalUnideskSshEndpointEnv(deploy);
const catalog = await loadCatalog(options, gitopsBranch);
const image = imageForService(catalog, "agentrun-mgr", options);
@@ -70,9 +77,9 @@ export async function renderGitops(options: RenderOptions): Promise<JsonRecord>
await writeFile(path.join(options.outDir, "runtime-v01", "kustomization.yaml"), kustomizationYaml());
await writeFile(path.join(options.outDir, "runtime-v01", "namespace.yaml"), namespaceYaml(runtimeNamespace));
await writeFile(path.join(options.outDir, "runtime-v01", "postgres.yaml"), postgresYaml(runtimeNamespace));
await writeFile(path.join(options.outDir, "runtime-v01", "mgr.yaml"), managerYaml(runtimeNamespace, image, options.sourceCommit));
await writeFile(path.join(options.outDir, "runtime-v01", "mgr.yaml"), managerYaml(runtimeNamespace, image, options.sourceCommit, unideskSshEndpointEnv));
await writeFile(path.join(options.outDir, "runtime-v01", "runner-rbac.yaml"), runnerRbacYaml(runtimeNamespace));
return { outDir: options.outDir, runtimeNamespace, gitopsBranch, runtimePath, image: repositoryDigestForService(image), sourceCommit: options.sourceCommit, envIdentity: image.envIdentity ?? null, artifactStatus: image.status ?? null };
return { outDir: options.outDir, runtimeNamespace, gitopsBranch, runtimePath, image: repositoryDigestForService(image), sourceCommit: options.sourceCommit, envIdentity: image.envIdentity ?? null, artifactStatus: image.status ?? null, unideskSshEndpointEnv: unideskSshEndpointEnv ? { name: unideskSshEndpointEnv.name, valuesPrinted: false } : null };
}
async function loadCatalog(options: RenderOptions, gitopsBranch: string): Promise<ArtifactCatalog> {
@@ -239,9 +246,10 @@ spec:
`;
}
function managerYaml(namespace: string, image: CatalogService, sourceCommit: string): string {
function managerYaml(namespace: string, image: CatalogService, sourceCommit: string, unideskSshEndpointEnv: EnvVarValue | null): string {
const imageRef = repositoryDigestForService(image);
const envIdentity = image.envIdentity ?? image.imageTag ?? "unknown";
const unideskSshEndpointEnvYaml = unideskSshEndpointEnv ? ` - name: ${unideskSshEndpointEnv.name}\n value: ${JSON.stringify(unideskSshEndpointEnv.value)}\n` : "";
return `apiVersion: v1
kind: ServiceAccount
metadata:
@@ -314,6 +322,7 @@ spec:
value: ${JSON.stringify(imageRef)}
- name: AGENTRUN_RUNNER_SERVICE_ACCOUNT
value: "agentrun-v01-runner"
${unideskSshEndpointEnvYaml}
readinessProbe:
httpGet:
path: /health/readiness
@@ -456,3 +465,15 @@ function stringField(record: JsonRecord, key: string, fallback: string): string
const value = record[key];
return typeof value === "string" && value.length > 0 ? value : fallback;
}
function optionalUnideskSshEndpointEnv(deploy: JsonRecord): EnvVarValue | null {
const value = deploy.unideskSshEndpointEnv;
if (value === undefined) return null;
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new AgentRunError("schema-invalid", "unideskSshEndpointEnv must be an object", { httpStatus: 2 });
const record = value as JsonRecord;
const name = record.name;
const envValue = record.value;
if (typeof name !== "string" || !unideskSshEndpointEnvNames.has(name)) throw new AgentRunError("schema-invalid", "unideskSshEndpointEnv.name must be UNIDESK_MAIN_SERVER_IP, UNIDESK_MAIN_SERVER_HOST, or UNIDESK_FRONTEND_URL", { httpStatus: 2 });
if (typeof envValue !== "string" || envValue.length === 0) throw new AgentRunError("schema-invalid", "unideskSshEndpointEnv.value must be a non-empty string", { httpStatus: 2 });
return { name, value: envValue };
}
+53 -2
View File
@@ -3,7 +3,7 @@ import { AgentRunError } from "../common/errors.js";
import { redactJson, redactText } from "../common/redaction.js";
import { isTerminalCommandState, isTerminalRunStatus, summarizeResourceBundleRef, summarizeSessionRef } from "./store.js";
import type { AgentRunStore } from "./store.js";
import type { JsonRecord } from "../common/types.js";
import type { ExecutionPolicy, JsonRecord } from "../common/types.js";
import { stableHash, validateEnvName } from "../common/validation.js";
import { renderRunnerJobManifest } from "../runner/k8s-job.js";
import type { RunnerSessionPvcOptions, RunnerTransientEnv } from "../runner/k8s-job.js";
@@ -20,6 +20,17 @@ const reusableCredentialEnvNames = new Set([
"UNIDESK_SSH_CLIENT_TOKEN",
]);
const unideskSshEndpointEnvNames = ["UNIDESK_MAIN_SERVER_IP", "UNIDESK_MAIN_SERVER_HOST", "UNIDESK_FRONTEND_URL"] as const;
const unideskSshEndpointEnvNameSet = new Set<string>(unideskSshEndpointEnvNames);
const unideskSshEndpointConfigEnvNames: Array<{ configName: string; targetName: (typeof unideskSshEndpointEnvNames)[number] }> = [
{ configName: "AGENTRUN_UNIDESK_MAIN_SERVER_IP", targetName: "UNIDESK_MAIN_SERVER_IP" },
{ configName: "AGENTRUN_UNIDESK_MAIN_SERVER_HOST", targetName: "UNIDESK_MAIN_SERVER_HOST" },
{ configName: "AGENTRUN_UNIDESK_FRONTEND_URL", targetName: "UNIDESK_FRONTEND_URL" },
{ configName: "UNIDESK_MAIN_SERVER_IP", targetName: "UNIDESK_MAIN_SERVER_IP" },
{ configName: "UNIDESK_MAIN_SERVER_HOST", targetName: "UNIDESK_MAIN_SERVER_HOST" },
{ configName: "UNIDESK_FRONTEND_URL", targetName: "UNIDESK_FRONTEND_URL" },
];
export interface RunnerJobDefaults {
namespace: string;
managerUrl: string;
@@ -27,6 +38,7 @@ export interface RunnerJobDefaults {
sourceCommit: string;
serviceAccountName?: string;
kubectlCommand?: string;
unideskSshEndpointEnv?: JsonRecord;
}
export interface CreateRunnerJobInput extends JsonRecord {
@@ -56,7 +68,7 @@ export async function createKubernetesRunnerJob(options: { store: AgentRunStore;
const sourceCommit = optionalString(options.input.sourceCommit) ?? options.defaults.sourceCommit;
const serviceAccountName = optionalString(options.input.serviceAccountName) ?? options.defaults.serviceAccountName;
const idempotencyKey = optionalString(options.input.idempotencyKey);
const transientEnv = transientEnvField(options.input.transientEnv);
const transientEnv = assembleToolContextTransientEnv(run.executionPolicy, transientEnvField(options.input.transientEnv), options.defaults);
const normalizedPayload = {
commandId,
image,
@@ -198,6 +210,45 @@ function transientEnvField(value: unknown): RunnerTransientEnv[] {
});
}
function assembleToolContextTransientEnv(policy: ExecutionPolicy, provided: RunnerTransientEnv[], defaults: RunnerJobDefaults): RunnerTransientEnv[] {
if (!usesUnideskSsh(policy)) return provided;
if (provided.some((item) => unideskSshEndpointEnvNameSet.has(item.name))) return provided;
const endpoint = defaultUnideskSshEndpointEnv(defaults);
if (!endpoint) {
throw new AgentRunError("schema-invalid", "unidesk-ssh tool credential requires runner-job transientEnv UNIDESK_MAIN_SERVER_IP, UNIDESK_MAIN_SERVER_HOST, or UNIDESK_FRONTEND_URL", {
httpStatus: 400,
details: { tool: "unidesk-ssh", requiredEnv: [...unideskSshEndpointEnvNames], valuesPrinted: false },
});
}
return [...provided, endpoint];
}
function usesUnideskSsh(policy: ExecutionPolicy): boolean {
return (policy.secretScope.toolCredentials ?? []).some((item) => item.tool === "unidesk-ssh");
}
function defaultUnideskSshEndpointEnv(defaults: RunnerJobDefaults): RunnerTransientEnv | null {
const fromDefaults = defaults.unideskSshEndpointEnv;
if (fromDefaults !== undefined) return unideskSshEndpointEnvFromRecord(fromDefaults, "runnerJobDefaults.unideskSshEndpointEnv");
for (const item of unideskSshEndpointConfigEnvNames) {
const value = optionalString(process.env[item.configName]);
if (value) return { name: item.targetName, value, sensitive: true };
}
return null;
}
function unideskSshEndpointEnvFromRecord(record: JsonRecord, fieldName: string): RunnerTransientEnv {
const name = stringField(record, "name");
validateEnvName(name, `${fieldName}.name`);
if (!unideskSshEndpointEnvNameSet.has(name)) {
throw new AgentRunError("schema-invalid", `${fieldName}.name must be one of ${unideskSshEndpointEnvNames.join(", ")}`, { httpStatus: 400, details: { requiredEnv: [...unideskSshEndpointEnvNames], valuesPrinted: false } });
}
const rawValue = record.value;
if (typeof rawValue !== "string" || rawValue.length === 0) throw new AgentRunError("schema-invalid", `${fieldName}.value must be a non-empty string`, { httpStatus: 400 });
if (Buffer.byteLength(rawValue, "utf8") > 8192) throw new AgentRunError("schema-invalid", `${fieldName}.value is too large`, { httpStatus: 400 });
return { name, value: rawValue, sensitive: true };
}
function summarizeToolCredentials(items: Array<{ tool: string; purpose: string | null; secretRef: { namespace?: string; name: string; keys?: string[] }; envName: string; secretKey: string }>, namespace: string): JsonRecord {
return {
count: items.length,
+3
View File
@@ -42,6 +42,7 @@ export interface ManagerServerOptions {
image?: string;
serviceAccountName?: string;
kubectlCommand?: string;
unideskSshEndpointEnv?: JsonRecord;
};
sessionPvcOptions?: { kubectlHandler?: import("./session-pvc.js").KubectlHandler; kubectlCommand?: string; storageClassName?: string; size?: string };
providerProfileOptions?: { namespace?: string; kubectlCommand?: string };
@@ -244,6 +245,7 @@ async function route({ method, url, body, store, sourceCommit, runnerJobDefaults
sourceCommit,
serviceAccountName: runnerJobDefaults?.serviceAccountName ?? process.env.AGENTRUN_RUNNER_SERVICE_ACCOUNT ?? "agentrun-v01-runner",
...(runnerJobDefaults?.kubectlCommand ? { kubectlCommand: runnerJobDefaults.kubectlCommand } : {}),
...(runnerJobDefaults?.unideskSshEndpointEnv ? { unideskSshEndpointEnv: runnerJobDefaults.unideskSshEndpointEnv } : {}),
},
}) as unknown as JsonValue;
}
@@ -297,6 +299,7 @@ async function route({ method, url, body, store, sourceCommit, runnerJobDefaults
sourceCommit,
serviceAccountName: runnerJobDefaults?.serviceAccountName ?? process.env.AGENTRUN_RUNNER_SERVICE_ACCOUNT ?? "agentrun-v01-runner",
...(runnerJobDefaults?.kubectlCommand ? { kubectlCommand: runnerJobDefaults.kubectlCommand } : {}),
...(runnerJobDefaults?.unideskSshEndpointEnv ? { unideskSshEndpointEnv: runnerJobDefaults.unideskSshEndpointEnv } : {}),
},
}) as unknown as JsonValue;
}
+12 -1
View File
@@ -143,6 +143,7 @@ console.log(JSON.stringify({ apiVersion: manifest.apiVersion, kind: manifest.kin
managerUrl: "http://agentrun-mgr.agentrun-v01.svc.cluster.local:8080",
image: "127.0.0.1:5000/agentrun/agentrun-mgr@sha256:1111111111111111111111111111111111111111111111111111111111111111",
kubectlCommand: fakeKubectl,
unideskSshEndpointEnv: { name: "UNIDESK_MAIN_SERVER_IP", value: "https://unidesk.default.example.test" },
},
});
try {
@@ -175,6 +176,16 @@ console.log(JSON.stringify({ apiVersion: manifest.apiVersion, kind: manifest.kin
assertRunnerJobUsesToolCredential({ manifest, toolCredentials: (created as JsonRecord).toolCredentials } as JsonRecord, "GH_TOKEN", "agentrun-v01-tool-github-pr", "GH_TOKEN");
assertRunnerJobUsesToolCredential({ manifest, toolCredentials: (created as JsonRecord).toolCredentials } as JsonRecord, "UNIDESK_SSH_CLIENT_TOKEN", "agentrun-v01-tool-unidesk-ssh", "UNIDESK_SSH_CLIENT_TOKEN");
assertNoSecretLeak(created);
const defaultEndpointJobItem = await createRunWithCommand(jobClient, { ...context, toolCredentials: unideskSshToolCredentials }, "job create unidesk ssh default endpoint", "selftest-job-create-unidesk-ssh-default-endpoint", 15_000);
const defaultEndpointCreated = await jobClient.post(`/api/v1/runs/${defaultEndpointJobItem.runId}/runner-jobs`, {
commandId: defaultEndpointJobItem.commandId,
attemptId: "attempt_selftest_unidesk_ssh_default_endpoint",
}) as JsonRecord;
assert.deepEqual(((defaultEndpointCreated.transientEnv as JsonRecord).names) as string[], ["UNIDESK_MAIN_SERVER_IP"]);
const defaultEndpointManifest = JSON.parse(await readFile(createdManifest, "utf8")) as JsonRecord;
assert.equal(runnerEnvValue(defaultEndpointManifest, "UNIDESK_MAIN_SERVER_IP"), "https://unidesk.default.example.test");
assertRunnerJobUsesToolCredential({ manifest: defaultEndpointManifest, toolCredentials: defaultEndpointCreated.toolCredentials } as JsonRecord, "UNIDESK_SSH_CLIENT_TOKEN", "agentrun-v01-tool-unidesk-ssh", "UNIDESK_SSH_CLIENT_TOKEN");
assertNoSecretLeak(defaultEndpointCreated);
await assert.rejects(
() => jobClient.post(`/api/v1/runs/${jobItem.runId}/runner-jobs`, {
commandId: jobItem.commandId,
@@ -228,7 +239,7 @@ console.log(JSON.stringify({ apiVersion: manifest.apiVersion, kind: manifest.kin
assert.equal(envMap.get("AGENTRUN_SESSION_PVC_NAMESPACE"), "agentrun-v01");
assert.equal(envMap.get("AGENTRUN_SESSION_PVC_MOUNT_PATH"), "/home/agentrun/.codex-codex/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-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-tool-credential-env", "runner-job-unidesk-ssh-tool-credential-env", "runner-job-unidesk-ssh-transient-env-denied", "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-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-tool-credential-env", "runner-job-unidesk-ssh-tool-credential-env", "runner-job-unidesk-ssh-endpoint-auto-env", "runner-job-unidesk-ssh-transient-env-denied", "runner-k8s-job-session-pvc-volume-and-env"] };
} finally {
await new Promise<void>((resolve) => server.server.close(() => resolve()));
}
+54 -1
View File
@@ -30,6 +30,7 @@ console.log(JSON.stringify({ apiVersion: manifest.apiVersion, kind: manifest.kin
managerUrl: "http://agentrun-mgr.agentrun-v01.svc.cluster.local:8080",
image: "127.0.0.1:5000/agentrun/agentrun-mgr@sha256:1111111111111111111111111111111111111111111111111111111111111111",
kubectlCommand: fakeKubectl,
unideskSshEndpointEnv: { name: "UNIDESK_MAIN_SERVER_IP", value: "https://unidesk.default.example.test" },
},
});
try {
@@ -96,6 +97,46 @@ console.log(JSON.stringify({ apiVersion: manifest.apiVersion, kind: manifest.kin
const dispatchManifest = JSON.parse(await readFile(createdManifest, "utf8")) as JsonRecord;
assert.ok(JSON.stringify(dispatchManifest).includes(dispatched.run.id));
const unideskCreated = await client.post("/api/v1/queue/tasks", {
tenantId: "unidesk",
projectId: "pikasTech/unidesk",
queue: "dev",
lane: "q2",
title: "Q2 queue unidesk ssh dispatch task",
priority: 22,
backendProfile: "codex",
providerId: "G14",
workspaceRef: { kind: "host-path", path: context.workspace },
sessionRef: { sessionId: "sess_queue_q2_unidesk_ssh_selftest", metadata: { source: "queue-q2-unidesk-ssh-self-test" } },
executionPolicy: {
sandbox: "workspace-write",
approval: "never",
timeoutMs: 15_000,
network: "default",
secretScope: {
allowCredentialEcho: false,
providerCredentials: [{ profile: "codex", secretRef: { name: "agentrun-v01-provider-codex", keys: ["auth.json", "config.toml"], mountPath: context.codexHome } }],
toolCredentials: [{
tool: "unidesk-ssh",
purpose: "ssh-passthrough-readonly",
secretRef: { name: "agentrun-v01-tool-unidesk-ssh", keys: ["UNIDESK_SSH_CLIENT_TOKEN"] },
projection: { kind: "env", envName: "UNIDESK_SSH_CLIENT_TOKEN", secretKey: "UNIDESK_SSH_CLIENT_TOKEN" },
}],
},
},
resourceBundleRef: null,
payload: { prompt: "queue unidesk ssh dispatch hello" },
references: [{ kind: "issue", url: "https://github.com/pikasTech/agentrun/issues/112" }],
metadata: { source: "queue-q2-unidesk-ssh-self-test" },
idempotencyKey: "queue-q2-unidesk-ssh-self-test",
}) as QueueTaskRecord;
const unideskDispatched = await client.post(`/api/v1/queue/tasks/${unideskCreated.id}/dispatch`, { attemptId: "attempt_queue_q2_unidesk_ssh_selftest" }) as QueueDispatchResult;
assert.deepEqual((((unideskDispatched.runnerJob as JsonRecord).transientEnv as JsonRecord).names) as string[], ["UNIDESK_MAIN_SERVER_IP"]);
const unideskManifest = JSON.parse(await readFile(createdManifest, "utf8")) as JsonRecord;
assert.equal(runnerEnvValue(unideskManifest, "UNIDESK_MAIN_SERVER_IP"), "https://unidesk.default.example.test");
assert.equal(runnerEnvValue(unideskManifest, "UNIDESK_SSH_CLIENT_TOKEN"), "secretRef");
assertNoSecretLeak(unideskDispatched);
const cancelCreated = await client.post("/api/v1/queue/tasks", {
tenantId: "unidesk",
projectId: "pikasTech/unidesk",
@@ -142,10 +183,22 @@ console.log(JSON.stringify({ apiVersion: manifest.apiVersion, kind: manifest.kin
assert.ok(JSON.stringify(cancelManifest).includes(cancelDispatched.run.id));
assertNoSecretLeak(dispatched);
assertNoSecretLeak(cancelled);
return { name: "queue-q2-dispatch", tests: ["queue-dispatch-run-command-runner-job", "queue-refresh-from-core-status", "queue-dispatch-no-repeat", "queue-cancel-propagates-to-run-command-session"] };
return { name: "queue-q2-dispatch", tests: ["queue-dispatch-run-command-runner-job", "queue-refresh-from-core-status", "queue-dispatch-no-repeat", "queue-unidesk-ssh-endpoint-auto-env", "queue-cancel-propagates-to-run-command-session"] };
} finally {
await new Promise<void>((resolve) => server.server.close(() => resolve()));
}
};
export default selfTest;
function runnerEnvValue(manifest: JsonRecord, name: string): unknown {
const spec = manifest.spec as JsonRecord;
const template = spec.template as JsonRecord;
const podSpec = template.spec as JsonRecord;
const containers = podSpec.containers as JsonRecord[];
const env = containers[0]?.env as JsonRecord[];
const item = env.find((entry) => entry.name === name);
if (!item) return undefined;
if (item.valueFrom) return "secretRef";
return item.value;
}