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
@@ -37,6 +37,7 @@ Runtime装配负责把一次 AgentRun 执行需要的镜像、provider profile
- provider credential、Git resource credential、tool credential 和 short-lived execution context 的 SecretRef/projection/redaction 归属。
- Git-only `ResourceBundleRef.kind="gitbundle"``bundles[]``promptRefs[]``requiredSkills[]`、workspace materialization 和实际 commit 摘要。
- AipodSpec YAML、`imageRef`、env image reuse、Artificer 默认装配和 `--aipod` render 入口。
- AipodSpec render 对目标 node/lane 的 backendProfile、provider credential SecretRef、tool credential SecretRef、workspaceRef 和 bundle 绑定。
- SessionRef 到 PVC 直接挂载、`CODEX_HOME`、profile runtime home 和 Secret projection 分离。
### 2.3 范围外
@@ -57,6 +58,7 @@ Runtime装配负责把一次 AgentRun 执行需要的镜像、provider profile
| SessionRef | 描述 backend 会话、thread、PVC 和续跑状态的运行时要素。 |
| ResourceBundleRef | 描述 Git-only 资源包、工具、skill、prompt 和实际物化 commit 的运行时要素。 |
| AipodSpec | 声明式 agent 装配 YAML,用于集中描述 imageRef、backendProfile、executionPolicy、resourceBundleRef 和 payloadDefaults。 |
| AipodSpec lane binding | AipodSpec render 时将声明式装配与目标 node/lane 的 YAML SecretRef、workspaceRef、bundle 和 provider profile 绑定的过程。 |
| toolCredentials | 运行时工具授权的 SecretRef 装配项,例如 GitHub PR token、GitHub SSH key 或 UniDesk SSH passthrough token。 |
| transientEnv | 单次 runner job 的短期运行上下文,通过短期 Secret 投影,不作为 run durable fact。 |
@@ -105,6 +107,8 @@ Runtime装配应通过 SecretRef 和 projection intent 分发 provider credentia
输出只能包含 SecretRef 名称、key 名、projection kind、presence、hash/fingerprint、ownerReference 状态和 `valuesPrinted=false`。provider API key、Codex `auth.json``config.toml`、DSN、token、SSH private key 和 transientEnv value 都不得以明文形式出现在响应、dry-run、event 或日志中。
RuntimeAssembly 或 AipodSpec render 缺少目标 lane 所需的 provider credential SecretRef、tool credential SecretRef 或 sourceRef 时,应报告为 YAML/AipodSpec binding 缺口。修复路径是补齐 YAML/sourceRef、受控 Secret 同步和重新 render,不允许通过手工创建 legacy Secret、复制其他 lane Secret 或 patch runtime namespace 规避装配缺陷。
### 6.3 AR-RUNTIME-REQ-003 GitBundle 资源物化
| 编号 | 短名 | 主责模块 | 关联模块 |
@@ -136,3 +140,5 @@ SessionRef 是 Web、CLI、Queue 和 HWLAB接入共享的会话连续性 authori
Runtime装配应提供 AipodSpec YAML,使 imageRef、backendProfile、executionPolicy、resourceBundleRef、model 和 payloadDefaults 可以从声明式规格进入 Queue submit 或 Session send。
AipodSpec 只声明装配意图,不保存 API key、SSH private key、token、`auth.json``config.toml` 或其他 Secret 明文。`imageRef` 必须指向可追溯的 env image Dockerfile source 和完整 commit,最终 runner job 只能使用解析后的 digest-pinned image 或结构化报告 build-required。
当 Queue submit、Session send 或 CLI `--aipod` render 已指定 node/lane 时,AipodSpec 必须使用该目标 lane 的 YAML 事实绑定 backendProfile、provider credential SecretRef、tool credential SecretRef、workspaceRef 和 resource bundle。除非 YAML 明确声明继承,AipodSpec 不得从其他 lane 或全局默认 lane 隐式继承 provider、tool credential、workspace 或 namespace。
@@ -37,6 +37,7 @@
- Session API/CLI 的输出、trace、命令流、debug/audit 详情、read 状态和会话控制。
- Queue task 到 Core run/command/runner job/session 的引用关系和 sessionPath 输出。
- `sessions send`、session continuation、unread/default/all 状态和 terminal projection。
- `send session` follow-up 对 node/lane、backendProfile、providerId、workspaceRef、executionPolicy 和 provider credential SecretRef 的策略解析。
- Queue task、Session、run 和 command 取消入口的用户语义、级联边界、同 session 续跑和 canceled terminal projection。
- 自动 scheduler 的 deferred 边界、future pending scan、capacity selection、runner assignment 和 stale lease recovery 方向。
@@ -56,6 +57,7 @@
| Queue task | 用户或系统提交到 AgentRun Queue 的任务资源,可 dispatch 为 Core run/command。 |
| attempt | Queue task 的一次执行尝试,引用 runId、commandId、runnerJobId 和 sessionId。 |
| Session | 面向用户的执行会话控制面,承载 output、trace、read、send、cancel 和 terminal projection。 |
| session policy | Session follow-up 创建下一次 run/command 时使用的 backendProfile、providerId、workspaceRef、executionPolicy 和 provider credential SecretRef 策略。 |
| sessionPath | Queue task 返回的 Session API 路径,用于读取输出和 trace。 |
| read cursor | 按 readerId 记录的已读水位,用于默认列表只显示 running 或 unread session/task。 |
| commander | Queue 侧的聚合视图,用于展示队列状态、最新 attempt 和下一步操作摘要。 |
@@ -69,7 +71,7 @@
| 边界项 | 内容 |
| --- | --- |
| 外部使用者 | UniDesk 用户、HWLAB Agent 入口、CLI 操作者、Artificer/Aipod、AgentRun 核心和平台运维。 |
| 外部输入 | Queue task JSON、dispatch body、sessionId、prompt、run base、readerId、backendProfile、resourceBundleRef、cancel/read/send 控制请求。 |
| 外部输入 | Queue task JSON、dispatch body、sessionId、prompt、run base、readerId、node/lane、backendProfile、providerId、workspaceRef、resourceBundleRef、cancel/read/send 控制请求。 |
| 受控资源 | Queue task、attempt、summary、stats、read cursor、Session projection、session trace/output 和 scheduler deferred 状态。 |
| 外部输出 | taskId、attemptId、sessionId、sessionPath、queue stats、commander summary、unread/default/all 列表、output/trace cursor 和控制结果。 |
| 用户接口 | AgentRun REST Queue/Session API、`./scripts/agentrun queue ...``./scripts/agentrun sessions ...`。 |
@@ -107,6 +109,10 @@ AgentRun Queue 直接吸收 UniDesk Code Queue 的新任务入口,不做 adapt
Queue task 详情只能返回 `sessionPath` 和有界摘要,不代理完整输出或 trace。输出、trace 和会话控制必须从 Session API/CLI 进入,避免 Queue 列表视图反推 Core event stream 或展开执行细节。
`send session/<sessionId>` 的短命令形态、显式 JSON body 形态和 `--aipod` 形态都必须使用目标 node/lane 的 YAML 事实解析 session policy。CLI 或 API 已显式给出 `node`/`lane` 时,backendProfile、providerId、workspaceRef、executionPolicy 和 provider credential SecretRef 不得回退到全局默认 lane;未显式给出目标时,输出必须披露使用的 YAML default 来源。
`explain session-policy``send session` 的同源预检入口,必须展示与实际 follow-up 创建相同的目标 node/lane、backendProfile、providerId、workspaceRef、executionPolicy、provider credential SecretRef 和脱敏来源摘要。若解释输出与目标 lane 不一致,应视为策略解析或渲染缺陷,不得通过复制其他 lane Secret、手工补 legacy Secret 或修改 runtime namespace 绕过。
### 6.3 AR-QUEUE-REQ-003 Queue/Session/Core 分层
| 编号 | 短名 | 主责模块 | 关联模块 |
@@ -41,7 +41,7 @@ YAML运维负责 HWLAB/UniDesk 自有平台配置的真相源、解析、渲染
- Sub2API、Codex pool、AgentRun control-plane、session policy 和平台基础设施配置的受控 CLI 读取、解释、计划和下发。
- AgentRun lane 的 runner retention、idle timeout、egress proxy 和 cancel lifecycle policy 等运行策略配置读取、解释和下发。
- FRP、Caddy、public URL、public health、Kubernetes Secret 和平台资源渲染所需的配置投递边界。
- 可复用 ops primitive,包括 YAML path 捕获、字段解析、fingerprint、摘要输出、Secret 引用和命令输出约束。
- 可复用 ops primitive,包括 YAML path 捕获、字段解析、fingerprint、摘要输出、Secret 引用、有界默认输出和命令输出约束。
### 2.3 范围外
@@ -66,6 +66,7 @@ YAML运维负责 HWLAB/UniDesk 自有平台配置的真相源、解析、渲染
| publicExposure | YAML 中描述 FRP、Caddy、domain、TLS、public URL 和 health 目标的公开入口声明。 |
| ops primitive | 平台运维 CLI 共享的底层能力,例如字段解析、fingerprint、Secret 引用、摘要输出和 YAML path 捕获。 |
| 配置解释输出 | CLI 将 YAML 解析后的默认值、来源和目标以非敏感摘要展示给操作人员的输出。 |
| 有界默认输出 | CLI 在未显式请求机器输出或完整输出时返回的短摘要、表格和 drill-down 命令集合。 |
## 4. 系统边界和接口
@@ -76,7 +77,7 @@ YAML运维负责 HWLAB/UniDesk 自有平台配置的真相源、解析、渲染
| 外部使用者 | 平台管理员、发布操作人员、Agent编排维护者、Sub2API/Codex pool 维护者和自动化运维任务。 |
| 外部输入 | YAML 配置文件、target/lane/node/service 声明、Secret sourceRef、providerCredential、publicExposure、运行目标选择和配置解释请求。 |
| 受控资源 | 配置解析器、公共 ops helper、Sub2API/Codex pool 配置、AgentRun lane 配置、Kubernetes Secret 声明、FRP/Caddy 渲染、public health 和 CLI 输出摘要。 |
| 外部输出 | 配置解释结果、计划摘要、同步摘要、Secret presence/fingerprint、publicExposure 渲染结果、运行目标解析结果和错误诊断。 |
| 外部输出 | 配置解释结果、计划摘要、同步摘要、Secret presence/fingerprint、publicExposure 渲染结果、运行目标解析结果、有界默认输出和错误诊断。 |
| 用户接口 | `bun scripts/cli.ts platform-infra ...``bun scripts/cli.ts agentrun ...`、受控 Secret 同步命令、配置解释命令和平台状态查询命令。 |
| 系统边界 | YAML运维负责让配置从 YAML 可解释、可校验并受控进入运行面;不定义业务策略数值、不替代 Agent/用户/客户端语义,也不把运行面观测反向变成本地配置真相。 |
@@ -115,6 +116,8 @@ YAML运维应提供 target、lane、node、service、namespace 和默认目标
当 issue、PR 或 CLI 已经明确 node/lane 时,YAML 解析只负责校验和解释该目标,不得用全局默认覆盖用户选择。没有明确目标时,CLI 可以读取 YAML default,但输出必须说明 default 来源、目标 node/lane、namespace、source truth 和公开入口,避免把历史 G14/v0.2、D601 legacy 或旧 `dev/prod` 口径误用为当前运行面。
AgentRun `send session``explain session-policy`、resource primitives 和 AipodSpec render 都属于 node/lane 敏感入口。命令行或请求体已明确 node/lane 时,这些入口必须使用目标 lane 的 YAML 事实解析 backendProfile、providerId、workspaceRef、executionPolicy、provider credential SecretRef、tool credential SecretRef、namespace 和 source truth;不得在解释、dry-run、短命令或 JSON body 路径中回退到 control-plane default 或历史全局 sessionPolicy。
### 6.3 OPS-YAML-REQ-003 Secret 绑定与敏感输出
| 编号 | 短名 | 主责模块 | 关联模块 |
@@ -125,6 +128,8 @@ YAML运维应通过 sourceRef、targetKey、providerCredential 和 manual bindin
密钥相关输出只能展示对象名、key 名、sourceRef、presence、fingerprint、字节数和执行摘要,不得打印 base64 payload、解码值、完整 DSN、API key 或可复制凭据。运行面只能作为 presence 和 health 观测对象,不能被用来反推本地配置真相。
目标 lane 缺少 providerCredential、tool credential、targetKey 或 sourceRef 时,应作为 YAML 配置或 AipodSpec binding 缺口暴露。受控 CLI 可以显示缺失对象名、key 名、YAML path、presence=false、fingerprint 缺失和 `valuesPrinted=false`,但不得用手工 Secret、复制其他 lane 凭据、读取运行面 Secret value 或 patch namespace 的方式修复配置缺口。
### 6.4 OPS-YAML-REQ-004 公开入口渲染
| 编号 | 短名 | 主责模块 | 关联模块 |
@@ -149,6 +154,8 @@ YAML运维应为 AgentRun control-plane default、client sessionPolicy、lane se
cancel lifecycle policy 至少应能声明取消信号投递方式、runner graceful abort、kill escalation、stale heartbeat fencing window、late write fencing 和默认事件阶段输出开关。CLI 只校验字段结构、类型、必填项和可渲染性;具体窗口、超时和开关值由 YAML 承载,不在代码或 SPEC 中写成第二真相。
sessionPolicy 的解释、Session follow-up 的 run body、AipodSpec render 结果和 resource primitive 的 dry-run/JSON 形态必须来自同一套 YAML 目标解析。非默认 lane 的 backendProfile、providerId、workspaceRef、executionPolicy 和 provider credential SecretRef 不得被全局 default lane 覆盖;输出中应明确目标 lane 与 policy 来源,使操作者能在创建 run/command 前发现 lane 绑定错误。
本需求只约束执行策略如何作为平台配置进入运行面。Agent run、command、session 状态机、任务恢复、取消语义和 provider 业务语义由 Agent编排负责,用户身份和 API key 约束由用户管理负责。
### 6.6 OPS-YAML-REQ-006 公共 ops primitive
@@ -160,3 +167,5 @@ cancel lifecycle policy 至少应能声明取消信号投递方式、runner grac
YAML运维应沉淀公共 ops primitive,使字段解析、YAML path 捕获、fingerprint、Secret 引用、摘要输出和配置错误说明在 Sub2API、AgentRun 和后续平台 CLI 中复用。
公共 ops primitive 的职责是减少重复硬编码和输出口径漂移。领域 CLI 仍负责自身命令形态和服务语义,但不得复制一套不同的敏感输出、fingerprint、字段解析或 YAML 缺失处理规则。
受控 CLI 的默认输出必须是有界摘要、关键字段表格和下一步 drill-down 命令;`/tmp/unidesk-cli-output` dump 只能作为异常兜底,不能成为正常交互路径。完整 JSON、raw payload、完整 task/result/log、长 plan 和机器消费输出只能在显式 `--full``--raw``-o json` 或等价机器消费参数下返回,并仍须遵守 Secret 脱敏规则。
+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";