1158 lines
64 KiB
TypeScript
1158 lines
64 KiB
TypeScript
// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. rest-bridge module for scripts/src/agentrun.ts.
|
|
|
|
// Moved mechanically from scripts/src/agentrun.ts:5847-6915 for #903.
|
|
|
|
// SPEC: PJ2026-01020108 cancel lifecycle + PJ2026-01020205 AipodSpec binding + PJ2026-01020302 session policy + PJ2026-01020305 cancel control + PJ2026-01060305/06 YAML execution policy and bounded output draft-2026-06-25-p0.
|
|
// Exposes AgentRun lane-scoped policy, AipodSpec SecretRef binding, cancel lifecycle, and bounded default output in the UniDesk CLI.
|
|
import { chmodSync, copyFileSync, existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { spawnSync } from "node:child_process";
|
|
import { rootPath, type UniDeskConfig } from "../config";
|
|
import type { RenderedCliResult } from "../output";
|
|
import { applyLocalCaddyManagedSite } from "../pk01-caddy";
|
|
import { runSshCommandCapture, type SshCaptureResult } from "../ssh";
|
|
import { runRemoteSshCommandCapture } from "../remote";
|
|
import { startJob } from "../jobs";
|
|
import {
|
|
AGENTRUN_CONFIG_PATH,
|
|
agentRunLaneSummary,
|
|
agentRunPipelineRunName,
|
|
agentRunProviderCredentialRefs,
|
|
resolveAgentRunLaneTarget,
|
|
type AgentRunCancelLifecycleSpec,
|
|
type AgentRunLaneSpec,
|
|
} from "../agentrun-lanes";
|
|
import {
|
|
agentRunImageArtifact,
|
|
placeholderAgentRunImage,
|
|
renderedFilesDigest,
|
|
renderedObjectsDigest,
|
|
renderAgentRunControlPlaneManifests,
|
|
renderAgentRunGitopsFiles,
|
|
type AgentRunArtifactService,
|
|
} from "../agentrun-manifests";
|
|
import { sha256Fingerprint } from "../platform-infra-ops-library";
|
|
|
|
import type { AgentRunClientConfig } from "./config";
|
|
import type { GitMirrorOptions } from "./options";
|
|
import type { AgentRunFailureKind, AgentRunHttpMethod, AgentRunResourceOptions, AgentRunRestCompatGroup, AgentRunRestCompatOptions, AgentRunRestTarget, AgentRunRestTargetOptions } from "./utils";
|
|
import { addAgentRunNotFoundLookupHint, agentRunDryRunPlan, agentRunHasFlag, agentRunLaneRestBridgeMetadata, agentRunOption, agentRunQuery, agentRunRestBridgeMetadata, aipodRenderInputFromArgs, cancelBodyFromArgs, classifyAgentRunProxyFailureKind, cloneJsonRecord, copyAgentRunOptions, defaultAgentRunExecutionPolicy, defaultAgentRunProviderId, jsonInputDisclosureFromArgs, jsonObjectOption, normalizeAgentRunFailureKind, optionalJsonBody, optionalPromptFromArgs, optionalRunnerJsonBody, queueDispatchBodyFromArgs, queueSubmitConfirmCommand, readAgentRunClientConfig, readYamlInputFromArgs, requiredAgentRunOption, requiredJsonBody, resolveAgentRunAuth, resolveAgentRunSessionPolicyTarget, safeAgentRunEnvelope } from "./config";
|
|
import { status } from "./control-plane";
|
|
import { gitMirrorStatus } from "./git-mirror";
|
|
import { arrayRecords, displayValue, isRecord, pickCompact, renderTable, truncateOneLine } from "./options";
|
|
import { innerData, pathValue, renderMachine, renderedCliResult } from "./render";
|
|
import { parseResourceOptions, stripAgentRunResourceWrapperArgs } from "./resource-actions";
|
|
import { AGENTRUN_GIT_MIRROR_RETRY_MAX_ATTEMPTS, agentRunGitMirrorRetryDelayMs, agentRunGitMirrorRetrySummary, agentRunGitMirrorRetryableFailure, createYamlLaneJobScript, yamlLaneGitMirrorJobManifest, yamlLaneGitMirrorStatusScript, yamlLaneJobProbeScript } from "./secrets";
|
|
import { capture, captureJsonPayload, compactCapture, nonNegativeIntegerOrNull, progressEvent, record, shQuote, sleep, stringOrNull } from "./utils";
|
|
|
|
export async function readGitMirrorStatus(config: UniDeskConfig, target: { configPath: string; spec: AgentRunLaneSpec }): Promise<Record<string, unknown> & { result: SshCaptureResult; raw: string; summary: Record<string, unknown> }> {
|
|
const spec = target.spec;
|
|
const result = await capture(config, spec.nodeKubeRoute, ["sh", "--", yamlLaneGitMirrorStatusScript(spec)]);
|
|
const raw = result.stdout;
|
|
const summary = captureJsonPayload(result);
|
|
return {
|
|
ok: result.exitCode === 0 && summary.ok !== false,
|
|
configPath: target.configPath,
|
|
target: agentRunLaneSummary(spec),
|
|
namespace: spec.gitMirror.namespace,
|
|
readUrl: spec.gitMirror.readUrl,
|
|
writeUrl: spec.gitMirror.writeUrl,
|
|
raw,
|
|
summary,
|
|
result,
|
|
};
|
|
}
|
|
|
|
export async function runGitMirrorJob(config: UniDeskConfig, action: "sync" | "flush", options: GitMirrorOptions): Promise<Record<string, unknown>> {
|
|
const { configPath, spec } = resolveAgentRunLaneTarget(options);
|
|
const previewJobName = `${action === "sync" ? spec.gitMirror.syncJobPrefix : spec.gitMirror.flushJobPrefix}-${Date.now().toString(36)}`.slice(0, 63);
|
|
const previewManifest = yamlLaneGitMirrorJobManifest(spec, action, previewJobName);
|
|
const command = `agentrun git-mirror ${action}`;
|
|
if (options.dryRun || !options.confirm) {
|
|
return {
|
|
ok: true,
|
|
command,
|
|
mode: "yaml-declared-node-lane",
|
|
configPath,
|
|
target: agentRunLaneSummary(spec),
|
|
dryRun: true,
|
|
namespace: spec.gitMirror.namespace,
|
|
jobName: previewJobName,
|
|
manifest: previewManifest,
|
|
next: { confirm: `bun scripts/cli.ts ${command} --node ${spec.nodeId} --lane ${spec.lane} --confirm` },
|
|
};
|
|
}
|
|
if (!options.wait) {
|
|
const jobName = previewJobName;
|
|
const manifest = yamlLaneGitMirrorJobManifest(spec, action, jobName);
|
|
const created = await capture(config, spec.nodeKubeRoute, ["sh", "--", createYamlLaneJobScript(spec.gitMirror.namespace, jobName, manifest)]);
|
|
const status = await gitMirrorStatus(config, { full: false, raw: false, node: spec.nodeId, lane: spec.lane });
|
|
return {
|
|
ok: created.exitCode === 0,
|
|
command,
|
|
mode: created.exitCode === 0 ? "submitted" : "create-failed",
|
|
configPath,
|
|
target: agentRunLaneSummary(spec),
|
|
dryRun: false,
|
|
namespace: spec.gitMirror.namespace,
|
|
jobName,
|
|
result: compactCapture(created),
|
|
status,
|
|
next: {
|
|
status: `bun scripts/cli.ts agentrun git-mirror status --node ${spec.nodeId} --lane ${spec.lane}`,
|
|
wait: `bun scripts/cli.ts agentrun git-mirror ${action} --node ${spec.nodeId} --lane ${spec.lane} --confirm --wait`,
|
|
},
|
|
};
|
|
}
|
|
const attempts: Record<string, unknown>[] = [];
|
|
let lastCreated: SshCaptureResult | null = null;
|
|
let lastWait: Record<string, unknown> | null = null;
|
|
for (let attempt = 1; attempt <= AGENTRUN_GIT_MIRROR_RETRY_MAX_ATTEMPTS; attempt += 1) {
|
|
const jobName = `${action === "sync" ? spec.gitMirror.syncJobPrefix : spec.gitMirror.flushJobPrefix}-${Date.now().toString(36)}-${attempt}`.slice(0, 63);
|
|
const manifest = yamlLaneGitMirrorJobManifest(spec, action, jobName);
|
|
const created = await capture(config, spec.nodeKubeRoute, ["sh", "--", createYamlLaneJobScript(spec.gitMirror.namespace, jobName, manifest)]);
|
|
lastCreated = created;
|
|
if (created.exitCode !== 0) {
|
|
const result = { phase: "create-job", capture: compactCapture(created, { full: true, stdoutTailChars: 4000, stderrTailChars: 4000 }) };
|
|
const retryableFailure = agentRunGitMirrorRetryableFailure(spec, action, attempt, result);
|
|
attempts.push({ attempt, retryLabel: `${attempt}/${AGENTRUN_GIT_MIRROR_RETRY_MAX_ATTEMPTS}`, jobName, ok: false, phase: "create-job", retryableFailure, retryExhausted: retryableFailure !== null && record(retryableFailure).retryExhausted === true });
|
|
if (retryableFailure !== null && record(retryableFailure).retryExhausted !== true) {
|
|
progressEvent(`agentrun.git-mirror.${action}.retry`, retryableFailure);
|
|
await sleep(agentRunGitMirrorRetryDelayMs(attempt));
|
|
continue;
|
|
}
|
|
const status = await gitMirrorStatus(config, { full: false, raw: false, node: spec.nodeId, lane: spec.lane });
|
|
return {
|
|
ok: false,
|
|
command,
|
|
mode: "create-failed",
|
|
configPath,
|
|
target: agentRunLaneSummary(spec),
|
|
dryRun: false,
|
|
namespace: spec.gitMirror.namespace,
|
|
jobName,
|
|
result: compactCapture(created),
|
|
retryableFailure,
|
|
retry: agentRunGitMirrorRetrySummary(attempts),
|
|
status,
|
|
next: {
|
|
status: `bun scripts/cli.ts agentrun git-mirror status --node ${spec.nodeId} --lane ${spec.lane}`,
|
|
wait: `bun scripts/cli.ts agentrun git-mirror ${action} --node ${spec.nodeId} --lane ${spec.lane} --confirm --wait`,
|
|
},
|
|
};
|
|
}
|
|
const wait = await waitForGitMirrorJob(config, spec, action, jobName, options.timeoutSeconds);
|
|
lastWait = wait;
|
|
if (wait.ok === true) {
|
|
attempts.push({ attempt, retryLabel: `${attempt}/${AGENTRUN_GIT_MIRROR_RETRY_MAX_ATTEMPTS}`, jobName, ok: true, wait });
|
|
const status = await gitMirrorStatus(config, { full: false, raw: false, node: spec.nodeId, lane: spec.lane });
|
|
return {
|
|
ok: true,
|
|
command,
|
|
mode: "waited",
|
|
configPath,
|
|
target: agentRunLaneSummary(spec),
|
|
dryRun: false,
|
|
namespace: spec.gitMirror.namespace,
|
|
jobName,
|
|
result: compactCapture(created),
|
|
wait,
|
|
retry: agentRunGitMirrorRetrySummary(attempts),
|
|
status,
|
|
next: {
|
|
status: `bun scripts/cli.ts agentrun git-mirror status --node ${spec.nodeId} --lane ${spec.lane}`,
|
|
flush: action === "sync" && status.summary && record(status.summary).pendingFlush === true ? `bun scripts/cli.ts agentrun git-mirror flush --node ${spec.nodeId} --lane ${spec.lane} --confirm` : null,
|
|
},
|
|
};
|
|
}
|
|
const retryableFailure = agentRunGitMirrorRetryableFailure(spec, action, attempt, wait);
|
|
attempts.push({ attempt, retryLabel: `${attempt}/${AGENTRUN_GIT_MIRROR_RETRY_MAX_ATTEMPTS}`, jobName, ok: false, wait, retryableFailure, retryExhausted: retryableFailure !== null && record(retryableFailure).retryExhausted === true });
|
|
if (retryableFailure !== null && record(retryableFailure).retryExhausted !== true) {
|
|
progressEvent(`agentrun.git-mirror.${action}.retry`, retryableFailure);
|
|
await sleep(agentRunGitMirrorRetryDelayMs(attempt));
|
|
continue;
|
|
}
|
|
const status = await gitMirrorStatus(config, { full: false, raw: false, node: spec.nodeId, lane: spec.lane });
|
|
return {
|
|
ok: false,
|
|
command,
|
|
mode: "waited",
|
|
configPath,
|
|
target: agentRunLaneSummary(spec),
|
|
dryRun: false,
|
|
namespace: spec.gitMirror.namespace,
|
|
jobName,
|
|
result: compactCapture(created),
|
|
wait,
|
|
retryableFailure,
|
|
retry: agentRunGitMirrorRetrySummary(attempts),
|
|
status,
|
|
next: {
|
|
status: `bun scripts/cli.ts agentrun git-mirror status --node ${spec.nodeId} --lane ${spec.lane}`,
|
|
wait: `bun scripts/cli.ts agentrun git-mirror ${action} --node ${spec.nodeId} --lane ${spec.lane} --confirm --wait`,
|
|
},
|
|
};
|
|
}
|
|
const status = await gitMirrorStatus(config, { full: false, raw: false, node: spec.nodeId, lane: spec.lane });
|
|
return {
|
|
ok: false,
|
|
command,
|
|
mode: "retry-exhausted",
|
|
configPath,
|
|
target: agentRunLaneSummary(spec),
|
|
dryRun: false,
|
|
namespace: spec.gitMirror.namespace,
|
|
result: lastCreated === null ? null : compactCapture(lastCreated),
|
|
wait: lastWait,
|
|
retry: agentRunGitMirrorRetrySummary(attempts),
|
|
status,
|
|
next: { status: `bun scripts/cli.ts agentrun git-mirror status --node ${spec.nodeId} --lane ${spec.lane}` },
|
|
};
|
|
}
|
|
|
|
export async function waitForGitMirrorJob(config: UniDeskConfig, spec: AgentRunLaneSpec, action: "sync" | "flush", jobName: string, timeoutSeconds: number): Promise<Record<string, unknown>> {
|
|
const startedAtMs = Date.now();
|
|
let lastProbe: SshCaptureResult | null = null;
|
|
let polls = 0;
|
|
while (Date.now() - startedAtMs <= timeoutSeconds * 1000) {
|
|
polls += 1;
|
|
lastProbe = await capture(config, spec.nodeKubeRoute, ["sh", "--", yamlLaneJobProbeScript(spec.gitMirror.namespace, jobName)]);
|
|
const summary = captureJsonPayload(lastProbe);
|
|
process.stderr.write(`${JSON.stringify({
|
|
event: `agentrun.git-mirror.${action}.progress`,
|
|
at: new Date().toISOString(),
|
|
stage: "k8s-job",
|
|
node: spec.nodeId,
|
|
lane: spec.lane,
|
|
status: summary.succeeded === true ? "succeeded" : summary.failed === true ? "failed" : "running",
|
|
jobName,
|
|
polls,
|
|
elapsedMs: Date.now() - startedAtMs,
|
|
})}\n`);
|
|
if (summary.succeeded === true) {
|
|
return {
|
|
ok: true,
|
|
action,
|
|
jobName,
|
|
polls,
|
|
elapsedMs: Date.now() - startedAtMs,
|
|
summary,
|
|
probe: compactCapture(lastProbe),
|
|
};
|
|
}
|
|
if (summary.failed === true) {
|
|
return {
|
|
ok: false,
|
|
action,
|
|
jobName,
|
|
polls,
|
|
elapsedMs: Date.now() - startedAtMs,
|
|
degradedReason: "git-mirror-job-failed",
|
|
summary,
|
|
probe: compactCapture(lastProbe),
|
|
};
|
|
}
|
|
await sleep(5_000);
|
|
}
|
|
return {
|
|
ok: false,
|
|
action,
|
|
jobName,
|
|
polls,
|
|
elapsedMs: Date.now() - startedAtMs,
|
|
degradedReason: "git-mirror-job-timeout",
|
|
probe: lastProbe ? compactCapture(lastProbe) : null,
|
|
};
|
|
}
|
|
|
|
export function startAsyncAgentRunJob(name: string, command: string[], note: string): Record<string, unknown> {
|
|
const job = startJob(name, command, note);
|
|
const statusCommand = `bun scripts/cli.ts job status ${job.id} --tail-bytes 12000`;
|
|
return {
|
|
ok: true,
|
|
mode: "async-job",
|
|
job,
|
|
statusCommand,
|
|
tailCommand: `tail -f ${job.stdoutFile}`,
|
|
next: {
|
|
status: statusCommand,
|
|
tail: `tail -f ${job.stdoutFile}`,
|
|
},
|
|
};
|
|
}
|
|
|
|
export async function runAgentRunRestCompatCommand(config: UniDeskConfig | null, group: AgentRunRestCompatGroup, args: string[], canonicalArgs: string[]): Promise<Record<string, unknown> | RenderedCliResult> {
|
|
const options = parseAgentRunRestCompatOptions(args);
|
|
const command = `agentrun ${canonicalArgs.join(" ")}`.trim();
|
|
try {
|
|
const raw = await withAgentRunRestTarget(resolveAgentRunRestTarget(config, options), async () => {
|
|
return await runAgentRunRestCommand(config, group, stripAgentRunResourceWrapperArgs(args));
|
|
});
|
|
return renderAgentRunRestCompatResult(command, group, stripAgentRunResourceWrapperArgs(args), raw, options);
|
|
} catch (error) {
|
|
if (error instanceof AgentRunRestError) {
|
|
if (options.raw || options.full || options.output === "json" || options.output === "yaml") {
|
|
return renderMachine(command, error.toPayload(command), options.output === "yaml" ? "yaml" : "json", false);
|
|
}
|
|
return error.toPayload(command);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export function parseAgentRunRestCompatOptions(args: string[]): AgentRunRestCompatOptions {
|
|
const resourceOptions = parseResourceOptions(args);
|
|
return {
|
|
node: resourceOptions.node,
|
|
lane: resourceOptions.lane,
|
|
output: resourceOptions.output,
|
|
full: resourceOptions.full,
|
|
raw: resourceOptions.raw,
|
|
};
|
|
}
|
|
|
|
export function renderAgentRunRestCompatResult(command: string, group: AgentRunRestCompatGroup, args: string[], raw: Record<string, unknown>, options: AgentRunRestCompatOptions): Record<string, unknown> | RenderedCliResult {
|
|
if (options.raw || options.full) return renderMachine(command, raw, "json", raw.ok !== false);
|
|
if (options.output === "json" || options.output === "yaml") return renderMachine(command, raw, options.output, raw.ok !== false);
|
|
const compatGroup = group === "aipods" ? "aipod-specs" : group;
|
|
const [action, id] = args;
|
|
if (compatGroup === "aipod-specs" && action === "render" && id !== undefined) return renderAgentRunAipodSpecRenderSummary(command, raw, id);
|
|
return raw;
|
|
}
|
|
|
|
export function renderAgentRunAipodSpecRenderSummary(command: string, raw: Record<string, unknown>, requestedAipod: string): RenderedCliResult {
|
|
const data = record(innerData(raw));
|
|
const aipod = record(data.aipod);
|
|
const task = record(data.queueTask);
|
|
const bridge = record(raw.bridge);
|
|
const executionPolicy = record(task.executionPolicy);
|
|
const secretScope = record(executionPolicy.secretScope);
|
|
const resourceBundle = record(task.resourceBundleRef ?? aipod.resourceBundleRef);
|
|
const dispatchDefaults = record(data.dispatchDefaults);
|
|
const runnerJobDefaults = record(dispatchDefaults.runnerJob);
|
|
const node = stringOrNull(bridge.node) ?? stringOrNull(task.providerId) ?? "-";
|
|
const lane = stringOrNull(bridge.lane) ?? stringOrNull(task.lane) ?? "-";
|
|
const namespace = stringOrNull(bridge.namespace) ?? stringOrNull(runnerJobDefaults.namespace) ?? "-";
|
|
const providerCredentials = agentRunRenderedProviderCredentials(secretScope, aipod);
|
|
const toolCredentials = agentRunRenderedToolCredentials(secretScope, aipod);
|
|
const bundleNames = agentRunResourceBundleNames(resourceBundle.bundles, "name");
|
|
const skillNames = agentRunResourceBundleNames(resourceBundle.requiredSkills, "name");
|
|
const targetArgs = agentRunCompatTargetCliArgs(node, lane);
|
|
const lines = [
|
|
"AIPODSPEC RENDER",
|
|
` AipodSpec: ${displayValue(aipod.name ?? requestedAipod)} hash=${displayValue(aipod.specHash)}`,
|
|
` Target: node=${displayValue(node)} lane=${displayValue(lane)} namespace=${displayValue(namespace)} bridge=${displayValue(bridge.mode)}`,
|
|
` TaskPolicy: backendProfile=${displayValue(task.backendProfile)} providerId=${displayValue(task.providerId)} queue=${displayValue(task.queue)} lane=${displayValue(task.lane)}`,
|
|
` WorkspaceRef: ${agentRunCompactJson(task.workspaceRef, 140)}`,
|
|
` Execution: ${agentRunCompactJson(pickCompact(executionPolicy, ["sandbox", "approval", "timeoutMs", "network"]), 160)}`,
|
|
" Values: secret payloads are not printed; valuesPrinted=false",
|
|
"",
|
|
"PROVIDER CREDENTIAL SECRETREFS",
|
|
providerCredentials.length > 0
|
|
? renderTable(["PROFILE", "NAMESPACE", "SECRET", "KEYS", "VALUES"], providerCredentials.map((credential) => [
|
|
displayValue(credential.profile),
|
|
displayValue(credential.namespace ?? namespace),
|
|
displayValue(credential.name),
|
|
agentRunJoinNames(credential.keys, 5),
|
|
"false",
|
|
]))
|
|
: " (none)",
|
|
"",
|
|
"TOOL CREDENTIAL SECRETREFS",
|
|
toolCredentials.length > 0
|
|
? renderTable(["TOOL", "PURPOSE", "NAMESPACE", "SECRET", "KEYS", "PROJECTION", "VALUES"], toolCredentials.map((credential) => [
|
|
displayValue(credential.tool),
|
|
displayValue(credential.purpose),
|
|
displayValue(credential.namespace ?? namespace),
|
|
displayValue(credential.name),
|
|
agentRunJoinNames(credential.keys, 5),
|
|
agentRunProjectionSummary(credential.projection),
|
|
"false",
|
|
]))
|
|
: " (none)",
|
|
"",
|
|
"RESOURCE BUNDLE",
|
|
` Kind: ${displayValue(resourceBundle.kind)} repo=${displayValue(resourceBundle.repoUrl)} ref=${displayValue(resourceBundle.ref)} commit=${displayValue(resourceBundle.commitId)}`,
|
|
` Bundles: ${agentRunJoinNames(bundleNames, 6)}`,
|
|
` RequiredSkills: ${agentRunJoinNames(skillNames, 8)}`,
|
|
` RunnerJob: namespace=${displayValue(runnerJobDefaults.namespace)} image=${agentRunCompactJson(runnerJobDefaults.imageRef, 120)}`,
|
|
"",
|
|
"DRILL-DOWN",
|
|
` bun scripts/cli.ts ${command} --full`,
|
|
` bun scripts/cli.ts ${command} -o json`,
|
|
` bun scripts/cli.ts agentrun create task --aipod ${requestedAipod}${targetArgs} --prompt-stdin`,
|
|
];
|
|
return renderedCliResult(raw.ok !== false, command, `${lines.join("\n")}\n`);
|
|
}
|
|
|
|
export function agentRunRenderedProviderCredentials(secretScope: Record<string, unknown>, aipod: Record<string, unknown>): Array<{ profile: unknown; namespace: unknown; name: unknown; keys: string[] }> {
|
|
const scoped = arrayRecords(secretScope.providerCredentials).map((credential) => {
|
|
const secretRef = record(credential.secretRef);
|
|
return {
|
|
profile: credential.profile,
|
|
namespace: secretRef.namespace,
|
|
name: secretRef.name,
|
|
keys: agentRunStringList(secretRef.keys),
|
|
};
|
|
});
|
|
if (scoped.length > 0) return scoped;
|
|
return arrayRecords(record(aipod.providerCredentials).items).map((credential) => ({
|
|
profile: credential.profile,
|
|
namespace: credential.namespace,
|
|
name: credential.name,
|
|
keys: agentRunStringList(credential.keys),
|
|
}));
|
|
}
|
|
|
|
export function agentRunRenderedToolCredentials(secretScope: Record<string, unknown>, aipod: Record<string, unknown>): Array<{ tool: unknown; purpose: unknown; namespace: unknown; name: unknown; keys: string[]; projection: Record<string, unknown> }> {
|
|
const scoped = arrayRecords(secretScope.toolCredentials).map((credential) => {
|
|
const secretRef = record(credential.secretRef);
|
|
return {
|
|
tool: credential.tool,
|
|
purpose: credential.purpose,
|
|
namespace: secretRef.namespace,
|
|
name: secretRef.name,
|
|
keys: agentRunStringList(secretRef.keys),
|
|
projection: record(credential.projection),
|
|
};
|
|
});
|
|
if (scoped.length > 0) return scoped;
|
|
return arrayRecords(record(aipod.toolCredentials).items).map((credential) => ({
|
|
tool: credential.tool,
|
|
purpose: credential.purpose,
|
|
namespace: credential.namespace,
|
|
name: credential.name,
|
|
keys: agentRunStringList(credential.keys),
|
|
projection: record(credential.projection),
|
|
}));
|
|
}
|
|
|
|
export function agentRunResourceBundleNames(value: unknown, key: string): string[] {
|
|
const direct = agentRunStringList(record(value).names);
|
|
if (direct.length > 0) return direct;
|
|
return arrayRecords(value).map((item) => stringOrNull(item[key]) ?? "").filter((item) => item.length > 0);
|
|
}
|
|
|
|
export function agentRunStringList(value: unknown): string[] {
|
|
if (!Array.isArray(value)) return [];
|
|
return value.map((item) => String(item)).filter((item) => item.length > 0);
|
|
}
|
|
|
|
export function agentRunJoinNames(values: string[], maxItems: number): string {
|
|
if (values.length === 0) return "-";
|
|
const visible = values.slice(0, maxItems);
|
|
const suffix = values.length > visible.length ? ` +${values.length - visible.length}` : "";
|
|
return `${visible.join(", ")}${suffix}`;
|
|
}
|
|
|
|
export function agentRunProjectionSummary(value: Record<string, unknown>): string {
|
|
const kind = stringOrNull(value.kind);
|
|
if (kind === "env") return `env:${displayValue(value.envName)}<-${displayValue(value.secretKey)}`;
|
|
if (kind === "volume") return `volume:${displayValue(value.mountPath)}`;
|
|
if (Object.keys(value).length === 0) return "-";
|
|
return agentRunCompactJson(value, 80);
|
|
}
|
|
|
|
export function agentRunCompactJson(value: unknown, maxChars: number): string {
|
|
if (value === undefined || value === null) return "-";
|
|
return truncateOneLine(JSON.stringify(value), maxChars);
|
|
}
|
|
|
|
export function agentRunCompatTargetCliArgs(node: string, lane: string): string {
|
|
const parts: string[] = [];
|
|
if (node !== "-") parts.push("--node", node);
|
|
if (lane !== "-") parts.push("--lane", lane);
|
|
return parts.length === 0 ? "" : ` ${parts.join(" ")}`;
|
|
}
|
|
|
|
export async function runAgentRunRestCommand(config: UniDeskConfig | null, group: AgentRunRestCompatGroup, args: string[]): Promise<Record<string, unknown>> {
|
|
void config;
|
|
const compatGroup = group === "aipods" ? "aipod-specs" : group;
|
|
const [action, id] = args;
|
|
if (compatGroup === "queue") return await runAgentRunQueueRest(action, id, args);
|
|
if (compatGroup === "sessions") return await runAgentRunSessionsRest(action, id, args);
|
|
if (compatGroup === "runs") return await runAgentRunRunsRest(action, id, args);
|
|
if (compatGroup === "commands") return await runAgentRunCommandsRest(action, id, args);
|
|
if (compatGroup === "runner") return await runAgentRunRunnerRest(action, id, args);
|
|
if (compatGroup === "aipod-specs") return await runAgentRunAipodSpecsRest(action, id, args);
|
|
throw new AgentRunRestError("unsupported-version", `unsupported AgentRun REST compatibility group: ${group}`);
|
|
}
|
|
|
|
export async function runAgentRunQueueRest(action: string | undefined, id: string | undefined, args: string[]): Promise<Record<string, unknown>> {
|
|
if (action === "list") return await agentRunRestRequest("agentrun queue list", "GET", `/api/v1/queue/tasks${agentRunQuery(args, ["queue", "state", "cursor", "limit", "updated-after"])}`);
|
|
if (action === "commander") return await agentRunRestRequest("agentrun queue commander", "GET", `/api/v1/queue/commander${agentRunQuery(args, ["queue", "reader-id"])}`);
|
|
if (action === "stats") return await agentRunRestRequest("agentrun queue stats", "GET", `/api/v1/queue/stats${agentRunQuery(args, ["queue"])}`);
|
|
if (action === "attempts" && id) return await agentRunRestRequest("agentrun queue attempts", "GET", `/api/v1/queue/tasks/${encodeURIComponent(id)}/attempts${agentRunQuery(args, ["cursor", "limit"])}`);
|
|
if (action === "show" && id) return await agentRunRestRequest("agentrun queue show", "GET", `/api/v1/queue/tasks/${encodeURIComponent(id)}`);
|
|
if (action === "submit") return await submitQueueTaskRest(args);
|
|
if (action === "dispatch" && id) return await mutateQueueTaskRest("queue-dispatch", id, "dispatch", queueDispatchBodyFromArgs(args), args);
|
|
if (action === "retry" && id) return await retryQueueTaskRest(id, args);
|
|
if (action === "read" && id) return await mutateQueueTaskRest("queue-read", id, "read", { readerId: agentRunOption(args, "reader-id") ?? "cli" }, args);
|
|
if (action === "cancel" && id) return await mutateQueueTaskRest("queue-cancel", id, "cancel", cancelBodyFromArgs(args), args);
|
|
if (action === "refresh" && id) return await mutateQueueTaskRest("queue-refresh", id, "refresh", {}, args);
|
|
throw new AgentRunRestError("validation-failed", `unsupported queue command: ${[action, id].filter(Boolean).join(" ") || "(empty)"}`);
|
|
}
|
|
|
|
export async function runAgentRunSessionsRest(action: string | undefined, id: string | undefined, args: string[]): Promise<Record<string, unknown>> {
|
|
if (action === "ps") return await agentRunRestRequest("agentrun sessions ps", "GET", `/api/v1/sessions${agentRunQuery(args, ["state", "profile", "backend-profile", "reader-id", "cursor", "limit"])}`);
|
|
if (action === "show" && id) return await agentRunRestRequest("agentrun sessions show", "GET", `/api/v1/sessions/${encodeURIComponent(id)}${agentRunQuery(args, ["reader-id"])}`);
|
|
if ((action === "trace" || action === "output") && id) return await agentRunRestRequest(`agentrun sessions ${action}`, "GET", `/api/v1/sessions/${encodeURIComponent(id)}/${action}${agentRunQuery(args, ["after-seq", "limit", "run-id"])}`);
|
|
if (action === "read" && id) {
|
|
const body = { readerId: agentRunOption(args, "reader-id") ?? "cli" };
|
|
if (agentRunHasFlag(args, "dry-run")) return agentRunDryRunPlan("session-read", `/api/v1/sessions/${encodeURIComponent(id)}/read`, body, `bun scripts/cli.ts agentrun sessions read ${id} --reader-id ${body.readerId}`);
|
|
return await agentRunRestRequest("agentrun sessions read", "POST", `/api/v1/sessions/${encodeURIComponent(id)}/read`, body);
|
|
}
|
|
if (action === "cancel" && id) {
|
|
const body = { action: "cancel", ...cancelBodyFromArgs(args) };
|
|
if (agentRunHasFlag(args, "dry-run")) return agentRunDryRunPlan("session-cancel", `/api/v1/sessions/${encodeURIComponent(id)}/control`, body, `bun scripts/cli.ts agentrun sessions cancel ${id}`);
|
|
return await agentRunRestRequest("agentrun sessions cancel", "POST", `/api/v1/sessions/${encodeURIComponent(id)}/control`, body);
|
|
}
|
|
if (action === "send" && id) return await sessionSendRest(id, args);
|
|
throw new AgentRunRestError("validation-failed", `unsupported sessions command: ${[action, id].filter(Boolean).join(" ") || "(empty)"}`);
|
|
}
|
|
|
|
export async function runAgentRunRunsRest(action: string | undefined, id: string | undefined, args: string[]): Promise<Record<string, unknown>> {
|
|
if (action === "create") return await agentRunRestRequest("agentrun runs create", "POST", "/api/v1/runs", await requiredJsonBody(args, "runs create"));
|
|
if (action === "show" && id) return await agentRunRestRequest("agentrun runs show", "GET", `/api/v1/runs/${encodeURIComponent(id)}`);
|
|
if (action === "events" && id) return await agentRunRestRequest("agentrun runs events", "GET", `/api/v1/runs/${encodeURIComponent(id)}/events${agentRunQuery(args, ["after-seq", "limit"])}`);
|
|
if (action === "result" && id) return await agentRunRestRequest("agentrun runs result", "GET", `/api/v1/runs/${encodeURIComponent(id)}/result${agentRunQuery(args, ["command-id", "command"])}`);
|
|
if (action === "cancel" && id) return await agentRunRestRequest("agentrun runs cancel", "POST", `/api/v1/runs/${encodeURIComponent(id)}/cancel`, cancelBodyFromArgs(args));
|
|
throw new AgentRunRestError("validation-failed", `unsupported runs command: ${[action, id].filter(Boolean).join(" ") || "(empty)"}`);
|
|
}
|
|
|
|
export async function runAgentRunCommandsRest(action: string | undefined, id: string | undefined, args: string[]): Promise<Record<string, unknown>> {
|
|
if (action === "create" && id) {
|
|
const body = await optionalJsonBody(args);
|
|
if (!body.type) body.type = agentRunOption(args, "type") ?? "turn";
|
|
const idempotencyKey = agentRunOption(args, "idempotency-key");
|
|
if (idempotencyKey) body.idempotencyKey = idempotencyKey;
|
|
return await agentRunRestRequest("agentrun commands create", "POST", `/api/v1/runs/${encodeURIComponent(id)}/commands`, body);
|
|
}
|
|
if (action === "show" && id) {
|
|
const runId = requiredAgentRunOption(args, ["run-id", "run"], "commands show requires --run-id");
|
|
return await agentRunRestRequest("agentrun commands show", "GET", `/api/v1/runs/${encodeURIComponent(runId)}/commands/${encodeURIComponent(id)}`);
|
|
}
|
|
if (action === "result" && id) {
|
|
const runId = requiredAgentRunOption(args, ["run-id", "run"], "commands result requires --run-id");
|
|
return await agentRunRestRequest("agentrun commands result", "GET", `/api/v1/runs/${encodeURIComponent(runId)}/commands/${encodeURIComponent(id)}/result`);
|
|
}
|
|
if (action === "cancel" && id) return await agentRunRestRequest("agentrun commands cancel", "POST", `/api/v1/commands/${encodeURIComponent(id)}/cancel`, cancelBodyFromArgs(args));
|
|
throw new AgentRunRestError("validation-failed", `unsupported commands command: ${[action, id].filter(Boolean).join(" ") || "(empty)"}`);
|
|
}
|
|
|
|
export async function runAgentRunRunnerRest(action: string | undefined, id: string | undefined, args: string[]): Promise<Record<string, unknown>> {
|
|
if (action === "jobs") {
|
|
const runId = requiredAgentRunOption(args, ["run-id", "run"], "runner jobs requires --run-id");
|
|
return await agentRunRestRequest("agentrun runner jobs", "GET", `/api/v1/runs/${encodeURIComponent(runId)}/runner-jobs${agentRunQuery(args, ["command-id", "command"])}`);
|
|
}
|
|
if (action === "job-status") {
|
|
const runId = requiredAgentRunOption(args, ["run-id", "run"], "runner job-status requires --run-id");
|
|
const runnerJobId = id ?? agentRunOption(args, "runner-job-id");
|
|
if (!runnerJobId) return await agentRunRestRequest("agentrun runner job-status", "GET", `/api/v1/runs/${encodeURIComponent(runId)}/runner-jobs${agentRunQuery(args, ["command-id", "command"])}`);
|
|
return await agentRunRestRequest("agentrun runner job-status", "GET", `/api/v1/runs/${encodeURIComponent(runId)}/runner-jobs/${encodeURIComponent(runnerJobId)}`);
|
|
}
|
|
if (action === "job") {
|
|
const runId = requiredAgentRunOption(args, ["run-id", "run"], "runner job requires --run-id");
|
|
const body = queueDispatchBodyFromArgs(args);
|
|
const commandId = requiredAgentRunOption(args, ["command-id", "command"], "runner job requires --command-id");
|
|
body.commandId = commandId;
|
|
if (agentRunHasFlag(args, "dry-run")) return agentRunDryRunPlan("runner-job", `/api/v1/runs/${encodeURIComponent(runId)}/runner-jobs`, body, `bun scripts/cli.ts agentrun runner job --run-id ${runId} --command-id ${commandId}`);
|
|
return await agentRunRestRequest("agentrun runner job", "POST", `/api/v1/runs/${encodeURIComponent(runId)}/runner-jobs`, body);
|
|
}
|
|
throw new AgentRunRestError("validation-failed", `unsupported runner command: ${[action, id].filter(Boolean).join(" ") || "(empty)"}`);
|
|
}
|
|
|
|
export async function runAgentRunAipodSpecsRest(action: string | undefined, id: string | undefined, args: string[]): Promise<Record<string, unknown>> {
|
|
if (action === "list") return await agentRunRestRequest("agentrun aipod-specs list", "GET", "/api/v1/aipod-specs");
|
|
if (action === "show" && id) return await agentRunRestRequest("agentrun aipod-specs show", "GET", `/api/v1/aipod-specs/${encodeURIComponent(id)}`);
|
|
if (action === "render" && id) return await agentRunRestRequest("agentrun aipod-specs render", "POST", `/api/v1/aipod-specs/${encodeURIComponent(id)}/render`, await aipodRenderInputFromArgs(args, 2));
|
|
if (action === "apply" || action === "set") {
|
|
const yaml = readYamlInputFromArgs(args);
|
|
const pathValue = id ? `/api/v1/aipod-specs/${encodeURIComponent(id)}` : "/api/v1/aipod-specs";
|
|
const method: AgentRunHttpMethod = id ? "PUT" : "POST";
|
|
const body = { yaml };
|
|
if (agentRunHasFlag(args, "dry-run")) return agentRunDryRunPlan("aipod-spec-apply", pathValue, { yamlBytes: Buffer.byteLength(yaml, "utf8") }, `bun scripts/cli.ts agentrun aipod-specs apply${id ? ` ${id}` : ""} --yaml-stdin`, method);
|
|
return await agentRunRestRequest("agentrun aipod-specs apply", method, pathValue, body);
|
|
}
|
|
if ((action === "delete" || action === "rm") && id) return await agentRunRestRequest("agentrun aipod-specs delete", "DELETE", `/api/v1/aipod-specs/${encodeURIComponent(id)}`);
|
|
throw new AgentRunRestError("validation-failed", `unsupported aipod-specs command: ${[action, id].filter(Boolean).join(" ") || "(empty)"}`);
|
|
}
|
|
|
|
export async function submitQueueTaskRest(args: string[]): Promise<Record<string, unknown>> {
|
|
const aipod = agentRunOption(args, "aipod") ?? agentRunOption(args, "aipod-spec");
|
|
if (aipod) {
|
|
const rendered = await agentRunRestRequest("agentrun aipod-specs render", "POST", `/api/v1/aipod-specs/${encodeURIComponent(aipod)}/render`, await aipodRenderInputFromArgs(args, 2));
|
|
const body = normalizeAipodRenderedQueueTask(record(record(innerData(rendered)).queueTask), args, aipod);
|
|
if (Object.keys(body).length === 0) throw new AgentRunRestError("schema-mismatch", `aipod-spec ${aipod} render did not return queueTask`);
|
|
if (agentRunHasFlag(args, "dry-run")) {
|
|
return agentRunDryRunPlan("queue-submit", "/api/v1/queue/tasks", body, queueSubmitConfirmCommand(args, aipod), "POST", {
|
|
jsonInput: { source: "aipod-spec", aipod, valuesPrinted: false },
|
|
aipodBinding: agentRunAipodBindingDisclosure(body, aipod),
|
|
});
|
|
}
|
|
return await agentRunRestRequest("agentrun queue submit", "POST", "/api/v1/queue/tasks", body);
|
|
}
|
|
const body = await requiredJsonBody(args, "queue submit");
|
|
const idempotencyKey = agentRunOption(args, "idempotency-key");
|
|
if (idempotencyKey) body.idempotencyKey = idempotencyKey;
|
|
if (agentRunHasFlag(args, "dry-run")) return agentRunDryRunPlan("queue-submit", "/api/v1/queue/tasks", body, queueSubmitConfirmCommand(args), "POST", { jsonInput: jsonInputDisclosureFromArgs(args) });
|
|
return await agentRunRestRequest("agentrun queue submit", "POST", "/api/v1/queue/tasks", body);
|
|
}
|
|
|
|
export async function mutateQueueTaskRest(action: string, taskId: string, suffix: string, body: Record<string, unknown>, args: string[]): Promise<Record<string, unknown>> {
|
|
const pathValue = `/api/v1/queue/tasks/${encodeURIComponent(taskId)}/${suffix}`;
|
|
if (agentRunHasFlag(args, "dry-run")) return agentRunDryRunPlan(action, pathValue, body, `bun scripts/cli.ts agentrun queue ${suffix} ${taskId}`);
|
|
return await agentRunRestRequest(`agentrun queue ${suffix}`, "POST", pathValue, body);
|
|
}
|
|
|
|
export async function retryQueueTaskRest(taskId: string, args: string[]): Promise<Record<string, unknown>> {
|
|
const idempotencyKey = requiredAgentRunOption(args, ["idempotency-key"], "queue retry requires --idempotency-key");
|
|
const reason = requiredAgentRunOption(args, ["reason"], "queue retry requires --reason");
|
|
return await agentRunRestRequest("agentrun queue retry", "POST", `/api/v1/queue/tasks/${encodeURIComponent(taskId)}/retry`, {
|
|
idempotencyKey,
|
|
reason,
|
|
dryRun: agentRunHasFlag(args, "dry-run"),
|
|
});
|
|
}
|
|
|
|
export async function sessionSendRest(sessionId: string, args: string[]): Promise<Record<string, unknown>> {
|
|
const aipod = agentRunOption(args, "aipod") ?? agentRunOption(args, "aipod-spec");
|
|
if (aipod) return await sessionSendWithAipodRest(sessionId, aipod, args);
|
|
const input = await optionalJsonBody(args);
|
|
const prompt = optionalPromptFromArgs(args, 2);
|
|
const sendBody = await sessionSendBodyFromRunInput(sessionId, args, input, sessionSendPayloadFromInput(input, prompt));
|
|
const response = await agentRunRestRequest("agentrun sessions send", "POST", `/api/v1/sessions/${encodeURIComponent(sessionId)}/send`, sendBody);
|
|
return {
|
|
ok: response.ok !== false,
|
|
command: "agentrun sessions send",
|
|
data: {
|
|
...record(innerData(response)),
|
|
sessionPolicy: agentRunSessionRunPolicyDisclosure(record(sendBody.run)),
|
|
},
|
|
bridge: response.bridge,
|
|
};
|
|
}
|
|
|
|
export function sessionSendPayloadFromInput(input: Record<string, unknown>, prompt: string | null): Record<string, unknown> {
|
|
const payload = record(input.payload);
|
|
if (Object.keys(payload).length > 0) return payload;
|
|
const text = prompt ?? stringOrNull(input.prompt) ?? stringOrNull(input.message) ?? stringOrNull(input.text);
|
|
if (text === null) throw new AgentRunRestError("validation-failed", "send requires payload or non-empty prompt/message/text; use --prompt-stdin, --prompt-file, --prompt, a trailing prompt, or JSON payload");
|
|
return { prompt: text };
|
|
}
|
|
|
|
export async function sessionSendBodyFromRunInput(sessionId: string, args: string[], input: Record<string, unknown>, payload: Record<string, unknown>): Promise<Record<string, unknown>> {
|
|
const fullSendBody = isExplicitSessionSendBody(input);
|
|
const runInput = fullSendBody ? record(input.run ?? input.runBase) : input;
|
|
const runBody = await sessionRunBodyFromArgs(sessionId, args, runInput);
|
|
const runnerDefaults = fullSendBody ? record(input.runnerJob) : {};
|
|
const runnerJob = await sessionRunnerJobBody(args, runnerDefaults);
|
|
const body: Record<string, unknown> = fullSendBody ? { ...input } : {};
|
|
body.run = runBody;
|
|
body.payload = record(input.payload);
|
|
if (Object.keys(record(body.payload)).length === 0) body.payload = payload;
|
|
body.createRunnerJob = agentRunHasFlag(args, "no-runner-job") ? false : input.createRunnerJob !== false;
|
|
body.runnerJob = runnerJob;
|
|
body.dryRun = agentRunHasFlag(args, "dry-run");
|
|
const commandIdempotencyKey = agentRunOption(args, "command-idempotency-key") ?? agentRunOption(args, "idempotency-key");
|
|
if (commandIdempotencyKey) body.commandIdempotencyKey = commandIdempotencyKey;
|
|
return body;
|
|
}
|
|
|
|
export function isExplicitSessionSendBody(input: Record<string, unknown>): boolean {
|
|
return isRecord(input.run)
|
|
|| isRecord(input.runBase)
|
|
|| isRecord(input.payload)
|
|
|| isRecord(input.runnerJob)
|
|
|| input.createRunnerJob !== undefined
|
|
|| input.commandIdempotencyKey !== undefined;
|
|
}
|
|
|
|
export async function sessionRunBodyFromArgs(sessionId: string, args: string[], input: Record<string, unknown>): Promise<Record<string, unknown>> {
|
|
const existing = await fetchAgentRunSessionOrNull(sessionId, args);
|
|
const sessionPolicy = readAgentRunClientConfig().client.sessionPolicy;
|
|
const policyTarget = resolveAgentRunSessionPolicyTarget();
|
|
const profile = agentRunOption(args, "profile")
|
|
?? agentRunOption(args, "backend-profile")
|
|
?? stringOrNull(input.backendProfile)
|
|
?? stringOrNull(existing?.backendProfile)
|
|
?? sessionPolicy.backendProfile;
|
|
const body: Record<string, unknown> = { ...input };
|
|
body.tenantId = agentRunOption(args, "tenant-id") ?? stringOrNull(body.tenantId) ?? stringOrNull(existing?.tenantId) ?? sessionPolicy.tenantId;
|
|
body.projectId = agentRunOption(args, "project-id") ?? stringOrNull(body.projectId) ?? stringOrNull(existing?.projectId) ?? sessionPolicy.projectId;
|
|
body.providerId = agentRunOption(args, "provider-id") ?? stringOrNull(body.providerId) ?? stringOrNull(existing?.providerId) ?? defaultAgentRunProviderId(sessionPolicy, policyTarget);
|
|
body.backendProfile = profile;
|
|
body.workspaceRef = jsonObjectOption(args, "workspace-json") ?? record(body.workspaceRef);
|
|
if (Object.keys(record(body.workspaceRef)).length === 0) body.workspaceRef = cloneJsonRecord(sessionPolicy.workspaceRef);
|
|
const executionPolicy = jsonObjectOption(args, "execution-policy-json") ?? record(body.executionPolicy);
|
|
body.executionPolicy = defaultAgentRunExecutionPolicy(profile, sessionPolicy, policyTarget, {
|
|
basePolicy: Object.keys(executionPolicy).length === 0 ? sessionPolicy.executionPolicy : executionPolicy,
|
|
});
|
|
const inheritedSessionRef = existing === null ? {} : {
|
|
conversationId: existing.conversationId,
|
|
threadId: existing.threadId,
|
|
metadata: existing.metadata,
|
|
};
|
|
const sessionRef = { ...inheritedSessionRef, ...record(body.sessionRef) };
|
|
const metadata = { ...record(inheritedSessionRef.metadata), ...record(sessionRef.metadata) };
|
|
const title = agentRunOption(args, "title");
|
|
if (title) metadata.title = title;
|
|
body.sessionRef = { ...sessionRef, sessionId, metadata };
|
|
return body;
|
|
}
|
|
|
|
export function normalizeAipodRenderedQueueTask(task: Record<string, unknown>, args: string[], aipod: string): Record<string, unknown> {
|
|
if (Object.keys(task).length === 0) return task;
|
|
const sessionPolicy = readAgentRunClientConfig().client.sessionPolicy;
|
|
const policyTarget = resolveAgentRunSessionPolicyTarget();
|
|
const explicitProfile = agentRunOption(args, "profile") ?? agentRunOption(args, "backend-profile");
|
|
const renderedProfile = stringOrNull(task.backendProfile);
|
|
const fallbackProfile = sessionPolicy.backendProfile;
|
|
let backendProfile = explicitProfile ?? renderedProfile ?? fallbackProfile;
|
|
if (agentRunProviderCredentialRefs(policyTarget.spec, backendProfile).length === 0) {
|
|
if (explicitProfile !== null) {
|
|
throw new AgentRunRestError("validation-failed", `${policyTarget.configPath} has no providerCredential Secret binding for explicit --backend-profile ${explicitProfile} on ${policyTarget.spec.nodeId}/${policyTarget.spec.lane}`);
|
|
}
|
|
backendProfile = fallbackProfile;
|
|
}
|
|
if (agentRunProviderCredentialRefs(policyTarget.spec, backendProfile).length === 0) {
|
|
throw new AgentRunRestError("validation-failed", `${policyTarget.configPath} has no providerCredential Secret binding for AipodSpec ${aipod} backendProfile=${backendProfile} on ${policyTarget.spec.nodeId}/${policyTarget.spec.lane}`);
|
|
}
|
|
const basePolicy = record(task.executionPolicy);
|
|
const normalized: Record<string, unknown> = { ...task };
|
|
const explicitProviderId = agentRunOption(args, "provider-id");
|
|
normalized.tenantId = stringOrNull(normalized.tenantId) ?? sessionPolicy.tenantId;
|
|
normalized.projectId = stringOrNull(normalized.projectId) ?? sessionPolicy.projectId;
|
|
normalized.providerId = explicitProviderId ?? defaultAgentRunProviderId(sessionPolicy, policyTarget);
|
|
normalized.backendProfile = backendProfile;
|
|
if (!isRecord(normalized.workspaceRef)) normalized.workspaceRef = cloneJsonRecord(sessionPolicy.workspaceRef);
|
|
normalized.executionPolicy = defaultAgentRunExecutionPolicy(backendProfile, sessionPolicy, policyTarget, {
|
|
includeToolCredentials: true,
|
|
basePolicy: Object.keys(basePolicy).length === 0 ? sessionPolicy.executionPolicy : basePolicy,
|
|
});
|
|
return normalized;
|
|
}
|
|
|
|
export function agentRunAipodBindingDisclosure(task: Record<string, unknown>, aipod: string): Record<string, unknown> {
|
|
const policyTarget = resolveAgentRunSessionPolicyTarget();
|
|
const executionPolicy = record(task.executionPolicy);
|
|
const secretScope = record(executionPolicy.secretScope);
|
|
const providerCredentials = arrayRecords(secretScope.providerCredentials).map((credential) => ({
|
|
profile: credential.profile ?? null,
|
|
secretRef: {
|
|
name: record(credential.secretRef).name ?? null,
|
|
keys: Array.isArray(record(credential.secretRef).keys) ? record(credential.secretRef).keys : [],
|
|
},
|
|
valuesPrinted: false,
|
|
}));
|
|
const toolCredentials = arrayRecords(secretScope.toolCredentials).map((credential) => ({
|
|
tool: credential.tool ?? null,
|
|
purpose: credential.purpose ?? null,
|
|
secretRef: {
|
|
name: record(credential.secretRef).name ?? null,
|
|
keys: Array.isArray(record(credential.secretRef).keys) ? record(credential.secretRef).keys : [],
|
|
},
|
|
projection: record(credential.projection),
|
|
valuesPrinted: false,
|
|
}));
|
|
return {
|
|
aipod,
|
|
node: policyTarget.spec.nodeId,
|
|
lane: policyTarget.spec.lane,
|
|
namespace: policyTarget.spec.runtime.namespace,
|
|
policySource: policyTarget.source,
|
|
providerId: task.providerId ?? null,
|
|
backendProfile: task.backendProfile ?? null,
|
|
workspaceRef: task.workspaceRef ?? null,
|
|
executionPolicy: pickCompact(executionPolicy, ["sandbox", "approval", "timeoutMs", "network"]),
|
|
providerCredentials,
|
|
toolCredentials,
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
|
|
export function agentRunSessionRunPolicyDisclosure(runBody: Record<string, unknown>): Record<string, unknown> {
|
|
const policyTarget = resolveAgentRunSessionPolicyTarget();
|
|
const executionPolicy = record(runBody.executionPolicy);
|
|
const secretScope = record(executionPolicy.secretScope);
|
|
const providerCredentials = arrayRecords(secretScope.providerCredentials).map((credential) => ({
|
|
profile: credential.profile ?? null,
|
|
secretRef: {
|
|
name: record(credential.secretRef).name ?? null,
|
|
keys: Array.isArray(record(credential.secretRef).keys) ? record(credential.secretRef).keys : [],
|
|
},
|
|
valuesPrinted: false,
|
|
}));
|
|
return {
|
|
node: policyTarget.spec.nodeId,
|
|
lane: policyTarget.spec.lane,
|
|
namespace: policyTarget.spec.runtime.namespace,
|
|
policySource: policyTarget.source,
|
|
providerId: runBody.providerId ?? null,
|
|
backendProfile: runBody.backendProfile ?? null,
|
|
workspaceRef: runBody.workspaceRef ?? null,
|
|
executionPolicy: pickCompact(executionPolicy, ["sandbox", "approval", "timeoutMs", "network"]),
|
|
providerCredentials,
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
|
|
export async function sessionSendWithAipodRest(sessionId: string, aipod: string, args: string[]): Promise<Record<string, unknown>> {
|
|
const rendered = await agentRunRestRequest("agentrun aipod-specs render", "POST", `/api/v1/aipod-specs/${encodeURIComponent(aipod)}/render`, await aipodRenderInputFromArgs(args, 2, { sessionId }));
|
|
const renderedData = record(innerData(rendered));
|
|
const task = normalizeAipodRenderedQueueTask(record(renderedData.queueTask), args, aipod);
|
|
if (Object.keys(task).length === 0) throw new AgentRunRestError("schema-mismatch", `aipod-spec ${aipod} render did not return queueTask`);
|
|
const sessionRef = record(task.sessionRef);
|
|
const metadata = record(sessionRef.metadata);
|
|
const title = agentRunOption(args, "title") ?? stringOrNull(task.title);
|
|
if (title) metadata.title = title;
|
|
const sessionPolicy = readAgentRunClientConfig().client.sessionPolicy;
|
|
const policyTarget = resolveAgentRunSessionPolicyTarget();
|
|
const backendProfile = stringOrNull(task.backendProfile) ?? sessionPolicy.backendProfile;
|
|
const executionPolicy = record(task.executionPolicy);
|
|
const runBody: Record<string, unknown> = {
|
|
tenantId: stringOrNull(task.tenantId) ?? sessionPolicy.tenantId,
|
|
projectId: stringOrNull(task.projectId) ?? sessionPolicy.projectId,
|
|
providerId: stringOrNull(task.providerId) ?? defaultAgentRunProviderId(sessionPolicy, policyTarget),
|
|
backendProfile,
|
|
workspaceRef: task.workspaceRef ?? cloneJsonRecord(sessionPolicy.workspaceRef),
|
|
sessionRef: { ...sessionRef, sessionId, metadata },
|
|
executionPolicy: Object.keys(executionPolicy).length === 0
|
|
? defaultAgentRunExecutionPolicy(backendProfile, sessionPolicy, policyTarget, { includeToolCredentials: true })
|
|
: defaultAgentRunExecutionPolicy(backendProfile, sessionPolicy, policyTarget, { includeToolCredentials: true, basePolicy: executionPolicy }),
|
|
resourceBundleRef: task.resourceBundleRef,
|
|
traceSink: { kind: "aipod-session", aipod, sessionId, valuesPrinted: false },
|
|
};
|
|
const runnerDefaults = record(record(renderedData.dispatchDefaults).runnerJob);
|
|
const sendBody: Record<string, unknown> = {
|
|
run: runBody,
|
|
payload: task.payload,
|
|
createRunnerJob: !agentRunHasFlag(args, "no-runner-job"),
|
|
runnerJob: await sessionRunnerJobBody(args, runnerDefaults),
|
|
dryRun: agentRunHasFlag(args, "dry-run"),
|
|
};
|
|
const commandIdempotencyKey = agentRunOption(args, "command-idempotency-key") ?? agentRunOption(args, "idempotency-key");
|
|
if (commandIdempotencyKey) sendBody.commandIdempotencyKey = commandIdempotencyKey;
|
|
const response = await agentRunRestRequest("agentrun sessions send", "POST", `/api/v1/sessions/${encodeURIComponent(sessionId)}/send`, sendBody);
|
|
const data = record(innerData(response));
|
|
return {
|
|
ok: response.ok !== false,
|
|
command: "agentrun sessions send",
|
|
data: {
|
|
...data,
|
|
aipod,
|
|
profile: String(task.backendProfile ?? ""),
|
|
sessionPolicy: agentRunSessionRunPolicyDisclosure(runBody),
|
|
aipodBinding: agentRunAipodBindingDisclosure(task, aipod),
|
|
valuesPrinted: false,
|
|
},
|
|
bridge: response.bridge,
|
|
};
|
|
}
|
|
|
|
export async function fetchAgentRunSessionOrNull(sessionId: string, args: string[]): Promise<Record<string, unknown> | null> {
|
|
try {
|
|
return record(innerData(await agentRunRestRequest("agentrun sessions show", "GET", `/api/v1/sessions/${encodeURIComponent(sessionId)}${agentRunQuery(args, ["reader-id"])}`)));
|
|
} catch (error) {
|
|
if (error instanceof AgentRunRestError && error.httpStatus === 404) return null;
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function sessionRunnerJobBody(args: string[], defaults: Record<string, unknown> = {}): Promise<Record<string, unknown>> {
|
|
const runnerBody = { ...defaults, ...(await optionalRunnerJsonBody(args)) };
|
|
copyAgentRunOptions(args, runnerBody, ["image", "namespace", "attempt-id", "runner-id", "source-commit", "service-account-name"]);
|
|
const managerUrl = agentRunOption(args, "runner-manager-url");
|
|
if (managerUrl !== null) runnerBody.managerUrl = managerUrl;
|
|
const runnerIdempotencyKey = agentRunOption(args, "runner-idempotency-key");
|
|
if (runnerIdempotencyKey !== null) runnerBody.idempotencyKey = runnerIdempotencyKey;
|
|
return runnerBody;
|
|
}
|
|
|
|
export async function agentRunRestRequest(command: string, method: AgentRunHttpMethod, pathValue: string, body?: unknown): Promise<Record<string, unknown>> {
|
|
const clientConfig = readAgentRunClientConfig();
|
|
if (activeAgentRunRestTarget !== null) return await agentRunLaneRestRequest(command, method, pathValue, body, clientConfig, activeAgentRunRestTarget);
|
|
const auth = resolveAgentRunAuth(clientConfig);
|
|
const bridgeBase = agentRunRestBridgeMetadata(clientConfig, auth, method, pathValue);
|
|
const startedAt = Date.now();
|
|
const controller = new AbortController();
|
|
const timeout = setTimeout(() => controller.abort(), clientConfig.manager.timeoutMs);
|
|
let response: Response;
|
|
try {
|
|
const headers: Record<string, string> = {
|
|
[clientConfig.auth.header]: `${clientConfig.auth.scheme} ${auth.value}`,
|
|
};
|
|
const init: RequestInit = { method, headers, signal: controller.signal };
|
|
if (body !== undefined) {
|
|
headers["content-type"] = "application/json";
|
|
init.body = JSON.stringify(body);
|
|
}
|
|
response = await fetch(new URL(pathValue, clientConfig.manager.baseUrl), init);
|
|
} catch (error) {
|
|
const timedOut = isAbortLikeError(error);
|
|
throw new AgentRunRestError(timedOut ? "agentrun-timeout" : "agentrun-connect-failed", timedOut ? `AgentRun server request timed out for ${method} ${pathValue} after ${clientConfig.manager.timeoutMs}ms` : `AgentRun server connection failed for ${method} ${pathValue}: ${error instanceof Error ? error.message : String(error)}`, { bridge: { ...bridgeBase, elapsedMs: Date.now() - startedAt } });
|
|
} finally {
|
|
clearTimeout(timeout);
|
|
}
|
|
const text = await response.text();
|
|
const bridge = { ...bridgeBase, httpStatus: response.status, elapsedMs: Date.now() - startedAt };
|
|
if (text.trim().length === 0) throw new AgentRunRestError("schema-mismatch", `AgentRun server returned an empty response for ${method} ${pathValue}`, { bridge, httpStatus: response.status });
|
|
let envelope: Record<string, unknown>;
|
|
try {
|
|
envelope = record(JSON.parse(text) as unknown);
|
|
} catch {
|
|
throw new AgentRunRestError("schema-mismatch", `AgentRun server returned non-JSON response for ${method} ${pathValue}`, { bridge, httpStatus: response.status });
|
|
}
|
|
if (response.status === 401 || response.status === 403) throw new AgentRunRestError("auth-failed", stringOrNull(envelope.message) ?? "AgentRun API key was rejected", { bridge, httpStatus: response.status, details: safeAgentRunEnvelope(envelope) });
|
|
if (!response.ok) {
|
|
const details = response.status === 404
|
|
? addAgentRunNotFoundLookupHint(safeAgentRunEnvelope(envelope), clientConfig, method, pathValue, null)
|
|
: safeAgentRunEnvelope(envelope);
|
|
throw new AgentRunRestError(response.status === 404 ? "not-found" : "validation-failed", stringOrNull(envelope.message) ?? `AgentRun request failed with HTTP ${response.status}`, { bridge, httpStatus: response.status, details });
|
|
}
|
|
if (envelope.ok !== true) {
|
|
const failureKind = normalizeAgentRunFailureKind(stringOrNull(envelope.failureKind), response.status);
|
|
throw new AgentRunRestError(failureKind, stringOrNull(envelope.message) ?? `AgentRun request failed for ${method} ${pathValue}`, { bridge, httpStatus: response.status, details: safeAgentRunEnvelope(envelope) });
|
|
}
|
|
return {
|
|
ok: true,
|
|
command,
|
|
data: envelope.data ?? null,
|
|
bridge,
|
|
};
|
|
}
|
|
|
|
export function isAbortLikeError(error: unknown): boolean {
|
|
if (!(error instanceof Error)) return false;
|
|
return error.name === "AbortError" || /abort|timed out|timeout/iu.test(error.message);
|
|
}
|
|
|
|
export async function agentRunLaneRestRequest(command: string, method: AgentRunHttpMethod, pathValue: string, body: unknown, clientConfig: AgentRunClientConfig, target: AgentRunRestTarget): Promise<Record<string, unknown>> {
|
|
const bridgeBase = agentRunLaneRestBridgeMetadata(clientConfig, target, method, pathValue);
|
|
const startedAt = Date.now();
|
|
const proxyBaseUrl = `http://127.0.0.1:${target.spec.runtime.managerPort}`;
|
|
const request = {
|
|
method,
|
|
path: pathValue,
|
|
body: body ?? null,
|
|
baseUrl: proxyBaseUrl,
|
|
serviceBaseUrl: target.spec.runtime.internalBaseUrl,
|
|
timeoutMs: clientConfig.manager.timeoutMs,
|
|
authEnv: clientConfig.auth.env,
|
|
header: clientConfig.auth.header,
|
|
scheme: clientConfig.auth.scheme,
|
|
valuesPrinted: false,
|
|
};
|
|
const captureResult = await capture(target.config, target.spec.nodeKubeRoute, ["sh", "--", agentRunLaneRestProxyScript(target.spec, request)]);
|
|
const captureSummary = compactCapture(captureResult, { full: false, stdoutTailChars: 1200, stderrTailChars: 2000 });
|
|
const bridge = { ...bridgeBase, elapsedMs: Date.now() - startedAt, capture: captureSummary };
|
|
if (captureResult.exitCode !== 0) {
|
|
throw new AgentRunRestError("agentrun-proxy-exec-failed", `AgentRun lane ${target.spec.nodeId}/${target.spec.lane} manager proxy execution failed for ${method} ${pathValue}`, { bridge });
|
|
}
|
|
const proxy = captureJsonPayload(captureResult);
|
|
if (proxy.ok !== true) {
|
|
const proxyFailureKind = stringOrNull(proxy.failureKind);
|
|
const failureKind = classifyAgentRunProxyFailureKind(proxyFailureKind);
|
|
throw new AgentRunRestError(failureKind, stringOrNull(proxy.message) ?? `AgentRun lane ${target.spec.nodeId}/${target.spec.lane} manager proxy failed`, { bridge: { ...bridge, proxy }, details: pickCompact(proxy, ["ok", "failureKind", "message", "elapsedMs", "path", "baseUrl", "valuesPrinted"]) });
|
|
}
|
|
const httpStatus = nonNegativeIntegerOrNull(proxy.httpStatus) ?? 0;
|
|
const text = stringOrNull(proxy.text) ?? "";
|
|
const responseBridge = { ...bridge, httpStatus, proxyElapsedMs: nonNegativeIntegerOrNull(proxy.elapsedMs) };
|
|
if (text.trim().length === 0) throw new AgentRunRestError("schema-mismatch", `AgentRun server returned an empty response for ${method} ${pathValue}`, { bridge: responseBridge, httpStatus });
|
|
let envelope: Record<string, unknown>;
|
|
try {
|
|
envelope = record(JSON.parse(text) as unknown);
|
|
} catch {
|
|
throw new AgentRunRestError("schema-mismatch", `AgentRun server returned non-JSON response for ${method} ${pathValue}`, { bridge: responseBridge, httpStatus });
|
|
}
|
|
if (httpStatus === 401 || httpStatus === 403) throw new AgentRunRestError("auth-failed", stringOrNull(envelope.message) ?? "AgentRun API key was rejected", { bridge: responseBridge, httpStatus, details: safeAgentRunEnvelope(envelope) });
|
|
if (httpStatus < 200 || httpStatus >= 300) {
|
|
const details = httpStatus === 404
|
|
? addAgentRunNotFoundLookupHint(safeAgentRunEnvelope(envelope), clientConfig, method, pathValue, target)
|
|
: safeAgentRunEnvelope(envelope);
|
|
throw new AgentRunRestError(httpStatus === 404 ? "not-found" : "validation-failed", stringOrNull(envelope.message) ?? `AgentRun request failed with HTTP ${httpStatus}`, { bridge: responseBridge, httpStatus, details });
|
|
}
|
|
if (envelope.ok !== true) {
|
|
const failureKind = normalizeAgentRunFailureKind(stringOrNull(envelope.failureKind), httpStatus);
|
|
throw new AgentRunRestError(failureKind, stringOrNull(envelope.message) ?? `AgentRun request failed for ${method} ${pathValue}`, { bridge: responseBridge, httpStatus, details: safeAgentRunEnvelope(envelope) });
|
|
}
|
|
return {
|
|
ok: true,
|
|
command,
|
|
data: envelope.data ?? null,
|
|
bridge: responseBridge,
|
|
};
|
|
}
|
|
|
|
export function agentRunLaneRestProxyScript(spec: AgentRunLaneSpec, request: Record<string, unknown>): string {
|
|
const requestB64 = Buffer.from(JSON.stringify(request), "utf8").toString("base64");
|
|
const evalScript = String.raw`
|
|
const startedAt = Date.now();
|
|
let input = {};
|
|
let method = "GET";
|
|
let path = "/";
|
|
let baseUrl = "http://127.0.0.1:8080";
|
|
let timeoutMs = 15000;
|
|
try {
|
|
input = JSON.parse(Buffer.from(process.env.AGENTRUN_LANE_REST_REQUEST_B64 || "", "base64").toString("utf8"));
|
|
method = String(input.method || "GET");
|
|
path = String(input.path || "/");
|
|
baseUrl = String(input.baseUrl || "http://127.0.0.1:8080");
|
|
timeoutMs = Number(input.timeoutMs || 15000);
|
|
const headers = {};
|
|
const authEnv = String(input.authEnv || "HWLAB_API_KEY");
|
|
const apiKey = process.env[authEnv] || process.env.AGENTRUN_API_KEY || process.env.HWLAB_API_KEY || "";
|
|
if (apiKey && input.header && input.scheme) headers[String(input.header)] = String(input.scheme) + " " + apiKey;
|
|
let body = undefined;
|
|
if (input.body !== null && input.body !== undefined) {
|
|
headers["content-type"] = "application/json";
|
|
body = JSON.stringify(input.body);
|
|
}
|
|
const controller = new AbortController();
|
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
let response;
|
|
try {
|
|
response = await fetch(new URL(path, baseUrl).toString(), {
|
|
method,
|
|
headers,
|
|
body,
|
|
signal: controller.signal,
|
|
});
|
|
} finally {
|
|
clearTimeout(timeout);
|
|
}
|
|
const text = await response.text();
|
|
console.log(JSON.stringify({ ok: true, httpStatus: response.status, statusText: response.statusText, text, elapsedMs: Date.now() - startedAt, valuesPrinted: false }));
|
|
} catch (error) {
|
|
const name = error instanceof Error ? error.name : "";
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
const timedOut = name === "AbortError" || /abort|timed out|timeout/iu.test(message);
|
|
console.log(JSON.stringify({ ok: false, failureKind: timedOut ? "manager-pod-fetch-timeout" : "manager-pod-fetch-failed", message: timedOut ? "AgentRun manager request timed out for " + method + " " + path + " after " + timeoutMs + "ms" : message, elapsedMs: Date.now() - startedAt, path, baseUrl, valuesPrinted: false }));
|
|
}
|
|
`;
|
|
return [
|
|
"set -eu",
|
|
`namespace=${shQuote(spec.runtime.namespace)}`,
|
|
`deployment=${shQuote(spec.runtime.managerDeployment)}`,
|
|
`request_b64=${shQuote(requestB64)}`,
|
|
`kubectl -n "$namespace" exec deploy/"$deployment" -- env AGENTRUN_LANE_REST_REQUEST_B64="$request_b64" bun --eval ${shQuote(evalScript)}`,
|
|
].join("\n");
|
|
}
|
|
|
|
export function resolveAgentRunRestTarget(config: UniDeskConfig | null, options: AgentRunRestTargetOptions): AgentRunRestTarget | null {
|
|
if (config === null) {
|
|
if (options.node === null && options.lane === null) return null;
|
|
throw new AgentRunRestError("validation-failed", "--node/--lane resource queries require UniDesk config");
|
|
}
|
|
const { configPath, spec } = resolveAgentRunLaneTarget(options);
|
|
return { config, configPath, spec };
|
|
}
|
|
|
|
export async function withAgentRunRestTarget<T>(target: AgentRunRestTarget | null, action: () => Promise<T>): Promise<T> {
|
|
const previous = activeAgentRunRestTarget;
|
|
activeAgentRunRestTarget = target;
|
|
try {
|
|
return await action();
|
|
} finally {
|
|
activeAgentRunRestTarget = previous;
|
|
}
|
|
}
|
|
|
|
export function renderAgentRunRestError(command: string, error: AgentRunRestError, options: AgentRunResourceOptions): RenderedCliResult {
|
|
const rawPayload = error.toPayload(command);
|
|
if (options.raw) return renderMachine(command, rawPayload, "json", false);
|
|
const payload = boundedAgentRunRestErrorPayload(rawPayload);
|
|
if (options.output === "json" || options.output === "yaml") return renderMachine(command, payload, options.output === "yaml" ? "yaml" : "json", false);
|
|
const laneHint = record(record(payload.agentrun).laneAwareLookup);
|
|
const currentEndpoint = record(laneHint.currentEndpoint);
|
|
const nextCommands = Array.isArray(laneHint.nextCommands)
|
|
? laneHint.nextCommands.filter((value): value is string => typeof value === "string" && value.length > 0).slice(0, 4)
|
|
: [];
|
|
const validationDetails = record(payload.agentrun);
|
|
const recoveryActions = Array.isArray(validationDetails.recoveryActions)
|
|
? validationDetails.recoveryActions.filter((value): value is string => typeof value === "string" && value.length > 0).slice(0, 4)
|
|
: [];
|
|
const hintLines = Object.keys(laneHint).length === 0
|
|
? []
|
|
: [
|
|
"",
|
|
"Lane lookup hint:",
|
|
` Current baseUrl: ${String(currentEndpoint.baseUrl ?? "(unknown)")}`,
|
|
` Config: ${String(currentEndpoint.configPath ?? "(unknown)")}`,
|
|
` ${String(laneHint.summary ?? "The resource was not found on the currently configured AgentRun manager endpoint.")}`,
|
|
];
|
|
const validationHelp = `bun scripts/cli.ts agentrun ${command.split(/\s+/u)[1] ?? "--help"} --help`;
|
|
const nextLines = nextCommands.length > 0
|
|
? nextCommands
|
|
: recoveryActions.length > 0
|
|
? recoveryActions
|
|
: error.failureKind === "validation-failed"
|
|
? [
|
|
"Review the AgentRun validation message and correct the request; do not bypass the formal resource API.",
|
|
validationHelp,
|
|
]
|
|
: [
|
|
"Check config/agentrun.yaml manager.baseUrl and the AgentRun API route.",
|
|
"Verify HWLAB_API_KEY is present in env or in the configured auth.file without printing the key.",
|
|
];
|
|
return renderedCliResult(false, command, [
|
|
`Error: ${payload.failureKind}`,
|
|
String(payload.message ?? ""),
|
|
...hintLines,
|
|
"",
|
|
"Next:",
|
|
...nextLines.map((line) => ` ${line}`),
|
|
].join("\n"));
|
|
}
|
|
|
|
export function boundedAgentRunRestErrorPayload(payload: Record<string, unknown>): Record<string, unknown> {
|
|
return boundedAgentRunRestErrorValue(payload, 0) as Record<string, unknown>;
|
|
}
|
|
|
|
function boundedAgentRunRestErrorValue(value: unknown, depth: number): unknown {
|
|
if (typeof value === "string") return truncateOneLine(value, 400);
|
|
if (value === null || typeof value === "number" || typeof value === "boolean") return value;
|
|
if (Array.isArray(value)) return value.slice(0, 12).map((item) => boundedAgentRunRestErrorValue(item, depth + 1));
|
|
if (typeof value !== "object") return String(value);
|
|
if (depth >= 5) return "[nested details omitted]";
|
|
const entries = Object.entries(value as Record<string, unknown>);
|
|
const result: Record<string, unknown> = {};
|
|
for (const [key, item] of entries.slice(0, 20)) result[key] = boundedAgentRunRestErrorValue(item, depth + 1);
|
|
if (entries.length > 20) result.omittedFieldCount = entries.length - 20;
|
|
return result;
|
|
}
|
|
|
|
export class AgentRunRestError extends Error {
|
|
readonly failureKind: AgentRunFailureKind;
|
|
readonly bridge: Record<string, unknown> | null;
|
|
readonly httpStatus: number | null;
|
|
readonly details: Record<string, unknown> | null;
|
|
|
|
constructor(failureKind: AgentRunFailureKind, message: string, options: { bridge?: Record<string, unknown>; httpStatus?: number; details?: Record<string, unknown> } = {}) {
|
|
super(message);
|
|
this.name = "AgentRunRestError";
|
|
this.failureKind = failureKind;
|
|
this.bridge = options.bridge ?? null;
|
|
this.httpStatus = options.httpStatus ?? null;
|
|
this.details = options.details ?? null;
|
|
}
|
|
|
|
toPayload(command: string): Record<string, unknown> {
|
|
const transport = stringOrNull(this.bridge?.mode) ?? "direct-http";
|
|
return {
|
|
ok: false,
|
|
command,
|
|
failureKind: this.failureKind,
|
|
message: this.message,
|
|
transport,
|
|
clientRole: "render-only",
|
|
...(this.httpStatus === null ? {} : { httpStatus: this.httpStatus }),
|
|
...(this.bridge === null ? {} : { bridge: this.bridge }),
|
|
...(this.details === null ? {} : { agentrun: this.details }),
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
}
|
|
|
|
export let activeAgentRunRestTarget: AgentRunRestTarget | null = null;
|