From 14834cb472bac432674aa3c72018d1b42afa3def Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 25 Jun 2026 03:12:38 +0000 Subject: [PATCH] fix: bind AgentRun lane policy and bounded output --- .../specs/PJ2026-010202-runtime-assembly.md | 6 + .../specs/PJ2026-010203-queue-session.md | 8 +- .../specs/PJ2026-010603-yaml-first-ops.md | 13 +- scripts/src/agentrun.ts | 455 ++++++++++++++++-- 4 files changed, 439 insertions(+), 43 deletions(-) diff --git a/project-management/PJ2026-01/specs/PJ2026-010202-runtime-assembly.md b/project-management/PJ2026-01/specs/PJ2026-010202-runtime-assembly.md index a23eab10..9e545d71 100644 --- a/project-management/PJ2026-01/specs/PJ2026-010202-runtime-assembly.md +++ b/project-management/PJ2026-01/specs/PJ2026-010202-runtime-assembly.md @@ -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。 diff --git a/project-management/PJ2026-01/specs/PJ2026-010203-queue-session.md b/project-management/PJ2026-01/specs/PJ2026-010203-queue-session.md index ae95739b..b19a0c82 100644 --- a/project-management/PJ2026-01/specs/PJ2026-010203-queue-session.md +++ b/project-management/PJ2026-01/specs/PJ2026-010203-queue-session.md @@ -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/` 的短命令形态、显式 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 分层 | 编号 | 短名 | 主责模块 | 关联模块 | diff --git a/project-management/PJ2026-01/specs/PJ2026-010603-yaml-first-ops.md b/project-management/PJ2026-01/specs/PJ2026-010603-yaml-first-ops.md index e79e5da0..9679d9cf 100644 --- a/project-management/PJ2026-01/specs/PJ2026-010603-yaml-first-ops.md +++ b/project-management/PJ2026-01/specs/PJ2026-010603-yaml-first-ops.md @@ -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 脱敏规则。 diff --git a/scripts/src/agentrun.ts b/scripts/src/agentrun.ts index 9319cbd6..ecb13078 100644 --- a/scripts/src/agentrun.ts +++ b/scripts/src/agentrun.ts @@ -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 { 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 { @@ -1515,6 +1530,46 @@ function renderTaskDescription(task: Record, options: AgentRunR return lines.join("\n"); } +function compactTaskDescriptionPayload(ref: AgentRunResourceRef, task: Record): Record { + 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): Record { 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 return renderedCliResult(result.ok !== false, "agentrun control-plane status", `${lines.join("\n")}\n`); } +function renderAgentRunControlPlanePlanSummary(result: Record): 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, 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[] { + 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, prompt: string | null): Record { @@ -6134,6 +6301,7 @@ function isExplicitSessionSendBody(input: Record): boolean { async function sessionRunBodyFromArgs(sessionId: string, args: string[], input: Record): Promise> { 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 = { ...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, args: string[], aipod: string): Record { + 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 = { ...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, aipod: string): Record { + 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): Record { + 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> { 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 = { 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 | 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 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 { }; } -function defaultAgentRunExecutionPolicy(profile: string, sessionPolicy: AgentRunSessionPolicyConfig = readAgentRunClientConfig().client.sessionPolicy): Record { - 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 } = {}, +): Record { + 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, profile: string, target: AgentRunSessionPolicyTarget, includeToolCredentials: boolean): Record { + 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";