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