fix: bind AgentRun lane policy and bounded output

This commit is contained in:
Codex
2026-06-25 03:12:38 +00:00
parent ee15a724d2
commit 14834cb472
4 changed files with 439 additions and 43 deletions
+415 -40
View File
@@ -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";