fix: bind AgentRun lane policy and bounded output
This commit is contained in:
+415
-40
@@ -1,5 +1,5 @@
|
||||
// SPEC: PJ2026-01020108 cancel lifecycle + PJ2026-01020305 cancel control + PJ2026-01060305 AgentRun execution policy draft-2026-06-25-p0.
|
||||
// Exposes AgentRun cancel lifecycle policy and dry-run visibility in the UniDesk CLI.
|
||||
// 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";
|
||||
@@ -97,7 +97,11 @@ export async function runAgentRunCommand(config: UniDeskConfig | null, args: str
|
||||
}
|
||||
if (config === null) throw new Error("agentrun control-plane and git-mirror commands require UniDesk config");
|
||||
if (group === "control-plane") {
|
||||
if (action === "plan") return await controlPlanePlan(config, parseStatusOptions(actionArgs));
|
||||
if (action === "plan") {
|
||||
const options = parseStatusOptions(actionArgs);
|
||||
const result = await controlPlanePlan(config, options);
|
||||
return options.full || options.raw ? result : renderAgentRunControlPlanePlanSummary(result);
|
||||
}
|
||||
if (action === "apply") return await controlPlaneApply(config, parseLaneConfirmOptions(actionArgs));
|
||||
if (action === "status") {
|
||||
const options = parseStatusOptions(actionArgs);
|
||||
@@ -108,8 +112,16 @@ export async function runAgentRunCommand(config: UniDeskConfig | null, args: str
|
||||
if (action === "restart") return await restartYamlLane(config, parseLaneConfirmOptions(actionArgs));
|
||||
if (action === "expose") return await exposeAgentRun(config, parseConfirmOptions(actionArgs));
|
||||
if (action === "trigger-current") return await triggerCurrent(config, parseTriggerOptions(actionArgs));
|
||||
if (action === "refresh") return await refresh(config, parseRefreshOptions(actionArgs));
|
||||
if (action === "cleanup-runners") return await cleanupRunners(config, parseCleanupRunnersOptions(actionArgs));
|
||||
if (action === "refresh") {
|
||||
const options = parseRefreshOptions(actionArgs);
|
||||
const result = await refresh(config, options);
|
||||
return options.full || options.raw ? result : renderAgentRunControlPlaneActionSummary(result, "AGENTRUN CONTROL-PLANE REFRESH");
|
||||
}
|
||||
if (action === "cleanup-runners") {
|
||||
const options = parseCleanupRunnersOptions(actionArgs);
|
||||
const result = await cleanupRunners(config, options);
|
||||
return options.full || options.raw ? result : renderAgentRunControlPlaneActionSummary(result, "AGENTRUN RUNNER CLEANUP");
|
||||
}
|
||||
if (action === "cleanup-runs") return await cleanupRuns(config, parseCleanupRunsOptions(actionArgs));
|
||||
if (action === "cleanup-released-pvs") return await cleanupReleasedPvs(config, parseCleanupReleasedPvOptions(actionArgs));
|
||||
}
|
||||
@@ -309,13 +321,13 @@ function agentRunGetKindHelp(kindRaw: string): string {
|
||||
|
||||
async function runAgentRunResourceCommand(config: UniDeskConfig | null, verb: AgentRunResourceVerb, action: string | undefined, actionArgs: string[], canonicalArgs: string[]): Promise<RenderedCliResult> {
|
||||
if (isHelpArg(action) || actionArgs.some(isHelpArg)) return renderAgentRunHelp(canonicalArgs);
|
||||
if (verb === "explain") return renderedCliResult(true, "agentrun explain", agentRunExplain(action ?? "task"));
|
||||
const resourceArgs = action === undefined ? actionArgs : [action, ...actionArgs];
|
||||
const options = parseResourceOptions(resourceArgs);
|
||||
const bridgeActionArgs = stripAgentRunResourceWrapperArgs(actionArgs);
|
||||
const command = `agentrun ${canonicalArgs.join(" ")}`.trim();
|
||||
try {
|
||||
return await withAgentRunRestTarget(resolveAgentRunRestTarget(config, options), async () => {
|
||||
if (verb === "explain") return renderedCliResult(true, command, agentRunExplain(action ?? "task", resourceArgs, options));
|
||||
if (verb === "get") return await resourceGet(config, command, action, bridgeActionArgs, options);
|
||||
if (verb === "describe") return await resourceDescribe(config, command, action, bridgeActionArgs, options);
|
||||
if (verb === "events") return await resourceEvents(config, command, action, bridgeActionArgs, options);
|
||||
@@ -501,7 +513,10 @@ async function resourceDescribe(config: UniDeskConfig | null, command: string, a
|
||||
const data = record(innerData(result));
|
||||
const task = unwrapTaskDetail(data);
|
||||
if (options.raw) return renderMachine(command, result, "json", result.ok !== false);
|
||||
if (options.output === "json" || options.output === "yaml") return renderMachine(command, { kind: ref.kind, name: ref.name, resource: task }, options.output, result.ok !== false);
|
||||
if (options.output === "json" || options.output === "yaml") {
|
||||
const payload = options.full ? { kind: ref.kind, name: ref.name, resource: task } : compactTaskDescriptionPayload(ref, task);
|
||||
return renderMachine(command, payload, options.output, result.ok !== false);
|
||||
}
|
||||
return renderedCliResult(result.ok !== false, command, renderTaskDescription(task, options));
|
||||
}
|
||||
if (ref.kind === "run") {
|
||||
@@ -755,7 +770,7 @@ async function resourceCreate(config: UniDeskConfig | null, command: string, act
|
||||
if (kind !== "task") throw new Error("create currently supports: create task");
|
||||
const submitArgs = ["submit", ...stripLeadingResource(args, "task")];
|
||||
const result = await runAgentRunRestCommand(config, "queue", submitArgs);
|
||||
return renderMutationSummary(command, result, options, "Task create submitted", options.dryRun ? [rerunWithoutDryRun(command)] : undefined);
|
||||
return renderMutationSummary(command, result, options, options.dryRun ? "Task create plan" : "Task create submitted", options.dryRun ? [rerunWithoutDryRun(command)] : undefined);
|
||||
}
|
||||
|
||||
async function resourceApply(config: UniDeskConfig | null, command: string, args: string[], options: AgentRunResourceOptions): Promise<RenderedCliResult> {
|
||||
@@ -1515,6 +1530,46 @@ function renderTaskDescription(task: Record<string, unknown>, options: AgentRunR
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function compactTaskDescriptionPayload(ref: AgentRunResourceRef, task: Record<string, unknown>): Record<string, unknown> {
|
||||
const attempt = record(task.latestAttempt);
|
||||
const taskId = stringOrNull(task.id) ?? ref.name;
|
||||
const runId = stringOrNull(attempt.runId);
|
||||
const commandId = stringOrNull(attempt.commandId);
|
||||
const runnerJobId = stringOrNull(attempt.runnerJobId);
|
||||
const sessionId = stringOrNull(attempt.sessionId) ?? stringOrNull(record(task.sessionRef).sessionId);
|
||||
return {
|
||||
kind: ref.kind,
|
||||
name: ref.name,
|
||||
summary: {
|
||||
id: taskId,
|
||||
state: task.state ?? null,
|
||||
queue: task.queue ?? null,
|
||||
lane: task.lane ?? null,
|
||||
backendProfile: task.backendProfile ?? null,
|
||||
providerId: task.providerId ?? null,
|
||||
title: task.title ?? null,
|
||||
unread: task.unread === true,
|
||||
failureKind: task.failureKind ?? task.degradedReason ?? null,
|
||||
},
|
||||
latestAttempt: {
|
||||
attemptId: attempt.attemptId ?? null,
|
||||
state: attempt.state ?? null,
|
||||
runId,
|
||||
commandId,
|
||||
runnerJobId,
|
||||
sessionId,
|
||||
},
|
||||
next: {
|
||||
events: runId === null ? null : `bun scripts/cli.ts agentrun events run/${runId} --after-seq 0 --limit 100`,
|
||||
logs: sessionId === null ? null : `bun scripts/cli.ts agentrun logs session/${sessionId} --tail 100`,
|
||||
result: runId === null || commandId === null ? null : `bun scripts/cli.ts agentrun result run/${runId} --command ${commandId}`,
|
||||
ack: `bun scripts/cli.ts agentrun ack task/${taskId}`,
|
||||
full: `bun scripts/cli.ts agentrun describe task/${taskId} --full -o json`,
|
||||
},
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
function unwrapTaskDetail(data: Record<string, unknown>): Record<string, unknown> {
|
||||
const task = record(data.task);
|
||||
if (Object.keys(task).length > 0) return task;
|
||||
@@ -1682,8 +1737,8 @@ function nextPagedResourceCommand(command: string, nextAfterSeq: string, limit:
|
||||
return `${parts} --after-seq ${nextAfterSeq} --limit ${limit}`;
|
||||
}
|
||||
|
||||
function agentRunExplain(kindRaw: string): string {
|
||||
if (kindRaw === "session-policy" || kindRaw === "provider-policy") return renderAgentRunSessionPolicyExplanation();
|
||||
function agentRunExplain(kindRaw: string, args: string[] = [], options: AgentRunRestTargetOptions = { node: null, lane: null }): string {
|
||||
if (kindRaw === "session-policy" || kindRaw === "provider-policy") return renderAgentRunSessionPolicyExplanation(args, options);
|
||||
const kind = parseResourceKind(kindRaw);
|
||||
if (kind === "task") {
|
||||
return [
|
||||
@@ -1798,7 +1853,7 @@ interface LaneConfirmOptions extends ConfirmOptions {
|
||||
lane: string | null;
|
||||
}
|
||||
|
||||
interface RefreshOptions extends ConfirmOptions {
|
||||
interface RefreshOptions extends ConfirmOptions, DisclosureOptions {
|
||||
node: string | null;
|
||||
lane: string | null;
|
||||
}
|
||||
@@ -1815,7 +1870,7 @@ interface GitMirrorOptions extends ConfirmOptions {
|
||||
wait: boolean;
|
||||
}
|
||||
|
||||
interface CleanupRunnersOptions extends ConfirmOptions {
|
||||
interface CleanupRunnersOptions extends ConfirmOptions, DisclosureOptions {
|
||||
node: string | null;
|
||||
lane: string | null;
|
||||
timeoutSeconds: number;
|
||||
@@ -2032,9 +2087,20 @@ function parseRefreshOptions(args: string[]): RefreshOptions {
|
||||
const base = parseConfirmOptions(args);
|
||||
let node: string | null = null;
|
||||
let lane: string | null = null;
|
||||
let full = false;
|
||||
let raw = false;
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index];
|
||||
if (arg === "--confirm" || arg === "--dry-run") continue;
|
||||
if (arg === "--full") {
|
||||
full = true;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--raw") {
|
||||
raw = true;
|
||||
full = true;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--node") {
|
||||
const value = args[index + 1];
|
||||
if (value === undefined || value.startsWith("--")) throw new Error("--node requires a value");
|
||||
@@ -2051,7 +2117,7 @@ function parseRefreshOptions(args: string[]): RefreshOptions {
|
||||
}
|
||||
throw new Error(`unsupported refresh option: ${arg}`);
|
||||
}
|
||||
return { ...base, node, lane };
|
||||
return { ...base, node, lane, full, raw };
|
||||
}
|
||||
|
||||
function parseConfirmOptions(args: string[]): ConfirmOptions {
|
||||
@@ -2078,14 +2144,17 @@ function parseGitMirrorOptions(args: string[]): GitMirrorOptions {
|
||||
}
|
||||
|
||||
function parseCleanupRunnersOptions(args: string[]): CleanupRunnersOptions {
|
||||
validateOptions(args, new Set(["--confirm", "--dry-run", "--force-active"]), new Set(["--timeout-seconds", "--node", "--lane"]));
|
||||
validateOptions(args, new Set(["--confirm", "--dry-run", "--force-active", "--full", "--raw"]), new Set(["--timeout-seconds", "--node", "--lane"]));
|
||||
const base = parseConfirmOptions(args);
|
||||
const raw = args.includes("--raw");
|
||||
return {
|
||||
...base,
|
||||
node: optionValue(args, "--node") ?? null,
|
||||
lane: optionValue(args, "--lane") ?? null,
|
||||
timeoutSeconds: positiveIntegerOption(args, "--timeout-seconds", 180, 600),
|
||||
forceActive: args.includes("--force-active"),
|
||||
full: raw || args.includes("--full"),
|
||||
raw,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2598,6 +2667,91 @@ function renderAgentRunControlPlaneStatusSummary(result: Record<string, unknown>
|
||||
return renderedCliResult(result.ok !== false, "agentrun control-plane status", `${lines.join("\n")}\n`);
|
||||
}
|
||||
|
||||
function renderAgentRunControlPlanePlanSummary(result: Record<string, unknown>): RenderedCliResult {
|
||||
const target = record(result.target);
|
||||
const node = record(target.node);
|
||||
const runtime = record(target.runtime);
|
||||
const source = record(target.source);
|
||||
const checks = Array.isArray(result.plannedChecks) ? result.plannedChecks.map(String) : [];
|
||||
const lines = [
|
||||
"AGENTRUN CONTROL-PLANE PLAN",
|
||||
renderTable(
|
||||
["NODE", "LANE", "NAMESPACE", "SOURCE", "CHECKS", "MUTATION"],
|
||||
[[
|
||||
displayValue(node.id ?? target.node ?? "-"),
|
||||
displayValue(target.lane ?? "-"),
|
||||
displayValue(runtime.namespace ?? "-"),
|
||||
displayValue(source.branch ?? "-"),
|
||||
String(checks.length),
|
||||
"false",
|
||||
]],
|
||||
),
|
||||
"",
|
||||
"CHECKS",
|
||||
...(checks.length === 0 ? ["-"] : checks.slice(0, 12).map((check) => `- ${check}`)),
|
||||
"",
|
||||
"NEXT",
|
||||
...renderNextObjectLines(record(result.next)),
|
||||
"",
|
||||
"DETAIL",
|
||||
" use --full for rendered target details; valuesPrinted=false",
|
||||
];
|
||||
return renderedCliResult(result.ok !== false, "agentrun control-plane plan", `${lines.join("\n")}\n`);
|
||||
}
|
||||
|
||||
function renderAgentRunControlPlaneActionSummary(result: Record<string, unknown>, title: string): RenderedCliResult {
|
||||
const target = record(result.target);
|
||||
const node = record(target.node);
|
||||
const runtime = record(target.runtime);
|
||||
const summaryRows = [
|
||||
["ok", String(result.ok !== false)],
|
||||
["mode", displayValue(result.mode ?? "-")],
|
||||
["dryRun", displayValue(result.dryRun ?? "-")],
|
||||
["mutation", displayValue(result.mutation ?? "-")],
|
||||
["namespace", displayValue(result.namespace ?? runtime.namespace ?? "-")],
|
||||
];
|
||||
const countKeys = [
|
||||
"runnerJobCount",
|
||||
"inactiveCandidateCount",
|
||||
"selectedRunnerJobCount",
|
||||
"protectedActiveRunnerCount",
|
||||
"remainingAfterSelection",
|
||||
"deletedRunnerJobCount",
|
||||
"nonTerminalPodCount",
|
||||
"forceActive",
|
||||
];
|
||||
const countRows = countKeys
|
||||
.filter((key) => result[key] !== undefined)
|
||||
.map((key) => [key, displayValue(result[key])]);
|
||||
const lines = [
|
||||
title,
|
||||
renderTable(
|
||||
["NODE", "LANE", "NAMESPACE", "MODE", "OK"],
|
||||
[[
|
||||
displayValue(node.id ?? target.node ?? "-"),
|
||||
displayValue(target.lane ?? "-"),
|
||||
displayValue(result.namespace ?? runtime.namespace ?? "-"),
|
||||
displayValue(result.mode ?? "-"),
|
||||
String(result.ok !== false),
|
||||
]],
|
||||
),
|
||||
"",
|
||||
renderTable(["FIELD", "VALUE"], summaryRows),
|
||||
];
|
||||
if (countRows.length > 0) lines.push("", renderTable(["COUNT", "VALUE"], countRows));
|
||||
const nextLines = renderNextObjectLines(record(result.next ?? result.followUp));
|
||||
if (nextLines.length > 0) lines.push("", "NEXT", ...nextLines);
|
||||
lines.push("", "DETAIL", " use --full for capture/probe details; valuesPrinted=false");
|
||||
return renderedCliResult(result.ok !== false, String(result.command ?? "agentrun control-plane"), `${lines.join("\n")}\n`);
|
||||
}
|
||||
|
||||
function renderNextObjectLines(next: Record<string, unknown>): string[] {
|
||||
return Object.values(next)
|
||||
.filter((value): value is string => typeof value === "string" && value.length > 0)
|
||||
.slice(0, 5)
|
||||
.map((line) => ` ${line}`);
|
||||
}
|
||||
|
||||
function yesNo(value: unknown): string {
|
||||
if (value === true) return "yes";
|
||||
if (value === false) return "no";
|
||||
@@ -6068,9 +6222,14 @@ async function submitQueueTaskRest(args: string[]): Promise<Record<string, unkno
|
||||
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 = record(record(innerData(rendered)).queueTask);
|
||||
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 } });
|
||||
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");
|
||||
@@ -6093,7 +6252,15 @@ async function sessionSendRest(sessionId: string, args: string[]): Promise<Recor
|
||||
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: innerData(response), bridge: response.bridge };
|
||||
return {
|
||||
ok: response.ok !== false,
|
||||
command: "agentrun sessions send",
|
||||
data: {
|
||||
...record(innerData(response)),
|
||||
sessionPolicy: agentRunSessionRunPolicyDisclosure(record(sendBody.run)),
|
||||
},
|
||||
bridge: response.bridge,
|
||||
};
|
||||
}
|
||||
|
||||
function sessionSendPayloadFromInput(input: Record<string, unknown>, prompt: string | null): Record<string, unknown> {
|
||||
@@ -6134,6 +6301,7 @@ function isExplicitSessionSendBody(input: Record<string, unknown>): boolean {
|
||||
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)
|
||||
@@ -6142,12 +6310,14 @@ async function sessionRunBodyFromArgs(sessionId: string, args: string[], input:
|
||||
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) ?? sessionPolicy.providerId;
|
||||
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);
|
||||
body.executionPolicy = jsonObjectOption(args, "execution-policy-json") ?? record(body.executionPolicy);
|
||||
if (Object.keys(record(body.executionPolicy)).length === 0) body.executionPolicy = defaultAgentRunExecutionPolicy(profile, sessionPolicy);
|
||||
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,
|
||||
@@ -6161,26 +6331,123 @@ async function sessionRunBodyFromArgs(sessionId: string, args: string[], input:
|
||||
return body;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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,
|
||||
secretRef: {
|
||||
name: record(credential.secretRef).name ?? null,
|
||||
key: record(credential.secretRef).key ?? null,
|
||||
},
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
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 = record(renderedData.queueTask);
|
||||
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) ?? sessionPolicy.providerId,
|
||||
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) : executionPolicy,
|
||||
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 },
|
||||
};
|
||||
@@ -6196,7 +6463,19 @@ async function sessionSendWithAipodRest(sessionId: string, aipod: string, args:
|
||||
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 ?? ""), valuesPrinted: false }, bridge: response.bridge };
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchAgentRunSessionOrNull(sessionId: string, args: string[]): Promise<Record<string, unknown> | null> {
|
||||
@@ -6860,9 +7139,19 @@ async function aipodRenderInputFromArgs(args: string[], trailingPromptStart: num
|
||||
const input = await optionalJsonBody(args);
|
||||
const prompt = optionalPromptFromArgs(args, trailingPromptStart);
|
||||
if (prompt !== null) input.prompt = prompt;
|
||||
copyAgentRunOptions(args, input, ["tenant-id", "project-id", "queue", "lane", "title", "provider-id", "idempotency-key", "session-id"]);
|
||||
copyAgentRunOptions(args, input, ["tenant-id", "project-id", "queue", "node", "lane", "title", "provider-id", "idempotency-key", "session-id"]);
|
||||
const sessionPolicy = readAgentRunClientConfig().client.sessionPolicy;
|
||||
const policyTarget = resolveAgentRunSessionPolicyTarget({ node: stringOrNull(input.node), lane: stringOrNull(input.lane) });
|
||||
if (input.node === undefined) input.node = policyTarget.spec.nodeId;
|
||||
if (input.lane === undefined) input.lane = policyTarget.spec.lane;
|
||||
if (input.providerId === undefined) input.providerId = defaultAgentRunProviderId(sessionPolicy, policyTarget);
|
||||
const profile = agentRunOption(args, "profile") ?? agentRunOption(args, "backend-profile");
|
||||
if (profile) input.backendProfile = profile;
|
||||
const backendProfile = profile ?? stringOrNull(input.backendProfile) ?? sessionPolicy.backendProfile;
|
||||
input.backendProfile = backendProfile;
|
||||
if (!isRecord(input.workspaceRef)) input.workspaceRef = cloneJsonRecord(sessionPolicy.workspaceRef);
|
||||
if (!isRecord(input.executionPolicy)) {
|
||||
input.executionPolicy = defaultAgentRunExecutionPolicy(backendProfile, sessionPolicy, policyTarget, { includeToolCredentials: true });
|
||||
}
|
||||
const priority = agentRunOption(args, "priority");
|
||||
if (priority) input.priority = Number(priority);
|
||||
const workspaceRef = jsonObjectOption(args, "workspace-json");
|
||||
@@ -6924,7 +7213,18 @@ function jsonObjectOption(args: string[], flagName: string): Record<string, unkn
|
||||
}
|
||||
|
||||
function queueSubmitConfirmCommand(args: string[], aipod?: string): string {
|
||||
const dryRunless = args.filter((arg) => arg !== "--dry-run").join(" ");
|
||||
const parts: string[] = [];
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index] ?? "";
|
||||
if (arg === "submit" || arg === "--dry-run") continue;
|
||||
if (arg === "--aipod" || arg === "--aipod-spec") {
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (arg.startsWith("--aipod=") || arg.startsWith("--aipod-spec=")) continue;
|
||||
parts.push(arg);
|
||||
}
|
||||
const dryRunless = parts.join(" ");
|
||||
return `bun scripts/cli.ts agentrun queue submit${aipod ? ` --aipod ${aipod}` : ""}${dryRunless.length > 0 ? ` ${dryRunless}` : ""}`.trim();
|
||||
}
|
||||
|
||||
@@ -6935,15 +7235,51 @@ function jsonInputDisclosureFromArgs(args: string[]): Record<string, unknown> {
|
||||
};
|
||||
}
|
||||
|
||||
function defaultAgentRunExecutionPolicy(profile: string, sessionPolicy: AgentRunSessionPolicyConfig = readAgentRunClientConfig().client.sessionPolicy): Record<string, unknown> {
|
||||
const { spec } = resolveAgentRunLaneTarget({});
|
||||
function resolveAgentRunSessionPolicyTarget(options: AgentRunRestTargetOptions = { node: null, lane: null }): AgentRunSessionPolicyTarget {
|
||||
if (activeAgentRunRestTarget !== null) {
|
||||
return {
|
||||
configPath: activeAgentRunRestTarget.configPath,
|
||||
spec: activeAgentRunRestTarget.spec,
|
||||
source: "selected-lane",
|
||||
transport: "lane-k8s-service-proxy",
|
||||
};
|
||||
}
|
||||
const node = options.node ?? null;
|
||||
const lane = options.lane ?? null;
|
||||
const { configPath, spec } = resolveAgentRunLaneTarget({ node, lane });
|
||||
return {
|
||||
configPath,
|
||||
spec,
|
||||
source: node !== null || lane !== null ? "selected-lane" : "default-lane",
|
||||
transport: "direct-http",
|
||||
};
|
||||
}
|
||||
|
||||
function defaultAgentRunProviderId(sessionPolicy: AgentRunSessionPolicyConfig, target: AgentRunSessionPolicyTarget): string {
|
||||
return target.source === "selected-lane" ? target.spec.nodeId : sessionPolicy.providerId;
|
||||
}
|
||||
|
||||
function defaultAgentRunExecutionPolicy(
|
||||
profile: string,
|
||||
sessionPolicy: AgentRunSessionPolicyConfig = readAgentRunClientConfig().client.sessionPolicy,
|
||||
target: AgentRunSessionPolicyTarget = resolveAgentRunSessionPolicyTarget(),
|
||||
options: { includeToolCredentials?: boolean; basePolicy?: Record<string, unknown> } = {},
|
||||
): Record<string, unknown> {
|
||||
const basePolicy = Object.keys(record(options.basePolicy)).length > 0
|
||||
? cloneJsonRecord(record(options.basePolicy))
|
||||
: cloneJsonRecord(sessionPolicy.executionPolicy);
|
||||
return agentRunExecutionPolicyWithLaneCredentials(basePolicy, profile, target, options.includeToolCredentials === true);
|
||||
}
|
||||
|
||||
function agentRunExecutionPolicyWithLaneCredentials(basePolicy: Record<string, unknown>, profile: string, target: AgentRunSessionPolicyTarget, includeToolCredentials: boolean): Record<string, unknown> {
|
||||
const spec = target.spec;
|
||||
const credentials = agentRunProviderCredentialRefs(spec, profile);
|
||||
if (credentials.length === 0) {
|
||||
throw new AgentRunRestError("validation-failed", `config/agentrun.yaml has no providerCredential Secret binding for backendProfile=${profile} on default lane ${spec.nodeId}/${spec.lane}`);
|
||||
throw new AgentRunRestError("validation-failed", `${target.configPath} has no providerCredential Secret binding for backendProfile=${profile} on ${target.source} ${spec.nodeId}/${spec.lane}`);
|
||||
}
|
||||
const providerCredentials = credentials.map((credential) => {
|
||||
if (credential.secretRef.namespace !== spec.runtime.namespace) {
|
||||
throw new AgentRunRestError("validation-failed", `providerCredential ${profile} Secret ${credential.secretRef.name} is in namespace ${credential.secretRef.namespace}, expected default lane namespace ${spec.runtime.namespace}`);
|
||||
throw new AgentRunRestError("validation-failed", `providerCredential ${profile} Secret ${credential.secretRef.name} is in namespace ${credential.secretRef.namespace}, expected target lane namespace ${spec.runtime.namespace}`);
|
||||
}
|
||||
return {
|
||||
profile: credential.profile,
|
||||
@@ -6953,37 +7289,69 @@ function defaultAgentRunExecutionPolicy(profile: string, sessionPolicy: AgentRun
|
||||
},
|
||||
};
|
||||
});
|
||||
const basePolicy = cloneJsonRecord(sessionPolicy.executionPolicy);
|
||||
const toolCredentials = includeToolCredentials ? agentRunToolCredentialRefs(spec).map((credential) => {
|
||||
if (credential.secretRef.namespace !== spec.runtime.namespace) {
|
||||
throw new AgentRunRestError("validation-failed", `toolCredential ${credential.tool} Secret ${credential.secretRef.name} is in namespace ${credential.secretRef.namespace}, expected target lane namespace ${spec.runtime.namespace}`);
|
||||
}
|
||||
return {
|
||||
tool: credential.tool,
|
||||
secretRef: {
|
||||
name: credential.secretRef.name,
|
||||
key: credential.secretRef.key,
|
||||
},
|
||||
};
|
||||
}) : [];
|
||||
const secretScope = record(basePolicy.secretScope);
|
||||
return {
|
||||
...basePolicy,
|
||||
secretScope: {
|
||||
...secretScope,
|
||||
providerCredentials,
|
||||
...(toolCredentials.length === 0 ? {} : { toolCredentials }),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function renderAgentRunSessionPolicyExplanation(): string {
|
||||
function agentRunToolCredentialRefs(spec: AgentRunLaneSpec): Array<{ tool: string; sourceId: string; secretRef: { namespace: string; name: string; key: string }; valuesPrinted: false }> {
|
||||
return spec.secrets
|
||||
.filter((secret) => secret.providerCredentialProfile === null && secret.id.startsWith("tool-"))
|
||||
.map((secret) => ({
|
||||
tool: secret.id.replace(/^tool-/u, "").replace(/-token$/u, ""),
|
||||
sourceId: secret.id,
|
||||
secretRef: secret.targetRef,
|
||||
valuesPrinted: false,
|
||||
}));
|
||||
}
|
||||
|
||||
function renderAgentRunSessionPolicyExplanation(args: string[] = [], options: AgentRunRestTargetOptions = { node: null, lane: null }): string {
|
||||
const config = readAgentRunClientConfig();
|
||||
const { spec } = resolveAgentRunLaneTarget({});
|
||||
const target = resolveAgentRunSessionPolicyTarget(options);
|
||||
const spec = target.spec;
|
||||
const sessionPolicy = config.client.sessionPolicy;
|
||||
const credentials = agentRunProviderCredentialRefs(spec, sessionPolicy.backendProfile);
|
||||
const execution = defaultAgentRunExecutionPolicy(sessionPolicy.backendProfile, sessionPolicy);
|
||||
const backendProfile = agentRunOption(args, "profile") ?? agentRunOption(args, "backend-profile") ?? sessionPolicy.backendProfile;
|
||||
const providerId = agentRunOption(args, "provider-id") ?? defaultAgentRunProviderId(sessionPolicy, target);
|
||||
const workspaceRef = jsonObjectOption(args, "workspace-json") ?? cloneJsonRecord(sessionPolicy.workspaceRef);
|
||||
const credentials = agentRunProviderCredentialRefs(spec, backendProfile);
|
||||
const execution = defaultAgentRunExecutionPolicy(backendProfile, sessionPolicy, target);
|
||||
const credentialSources = credentials.map((credential) => ({
|
||||
profile: credential.profile,
|
||||
secretRef: credential.secretRef,
|
||||
valuesPrinted: false,
|
||||
}));
|
||||
const toolCredentialSources = agentRunToolCredentialRefs(spec);
|
||||
return [
|
||||
"KIND: session-policy",
|
||||
`CONFIG: ${config.sourcePath}`,
|
||||
`DEFAULT LANE: ${spec.nodeId}/${spec.lane}`,
|
||||
`DEFAULTS: tenantId=${sessionPolicy.tenantId} projectId=${sessionPolicy.projectId} providerId=${sessionPolicy.providerId} backendProfile=${sessionPolicy.backendProfile}`,
|
||||
`WORKSPACE: ${JSON.stringify(sessionPolicy.workspaceRef)}`,
|
||||
`LANE CONFIG: ${target.configPath}`,
|
||||
`TARGET LANE: ${spec.nodeId}/${spec.lane}`,
|
||||
`POLICY SOURCE: ${target.source}`,
|
||||
`TRANSPORT: ${target.transport}`,
|
||||
`DEFAULTS: tenantId=${sessionPolicy.tenantId} projectId=${sessionPolicy.projectId} providerId=${providerId} backendProfile=${backendProfile}`,
|
||||
`WORKSPACE: ${JSON.stringify(workspaceRef)}`,
|
||||
`EXECUTION: ${JSON.stringify(execution)}`,
|
||||
`PROVIDER CREDENTIAL SOURCES: ${JSON.stringify(credentialSources)}`,
|
||||
"VALUES: secret payloads are not printed",
|
||||
`TOOL CREDENTIAL SOURCES: ${JSON.stringify(toolCredentialSources)}`,
|
||||
"VALUES: secret payloads are not printed; valuesPrinted=false",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
@@ -7141,6 +7509,13 @@ interface AgentRunRestTarget {
|
||||
spec: AgentRunLaneSpec;
|
||||
}
|
||||
|
||||
interface AgentRunSessionPolicyTarget {
|
||||
configPath: string;
|
||||
spec: AgentRunLaneSpec;
|
||||
source: "selected-lane" | "default-lane";
|
||||
transport: "direct-http" | "lane-k8s-service-proxy";
|
||||
}
|
||||
|
||||
let activeAgentRunRestTarget: AgentRunRestTarget | null = null;
|
||||
|
||||
type AgentRunBridgeCaptureBackend = "local-backend-core-broker" | "remote-frontend-websocket";
|
||||
|
||||
Reference in New Issue
Block a user