feat: add aipod spec Artificer assembly
This commit is contained in:
+194
-2
@@ -10,7 +10,7 @@ import { runOnce } from "../../src/runner/run-once.js";
|
||||
import { renderRunnerJobDryRun } from "../../src/runner/k8s-job.js";
|
||||
import type { RunnerSessionPvcOptions } from "../../src/runner/k8s-job.js";
|
||||
import { renderCodexProviderSecretPlan } from "./secret-render.js";
|
||||
import type { BackendProfile, CommandRecord, JsonRecord, JsonValue, RunRecord, SessionSummary } from "../../src/common/types.js";
|
||||
import type { BackendProfile, CommandRecord, JsonRecord, JsonValue, RenderAipodInput, RenderedAipodQueueTask, RunRecord, SessionSummary } from "../../src/common/types.js";
|
||||
import { AgentRunError, errorToJson } from "../../src/common/errors.js";
|
||||
import type { RunnerOnceOptions } from "../../src/runner/run-once.js";
|
||||
import { backendProfileSpec, isBackendProfile } from "../../src/common/backend-profiles.js";
|
||||
@@ -57,6 +57,11 @@ async function dispatch(args: ParsedArgs): Promise<CliResult> {
|
||||
if (group === "server" && command === "logs") return serverLogs(args);
|
||||
if (group === "server" && command === "stop") return stopServer(args);
|
||||
if (group === "backends" && command === "list") return client(args).get("/api/v1/backends");
|
||||
if ((group === "aipod-specs" || group === "aipods") && command === "list") return client(args).get("/api/v1/aipod-specs");
|
||||
if ((group === "aipod-specs" || group === "aipods") && command === "show" && id) return client(args).get(`/api/v1/aipod-specs/${encodeURIComponent(id)}`);
|
||||
if ((group === "aipod-specs" || group === "aipods") && command === "render" && id) return renderAipodSpecCli(args, id);
|
||||
if ((group === "aipod-specs" || group === "aipods") && (command === "apply" || command === "set")) return applyAipodSpecCli(args, id ?? null);
|
||||
if ((group === "aipod-specs" || group === "aipods") && (command === "delete" || command === "rm") && id) return client(args).delete(`/api/v1/aipod-specs/${encodeURIComponent(id)}`);
|
||||
if (group === "provider-profiles" && command === "list") return client(args).get("/api/v1/provider-profiles");
|
||||
if (group === "provider-profiles" && command === "show" && id) return client(args).get(`/api/v1/provider-profiles/${encodeURIComponent(normalizeProfile(id))}`);
|
||||
if (group === "provider-profiles" && command === "config" && id) return client(args).get(`/api/v1/provider-profiles/${encodeURIComponent(normalizeProfile(id))}/config`);
|
||||
@@ -64,6 +69,9 @@ async function dispatch(args: ParsedArgs): Promise<CliResult> {
|
||||
if (group === "provider-profiles" && command === "set-key" && id) return setProviderProfileKey(args, id);
|
||||
if (group === "provider-profiles" && command === "set-config" && id) return setProviderProfileConfig(args, id);
|
||||
if (group === "provider-profiles" && command === "validate" && id) return validateProviderProfileCli(args, id);
|
||||
if (group === "tool-credentials" && command === "list") return client(args).get("/api/v1/tool-credentials");
|
||||
if (group === "tool-credentials" && command === "show" && id) return client(args).get(`/api/v1/tool-credentials/${encodeURIComponent(id)}`);
|
||||
if (group === "tool-credentials" && command === "set-github-ssh") return setGithubSshToolCredentialCli(args);
|
||||
if (group === "secrets" && command === "codex" && id === "render") return renderCodexSecret(args);
|
||||
if (group === "sessions" && command === "ps") return listSessions(args);
|
||||
if (group === "sessions" && command === "create") return sessionCreate(args, id ?? null);
|
||||
@@ -689,6 +697,12 @@ function queueSubmitConfirmCommand(args: ParsedArgs): string {
|
||||
return parts.join(" ");
|
||||
}
|
||||
|
||||
function queueSubmitAipodConfirmCommand(args: ParsedArgs, aipod: string): string {
|
||||
const parts = [`./scripts/agentrun queue submit --aipod ${aipod} --prompt-stdin`];
|
||||
if (optionalFlag(args, "idempotency-key")) parts.push("--idempotency-key <idempotency-key>");
|
||||
return parts.join(" ");
|
||||
}
|
||||
|
||||
function queueDispatchConfirmCommand(args: ParsedArgs, taskId: string): string {
|
||||
const parts = [`./scripts/agentrun queue dispatch ${taskId}`];
|
||||
if (args.flags.get("json-stdin") === true || optionalFlag(args, "json-file")) parts.push("--json-stdin");
|
||||
@@ -1004,6 +1018,8 @@ async function sessionStorageDelete(args: ParsedArgs, sessionId: string): Promis
|
||||
}
|
||||
|
||||
async function sessionTurn(args: ParsedArgs, positionalSessionId: string | null): Promise<JsonRecord> {
|
||||
const aipod = optionalFlag(args, "aipod") ?? optionalFlag(args, "aipod-spec");
|
||||
if (aipod) return sessionTurnWithAipod(args, positionalSessionId, aipod);
|
||||
const body = await optionalJsonFile(args);
|
||||
const sessionId = positionalSessionId ?? optionalFlag(args, "session-id") ?? newSessionId();
|
||||
const requestedProfile = optionalFlag(args, "profile") ?? optionalFlag(args, "backend-profile") ?? (typeof body.backendProfile === "string" ? String(body.backendProfile) : "codex");
|
||||
@@ -1082,7 +1098,122 @@ async function sessionRead(args: ParsedArgs, sessionId: string): Promise<JsonVal
|
||||
return summarizeSessionMutationResult("session-read", sessionId, result, { read: true });
|
||||
}
|
||||
|
||||
async function renderAipodSpecCli(args: ParsedArgs, name: string): Promise<JsonValue> {
|
||||
const input = await aipodRenderInput(args, 3);
|
||||
return client(args).post(`/api/v1/aipod-specs/${encodeURIComponent(name)}/render`, input);
|
||||
}
|
||||
|
||||
async function renderAipodForCommand(args: ParsedArgs, name: string, trailingPromptStart: number, overrides: RenderAipodInput = {}): Promise<RenderedAipodQueueTask> {
|
||||
const input = await aipodRenderInput(args, trailingPromptStart, overrides);
|
||||
return await client(args).post(`/api/v1/aipod-specs/${encodeURIComponent(name)}/render`, input) as RenderedAipodQueueTask;
|
||||
}
|
||||
|
||||
async function aipodRenderInput(args: ParsedArgs, trailingPromptStart: number, overrides: RenderAipodInput = {}): Promise<RenderAipodInput> {
|
||||
const input = await optionalJsonFile(args) as RenderAipodInput;
|
||||
const prompt = await optionalPrompt(args, trailingPromptStart);
|
||||
if (prompt) input.prompt = prompt;
|
||||
copyOptionalFlag(args, input as JsonRecord, "tenant-id", "tenantId");
|
||||
copyOptionalFlag(args, input as JsonRecord, "project-id", "projectId");
|
||||
copyOptionalFlag(args, input as JsonRecord, "queue");
|
||||
copyOptionalFlag(args, input as JsonRecord, "lane");
|
||||
copyOptionalFlag(args, input as JsonRecord, "title");
|
||||
copyOptionalFlag(args, input as JsonRecord, "provider-id", "providerId");
|
||||
const profile = optionalFlag(args, "profile") ?? optionalFlag(args, "backend-profile");
|
||||
if (profile) input.backendProfile = normalizeProfile(profile);
|
||||
const priority = optionalFlag(args, "priority");
|
||||
if (priority) input.priority = Number(priority);
|
||||
copyOptionalFlag(args, input as JsonRecord, "idempotency-key", "idempotencyKey");
|
||||
copyOptionalFlag(args, input as JsonRecord, "session-id", "sessionId");
|
||||
const workspaceRef = jsonObjectFlag(args, "workspace-json");
|
||||
if (workspaceRef) input.workspaceRef = workspaceRef as never;
|
||||
return { ...input, ...overrides };
|
||||
}
|
||||
|
||||
async function applyAipodSpecCli(args: ParsedArgs, name: string | null): Promise<JsonValue> {
|
||||
const yaml = await aipodYamlInput(args);
|
||||
const pathValue = name ? `/api/v1/aipod-specs/${encodeURIComponent(name)}` : "/api/v1/aipod-specs";
|
||||
const method = name ? "PUT" : "POST";
|
||||
if (args.flags.get("dry-run") === true) {
|
||||
return { action: "aipod-spec-apply-plan", dryRun: true, mutation: false, request: { method, path: pathValue, yamlBytes: Buffer.byteLength(yaml, "utf8"), valuesPrinted: false }, next: { confirm: `./scripts/agentrun aipod-specs apply${name ? ` ${name}` : ""} --yaml-stdin` }, valuesPrinted: false };
|
||||
}
|
||||
return name ? client(args).put(pathValue, { yaml }) : client(args).post(pathValue, { yaml });
|
||||
}
|
||||
|
||||
async function aipodYamlInput(args: ParsedArgs): Promise<string> {
|
||||
if (args.flags.get("yaml-stdin") === true) return readStdinText();
|
||||
const file = optionalFlag(args, "yaml-file");
|
||||
if (!file) throw new AgentRunError("schema-invalid", "aipod-spec YAML input is required; use --yaml-stdin or --yaml-file <file>", { httpStatus: 2 });
|
||||
return readFile(file, "utf8");
|
||||
}
|
||||
|
||||
async function submitQueueTaskWithAipod(args: ParsedArgs, aipod: string): Promise<JsonValue> {
|
||||
const rendered = await renderAipodForCommand(args, aipod, 2);
|
||||
const body = rendered.queueTask as unknown as JsonRecord;
|
||||
if (args.flags.get("dry-run") === true) {
|
||||
return queueMutationDryRunPlan("queue-submit", null, "/api/v1/queue/tasks", body, "POST", queueSubmitAipodConfirmCommand(args, aipod), undefined, { source: "aipod-spec", aipod, preferred: "--aipod", valuesPrinted: false });
|
||||
}
|
||||
return client(args).post("/api/v1/queue/tasks", body);
|
||||
}
|
||||
|
||||
async function sessionTurnWithAipod(args: ParsedArgs, positionalSessionId: string | null, aipod: string): Promise<JsonRecord> {
|
||||
const sessionId = positionalSessionId ?? optionalFlag(args, "session-id") ?? newSessionId();
|
||||
const rendered = await renderAipodForCommand(args, aipod, positionalSessionId ? 3 : 2, { sessionId });
|
||||
const task = rendered.queueTask;
|
||||
const profile = String(task.backendProfile);
|
||||
if (positionalSessionId || optionalFlag(args, "session-id")) {
|
||||
try {
|
||||
await client(args).get(`/api/v1/sessions/${encodeURIComponent(sessionId)}/storage`);
|
||||
} catch {
|
||||
const expiresInDays = Number(optionalFlag(args, "expires-in-days") ?? 30);
|
||||
await client(args).post("/api/v1/sessions", {
|
||||
sessionId,
|
||||
tenantId: task.tenantId,
|
||||
projectId: task.projectId,
|
||||
backendProfile: task.backendProfile,
|
||||
expiresAt: new Date(Date.now() + Math.max(1, expiresInDays) * 24 * 60 * 60 * 1000).toISOString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
const sessionRef = objectField(task as unknown as JsonRecord, "sessionRef", {});
|
||||
const metadata = objectField(sessionRef, "metadata", {});
|
||||
const title = optionalFlag(args, "title") ?? task.title;
|
||||
if (title) metadata.title = title;
|
||||
const runBody: JsonRecord = {
|
||||
tenantId: task.tenantId,
|
||||
projectId: task.projectId,
|
||||
providerId: task.providerId ?? "G14",
|
||||
backendProfile: task.backendProfile,
|
||||
workspaceRef: task.workspaceRef ?? { kind: "opaque", path: "." },
|
||||
sessionRef: { ...sessionRef, sessionId, metadata },
|
||||
executionPolicy: task.executionPolicy,
|
||||
resourceBundleRef: task.resourceBundleRef,
|
||||
traceSink: { kind: "aipod-session", aipod, sessionId, valuesPrinted: false },
|
||||
};
|
||||
const run = await client(args).post("/api/v1/runs", runBody) as RunRecord;
|
||||
const commandBody: JsonRecord = { type: "turn", payload: task.payload };
|
||||
const commandIdempotencyKey = optionalFlag(args, "command-idempotency-key") ?? optionalFlag(args, "idempotency-key");
|
||||
if (commandIdempotencyKey) commandBody.idempotencyKey = commandIdempotencyKey;
|
||||
const command = await client(args).post(`/api/v1/runs/${encodeURIComponent(run.id)}/commands`, commandBody) as CommandRecord;
|
||||
let runnerJob: JsonValue = null;
|
||||
if (args.flags.get("no-runner-job") !== true) {
|
||||
const runnerDefaults = jsonRecordValue(rendered.dispatchDefaults.runnerJob) ?? {};
|
||||
const runnerOverrides = await optionalRunnerJsonFile(args);
|
||||
const runnerBody = { ...runnerDefaults, ...runnerOverrides, commandId: command.id } as JsonRecord;
|
||||
copyOptionalFlag(args, runnerBody, "image");
|
||||
copyOptionalFlag(args, runnerBody, "namespace");
|
||||
copyOptionalFlag(args, runnerBody, "attempt-id", "attemptId");
|
||||
copyOptionalFlag(args, runnerBody, "runner-id", "runnerId");
|
||||
copyOptionalFlag(args, runnerBody, "source-commit", "sourceCommit");
|
||||
copyRunnerManagerUrlFlag(args, runnerBody);
|
||||
copyOptionalFlag(args, runnerBody, "service-account-name", "serviceAccountName");
|
||||
runnerJob = await client(args).post(`/api/v1/runs/${encodeURIComponent(run.id)}/runner-jobs`, runnerBody);
|
||||
}
|
||||
return { action: "session-turn", aipod, sessionId, profile, run, command, runnerJob, valuesPrinted: false, pollCommands: { ps: `./scripts/agentrun sessions ps --reader-id cli --profile ${profile}`, show: `./scripts/agentrun sessions show ${sessionId} --reader-id cli`, trace: `./scripts/agentrun sessions trace ${sessionId} --after-seq 0 --limit 100`, output: `./scripts/agentrun sessions output ${sessionId} --after-seq 0 --limit 100`, read: `./scripts/agentrun sessions read ${sessionId} --reader-id cli`, steer: `./scripts/agentrun sessions steer ${sessionId} --prompt-file <file>`, cancel: `./scripts/agentrun sessions cancel ${sessionId}` } };
|
||||
}
|
||||
|
||||
async function submitQueueTask(args: ParsedArgs): Promise<JsonValue> {
|
||||
const aipod = optionalFlag(args, "aipod") ?? optionalFlag(args, "aipod-spec");
|
||||
if (aipod) return submitQueueTaskWithAipod(args, aipod);
|
||||
const body = await jsonFile(args);
|
||||
const idempotencyKey = optionalFlag(args, "idempotency-key");
|
||||
if (idempotencyKey) body.idempotencyKey = idempotencyKey;
|
||||
@@ -1270,6 +1401,40 @@ async function renderCodexSecret(args: ParsedArgs): Promise<JsonRecord> {
|
||||
return renderCodexProviderSecretPlan(options);
|
||||
}
|
||||
|
||||
async function setGithubSshToolCredentialCli(args: ParsedArgs): Promise<JsonRecord> {
|
||||
const privateKey = await textFromFileFlag(args, "private-key-file", "private key");
|
||||
const knownHosts = await textFromFileFlag(args, "known-hosts-file", "known_hosts");
|
||||
const configFile = optionalFlag(args, "config-file");
|
||||
const body: JsonRecord = { privateKey, knownHosts };
|
||||
if (configFile) body.config = await readFile(configFile, "utf8");
|
||||
if (args.flags.get("dry-run") === true) {
|
||||
return {
|
||||
action: "tool-credential-github-ssh-plan",
|
||||
mutation: false,
|
||||
dryRun: true,
|
||||
secretRef: { namespace: "agentrun-v01", name: "agentrun-v01-tool-github-ssh", keys: ["id_ed25519", "known_hosts", "config"], valuesPrinted: false },
|
||||
inputs: {
|
||||
privateKey: fileSummary("private-key-file", privateKey),
|
||||
knownHosts: fileSummary("known-hosts-file", knownHosts),
|
||||
config: configFile ? fileSummary("config-file", String(body.config)) : { provided: false, defaulted: true, valuesPrinted: false },
|
||||
},
|
||||
confirm: "./scripts/agentrun tool-credentials set-github-ssh --private-key-file <key> --known-hosts-file <known_hosts> [--config-file <ssh_config>]",
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
return await client(args).put("/api/v1/tool-credentials/github-ssh/credential", body) as JsonRecord;
|
||||
}
|
||||
|
||||
async function textFromFileFlag(args: ParsedArgs, flagName: string, label: string): Promise<string> {
|
||||
const file = optionalFlag(args, flagName);
|
||||
if (!file) throw new AgentRunError("schema-invalid", `tool-credentials set-github-ssh requires --${flagName} <file> for ${label}`, { httpStatus: 2 });
|
||||
return await readFile(file, "utf8");
|
||||
}
|
||||
|
||||
function fileSummary(flagName: string, text: string): JsonRecord {
|
||||
return { flag: flagName, bytes: Buffer.byteLength(text, "utf8"), valuesPrinted: false };
|
||||
}
|
||||
|
||||
async function setProviderProfileKey(args: ParsedArgs, profileValue: string): Promise<JsonRecord> {
|
||||
const profile = normalizeProfile(profileValue);
|
||||
if (args.flags.get("key-stdin") !== true) throw new AgentRunError("schema-invalid", "provider-profiles set-key requires --key-stdin", { httpStatus: 2 });
|
||||
@@ -1581,6 +1746,24 @@ async function readPrompt(args: ParsedArgs): Promise<string> {
|
||||
throw new AgentRunError("schema-invalid", "prompt is required; use --prompt, --prompt-file, --prompt-stdin, or trailing text", { httpStatus: 2 });
|
||||
}
|
||||
|
||||
async function optionalPrompt(args: ParsedArgs, trailingStart: number): Promise<string | undefined> {
|
||||
const promptFlag = optionalFlag(args, "prompt");
|
||||
if (promptFlag) return promptFlag;
|
||||
const promptFile = optionalFlag(args, "prompt-file");
|
||||
if (promptFile) {
|
||||
const text = await readFile(promptFile, "utf8");
|
||||
if (text.trim().length === 0) throw new AgentRunError("schema-invalid", "prompt file is empty", { httpStatus: 2 });
|
||||
return text;
|
||||
}
|
||||
if (args.flags.get("prompt-stdin") === true) {
|
||||
const text = await readStdinText();
|
||||
if (text.trim().length === 0) throw new AgentRunError("schema-invalid", "stdin prompt is empty", { httpStatus: 2 });
|
||||
return text;
|
||||
}
|
||||
const inline = args.positional.slice(trailingStart).join(" ").trim();
|
||||
return inline.length > 0 ? inline : undefined;
|
||||
}
|
||||
|
||||
async function readStdinText(): Promise<string> {
|
||||
const chunks: Buffer[] = [];
|
||||
for await (const chunk of process.stdin) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
||||
@@ -1687,7 +1870,7 @@ function help(args: ParsedArgs, group?: string): JsonRecord {
|
||||
"sessions storage <sessionId>",
|
||||
"sessions storage <sessionId> --delete",
|
||||
"sessions show <sessionId> [--reader-id <reader>]",
|
||||
"sessions turn [sessionId] [--json-stdin|--json-file <run-base.json>] [--prompt-stdin|--prompt-file <file>|--prompt <text>] [--profile codex|deepseek|minimax-m3|dsflash-go|<dynamic-profile>|M3] [--runner-json-stdin|--runner-json-file <job.json>]",
|
||||
"sessions turn [sessionId] [--aipod <name>|--json-stdin|--json-file <run-base.json>] [--prompt-stdin|--prompt-file <file>|--prompt <text>] [--profile codex|deepseek|minimax-m3|dsflash-go|<dynamic-profile>|M3] [--runner-json-stdin|--runner-json-file <job.json>] [--no-runner-job]",
|
||||
"sessions steer <sessionId> [--prompt-stdin|--prompt-file <file>|--prompt <text>]",
|
||||
"sessions cancel <sessionId> [--reason <text>] [--full|--raw]",
|
||||
"sessions trace <sessionId> [--after-seq <n>] [--limit <n>] [--run-id <runId>] [--summary-chars <n>] [--include-output] [--seq <n>|--event-id <id>|--item-id <id>] [--detail-scan-pages <n>] [--full|--raw]",
|
||||
@@ -1703,6 +1886,7 @@ function help(args: ParsedArgs, group?: string): JsonRecord {
|
||||
"runner jobs --run-id <runId> [--command-id <commandId>]",
|
||||
"runner job-status [runnerJobId] --run-id <runId>",
|
||||
"queue submit --json-stdin|--json-file <task.json> [--idempotency-key <key>] [--dry-run]",
|
||||
"queue submit --aipod <name> [--prompt-stdin|--prompt-file <file>|--prompt <text>] [--idempotency-key <key>] [--dry-run]",
|
||||
"queue list [--queue <queue>] [--state <state>] [--cursor <cursor>] [--limit <limit>] [--updated-after <version>] [--full|--raw]",
|
||||
"queue show <taskId> [--full|--raw]",
|
||||
"queue stats [--queue <queue>]",
|
||||
@@ -1711,7 +1895,15 @@ function help(args: ParsedArgs, group?: string): JsonRecord {
|
||||
"queue cancel <taskId> [--reason <text>] [--dry-run] [--full|--raw]",
|
||||
"queue dispatch <taskId> [--json-stdin|--json-file <dispatch.json>] [--idempotency-key <key>] [--image <image>] [--namespace <namespace>] [--dry-run] [--full|--raw]",
|
||||
"queue refresh <taskId> [--dry-run] [--full|--raw]",
|
||||
"aipod-specs list",
|
||||
"aipod-specs show <name>",
|
||||
"aipod-specs render <name> [--json-stdin|--json-file <input.json>] [--prompt-stdin|--prompt-file <file>|--prompt <text>]",
|
||||
"aipod-specs apply [name] --yaml-stdin|--yaml-file <spec.yaml> [--dry-run]",
|
||||
"aipod-specs delete <name>",
|
||||
"secrets codex render --dry-run [--profile codex|deepseek|minimax-m3|dsflash-go|<dynamic-profile>] [--codex-home <dir>] [--model-catalog-file <file>] [--namespace agentrun-v01] [--secret-name <name>]",
|
||||
"tool-credentials list",
|
||||
"tool-credentials show github-ssh|unidesk-ssh",
|
||||
"tool-credentials set-github-ssh --private-key-file <id_ed25519> --known-hosts-file <known_hosts> [--config-file <ssh_config>] [--dry-run]",
|
||||
"provider-profiles list",
|
||||
"provider-profiles show <profile>",
|
||||
"provider-profiles config <profile>",
|
||||
|
||||
Reference in New Issue
Block a user