diff --git a/.agents/skills/unidesk-code-queue/SKILL.md b/.agents/skills/unidesk-code-queue/SKILL.md index 394e3a1c..567a908a 100644 --- a/.agents/skills/unidesk-code-queue/SKILL.md +++ b/.agents/skills/unidesk-code-queue/SKILL.md @@ -13,6 +13,7 @@ description: UniDesk AgentRun-backed Code Queue CLI — Skill(cli-spec)。legacy bun scripts/cli.ts agentrun get task --limit 20 bun scripts/cli.ts agentrun describe task/ bun scripts/cli.ts agentrun describe task/ --input -o json +bun scripts/cli.ts agentrun describe aipodspec/ bun scripts/cli.ts agentrun events run/ --limit 20 bun scripts/cli.ts agentrun logs session/ --tail 120 bun scripts/cli.ts agentrun result command/ --run @@ -34,6 +35,14 @@ AipodSpec/Artificer、task manifest 和 queue 渐进披露见 [references/resour - 该入口只投影 task create 字段、AipodSpec identity、资源引用和脱敏 provenance; - 不打印 Secret value; - `--input` 与 `--full`、`--raw` 互斥,非法组合必须在请求服务端前返回 `validation-failed`。 +- 读取 AipodSpec: + - 默认 `describe aipodspec/` 的 human、JSON 和 YAML 使用同一有界摘要; + - 摘要至少包含: + - identity/hash 与 runtime; + - workspace 与主 resource bundle source identity; + - 引用计数和脱敏 provenance; + - credential 只显示 SecretRef 并固定 `valuesPrinted=false`; + - 完整服务端对象只通过显式 `--full -o json` 或 `--raw` 下钻。 - Session follow-up 使用 `agentrun send`,由服务端决定内部 steer vs turn。 - 终态失败重试只使用 `agentrun retry task/`:保持同一 task,新增不可变 attempt;先用 `--dry-run` 完成服务端全校验,再以同一幂等键执行真实重试。 - retry 只接受 Queue 已处于 `failed` 或 `blocked`;`completed`、`cancelled` 和其他状态必须 fail closed。不得复制 task、重置 task、引入本地 scheduler/backoff/judge 或直接创建 runner Job 来模拟重试。 diff --git a/.agents/skills/unidesk-code-queue/references/resources.md b/.agents/skills/unidesk-code-queue/references/resources.md index 1a505506..162e7a93 100644 --- a/.agents/skills/unidesk-code-queue/references/resources.md +++ b/.agents/skills/unidesk-code-queue/references/resources.md @@ -5,6 +5,7 @@ - `agentrun create|apply`:创建 task 或应用 AipodSpec manifest; - `agentrun get|describe`:有界读取列表和详情; - `agentrun describe task/ --input -o json`:读取可重建 task 输入、AipodSpec identity、资源引用和脱敏 provenance; +- `agentrun describe aipodspec/`:读取同构、有界且 SecretRef-only 的 AipodSpec 摘要; - `agentrun events|logs|result`:渐进披露运行证据; - `agentrun get attempts --task `:读取同一 task 的不可变 attempt 历史; - `agentrun retry task/ --idempotency-key --reason [--dry-run]`:对终态失败 task 创建下一次 attempt; @@ -18,6 +19,18 @@ - 完整事件使用显式 `--full`、`--raw`,或按 `item.identity` 和 `afterSeq=item.seq-1 --limit 1 --full` 下钻; - 该 CLI 只渲染 AgentRun durable facts,不删除事件、不按正文去重,也不改变 Kafka/SSE 事件链。 +AipodSpec describe 必须遵守以下边界: + +- 默认 human、`-o json` 和 `-o yaml` 从同一摘要投影渲染; +- 摘要至少包含: + - identity/hash 与 lane/profile/provider; + - workspaceRef 与主 resource bundle source identity; + - bundle/required skill/credential 引用计数; + - 脱敏 provenance; +- credential 只保留 SecretRef 名称、namespace、keys 和安全 projection 字段,固定 `valuesPrinted=false`; +- 默认输出必须保留 `--full -o json` 与 `--raw` 语义化下钻,禁止读取临时 dump 再二次裁剪; +- 显式完整路径继续返回服务端完整对象,禁止提高全局 stdout 阈值掩盖默认输出缺陷。 + task 输入下钻必须遵守以下边界: - 默认 `describe task/` 的 `Next` 必须给出 `--input -o json` 语义化命令; diff --git a/scripts/src/agentrun.test.ts b/scripts/src/agentrun.test.ts index a9f1d472..5d28dc11 100644 --- a/scripts/src/agentrun.test.ts +++ b/scripts/src/agentrun.test.ts @@ -374,6 +374,187 @@ describe("AgentRun default transport contract", () => { } }); + test("aipodspec describe is bounded and homologous while full and raw stay complete", async () => { + const aipodName = "Artificer"; + const hiddenCredential = "HIDDEN_AIPOD_CREDENTIAL_VALUE_MUST_NOT_BE_PRINTED"; + const providerCredential = { + profile: "sub2api", + name: "agentrun-provider", + namespace: null, + keys: ["auth.json", "config.toml"], + value: hiddenCredential, + valuesPrinted: false, + }; + const toolCredentials = [ + { + tool: "github", + purpose: "github-pr", + name: "agentrun-github", + namespace: null, + keys: ["GH_TOKEN"], + projection: { kind: "env", envName: "GH_TOKEN", secretKey: "GH_TOKEN", value: hiddenCredential }, + token: hiddenCredential, + valuesPrinted: false, + }, + { + tool: "ssh", + purpose: "remote-workspace", + name: "agentrun-ssh", + namespace: null, + keys: ["config", "key"], + projection: { kind: "mount", mountPath: "/run/secrets/ssh", value: hiddenCredential }, + valuesPrinted: false, + }, + { + tool: "provider", + purpose: "model-auth", + name: "agentrun-provider", + namespace: null, + keys: ["auth.json"], + projection: { kind: "mount", mountPath: "/run/secrets/provider", value: hiddenCredential }, + valuesPrinted: false, + }, + ]; + const data = { + action: "show", + item: { + name: aipodName, + displayName: "Artificer", + description: "x".repeat(20_000), + specHash: "sha256:aipod-fixture", + source: "config/aipods/artificer.yaml", + backendProfile: "sub2api", + model: { model: "gpt-5.5", reasoningEffort: "high" }, + imageRef: { + kind: "env-image-dockerfile", + repoUrl: "https://github.com/pikasTech/agentrun.git", + commitId: "a".repeat(40), + dockerfilePath: "deploy/container/Containerfile", + sourceIdentity: "agentrun@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + valuesPrinted: false, + }, + queue: "commander", + lane: "v0.1", + providerId: "NC01", + providerCredentials: { count: 1, profiles: ["sub2api"], items: [providerCredential], valuesPrinted: false }, + toolCredentials: { count: 3, tools: ["github", "ssh", "provider"], items: toolCredentials, valuesPrinted: false }, + resourceBundleRef: { + kind: "gitbundle", + repoUrl: `https://fixture:${hiddenCredential}@github.com/pikasTech/unidesk.git`, + ref: "master", + commitId: null, + bundles: { count: 6, items: [{ name: "unidesk-skills", targetPath: ".agents/skills" }], valuesPrinted: false }, + requiredSkills: { count: 11, names: ["unidesk-gh"], valuesPrinted: false }, + promptRefs: { count: 0, names: [], valuesPrinted: false }, + valuesPrinted: false, + }, + createdAt: "2026-07-12T00:00:00.000Z", + updatedAt: "2026-07-12T01:00:00.000Z", + valuesPrinted: false, + }, + spec: { + apiVersion: "agentrun.pikastech.local/v1alpha1", + kind: "AipodSpec", + metadata: { name: aipodName, displayName: "Artificer", description: "x".repeat(20_000) }, + spec: { + queue: "commander", + lane: "v0.1", + providerId: "NC01", + backendProfile: "sub2api", + model: { model: "gpt-5.5", reasoningEffort: "high" }, + workspaceRef: { kind: "opaque", path: "." }, + executionPolicy: { + secretScope: { + allowCredentialEcho: false, + providerCredentials: [{ profile: "sub2api", secretRef: { name: "agentrun-provider", keys: ["auth.json"], value: hiddenCredential } }], + toolCredentials: [{ tool: "github", secretRef: { name: "agentrun-github", keys: ["GH_TOKEN"], token: hiddenCredential } }], + }, + }, + resourceBundleRef: { + kind: "gitbundle", + repoUrl: `https://fixture:${hiddenCredential}@github.com/pikasTech/unidesk.git`, + ref: "master", + bundles: [{ name: "unidesk-skills", subpath: ".agents/skills", targetPath: ".agents/skills" }], + requiredSkills: [{ name: "unidesk-gh" }], + }, + metadata: { aipod: aipodName, source: "config/aipods/artificer.yaml" }, + }, + }, + valuesPrinted: false, + }; + let requests = 0; + const server = Bun.serve({ + port: 0, + fetch(request) { + const url = new URL(request.url); + expect(request.headers.get("authorization")).toBe("Bearer secret-value"); + expect(url.pathname).toBe(`/api/v1/aipod-specs/${aipodName}`); + requests += 1; + return Response.json({ ok: true, data }); + }, + }); + const previousConfig = process.env.AGENTRUN_CLIENT_CONFIG; + const previousKey = process.env.HWLAB_API_KEY; + process.env.HWLAB_API_KEY = "secret-value"; + const tempConfigPath = `/tmp/unidesk-agentrun-aipodspec-test-${process.pid}-${Date.now()}.yaml`; + try { + process.env.AGENTRUN_CLIENT_CONFIG = tempConfigPath; + await Bun.write(tempConfigPath, agentRunMinimalClientYaml.replace("http://agentrun.example.local:8080", server.url.href.replace(/\/$/u, ""))); + + const text = renderedTextOf(await runAgentRunCommand(null, ["describe", `aipodspec/${aipodName}`])); + expect(text).toContain(`Name: aipodspec/${aipodName}`); + expect(text).toContain("Hash: sha256:aipod-fixture"); + expect(text).toContain("Kind: opaque path=. branch=-"); + expect(text).toContain("References: bundles=6 requiredSkills=11 promptRefs=0"); + expect(text).toContain("Credentials: provider=1 tool=3 total=4"); + expect(text).toContain("secret=agentrun-provider keys=auth.json,config.toml"); + expect(text).toContain(`agentrun describe aipodspec/${aipodName} --full -o json`); + expect(Buffer.byteLength(text, "utf8")).toBeLessThan(10_240); + expect(text).not.toContain(hiddenCredential); + + const jsonText = renderedTextOf(await runAgentRunCommand(null, ["describe", `aipodspec/${aipodName}`, "-o", "json"])); + const json = JSON.parse(jsonText) as Record; + const yamlText = renderedTextOf(await runAgentRunCommand(null, ["describe", `aipodspec/${aipodName}`, "-o", "yaml"])); + const yaml = Bun.YAML.parse(yamlText) as Record; + expect(yaml).toEqual(json); + expect(json.kind).toBe("AipodSpec"); + expect(json.identity.specHash).toBe("sha256:aipod-fixture"); + expect(json.runtime).toMatchObject({ lane: "v0.1", backendProfile: "sub2api", providerId: "NC01" }); + expect(json.workspaceRef).toMatchObject({ kind: "opaque", path: ".", valuesPrinted: false }); + expect(json.resourceBundleRef.sourceIdentity.repoUrl).toBe("https://github.com/pikasTech/unidesk.git"); + expect(json.resourceBundleRef).toMatchObject({ bundleCount: 6, requiredSkillCount: 11, promptRefCount: 0 }); + expect(json.credentialRefs).toMatchObject({ totalCount: 4, valuesPrinted: false }); + expect(json.credentialRefs.provider.items[0].secretRef).toEqual({ + namespace: null, + name: "agentrun-provider", + keys: ["auth.json", "config.toml"], + valuesPrinted: false, + }); + expect(json.redaction).toEqual({ credentialValues: "omitted", secretRefsOnly: true, valuesPrinted: false }); + expect(json.valuesPrinted).toBe(false); + for (const bounded of [jsonText, yamlText]) { + expect(Buffer.byteLength(bounded, "utf8")).toBeLessThan(10_240); + expect(bounded).not.toContain(hiddenCredential); + expect(bounded).not.toContain("x".repeat(200)); + } + + const full = JSON.parse(renderedTextOf(await runAgentRunCommand(null, ["describe", `aipodspec/${aipodName}`, "--full", "-o", "json"]))) as { resource?: typeof data }; + expect(full.resource?.item.description).toBe("x".repeat(20_000)); + expect(full.resource?.spec.kind).toBe("AipodSpec"); + const raw = JSON.parse(renderedTextOf(await runAgentRunCommand(null, ["describe", `aipodspec/${aipodName}`, "--raw"]))) as { data?: typeof data }; + expect(raw.data?.item.description).toBe("x".repeat(20_000)); + expect(raw.data?.spec.kind).toBe("AipodSpec"); + expect(requests).toBe(5); + } finally { + server.stop(true); + if (previousConfig === undefined) delete process.env.AGENTRUN_CLIENT_CONFIG; + else process.env.AGENTRUN_CLIENT_CONFIG = previousConfig; + if (previousKey === undefined) delete process.env.HWLAB_API_KEY; + else process.env.HWLAB_API_KEY = previousKey; + rmSync(tempConfigPath, { force: true }); + } + }); + test("retry dry-run is server-validated and attempts use the formal resource API", async () => { const taskId = "qt_failed_1"; const attempt = { diff --git a/scripts/src/agentrun/render.ts b/scripts/src/agentrun/render.ts index 03008f94..6adc3bfb 100644 --- a/scripts/src/agentrun/render.ts +++ b/scripts/src/agentrun/render.ts @@ -1387,6 +1387,252 @@ export function compactTaskDescriptionPayload(ref: AgentRunResourceRef, task: Re }; } +export function compactAipodSpecDescriptionPayload(ref: AgentRunResourceRef, data: unknown): Record { + const root = record(data); + const describedItem = record(root.item); + const item = Object.keys(describedItem).length > 0 ? describedItem : root; + const manifest = record(root.spec); + const manifestMetadata = record(manifest.metadata); + const manifestSpec = record(manifest.spec); + const manifestAipodMetadata = record(manifestSpec.metadata); + const model = firstNonEmptyRecord(item.model, manifestSpec.model); + const workspaceRef = compactAipodSpecWorkspaceRef(firstNonEmptyRecord(item.workspaceRef, manifestSpec.workspaceRef)); + const resourceBundleRef = compactAipodSpecResourceBundleRef(item.resourceBundleRef, manifestSpec.resourceBundleRef); + const secretScope = record(record(manifestSpec.executionPolicy).secretScope); + const providerCredentialRefs = compactAipodSpecCredentialRefs(item.providerCredentials, secretScope.providerCredentials, "provider"); + const toolCredentialRefs = compactAipodSpecCredentialRefs(item.toolCredentials, secretScope.toolCredentials, "tool"); + const imageRef = firstNonEmptyRecord(item.imageRef, manifestSpec.imageRef); + return { + kind: "AipodSpec", + name: ref.name, + state: boundedAipodSpecText(item.state ?? item.status ?? "available", 80), + identity: { + declaredName: boundedAipodSpecText(item.name ?? manifestMetadata.name ?? ref.name, 160), + displayName: boundedAipodSpecText(item.displayName ?? manifestMetadata.displayName, 200), + specHash: boundedAipodSpecText(item.specHash, 160), + valuesPrinted: false, + }, + runtime: { + queue: boundedAipodSpecText(item.queue ?? manifestSpec.queue, 120), + lane: boundedAipodSpecText(item.lane ?? manifestSpec.lane, 120), + backendProfile: boundedAipodSpecText(item.backendProfile ?? item.profile ?? manifestSpec.backendProfile, 160), + providerId: boundedAipodSpecText(item.providerId ?? manifestSpec.providerId, 160), + model: { + name: boundedAipodSpecText(model.model, 200), + reasoningEffort: boundedAipodSpecText(model.reasoningEffort, 80), + valuesPrinted: false, + }, + valuesPrinted: false, + }, + workspaceRef, + resourceBundleRef, + credentialRefs: { + totalCount: providerCredentialRefs.count + toolCredentialRefs.count, + provider: providerCredentialRefs, + tool: toolCredentialRefs, + valuesPrinted: false, + }, + provenance: { + source: boundedAipodSpecText(item.source ?? manifestAipodMetadata.source, 320), + apiVersion: boundedAipodSpecText(manifest.apiVersion, 120), + sourceKind: boundedAipodSpecText(manifest.kind, 120), + imageSourceIdentity: boundedAipodSpecText(imageRef.sourceIdentity, 320), + createdAt: boundedAipodSpecText(item.createdAt, 120), + updatedAt: boundedAipodSpecText(item.updatedAt, 120), + valuesPrinted: false, + }, + redaction: { + credentialValues: "omitted", + secretRefsOnly: true, + valuesPrinted: false, + }, + next: { + full: `bun scripts/cli.ts agentrun describe aipodspec/${ref.name} --full -o json`, + raw: `bun scripts/cli.ts agentrun describe aipodspec/${ref.name} --raw`, + }, + valuesPrinted: false, + }; +} + +export function renderAipodSpecDescription(payload: Record): string { + const identity = record(payload.identity); + const runtime = record(payload.runtime); + const model = record(runtime.model); + const workspaceRef = record(payload.workspaceRef); + const resourceBundleRef = record(payload.resourceBundleRef); + const sourceIdentity = record(resourceBundleRef.sourceIdentity); + const credentialRefs = record(payload.credentialRefs); + const providerCredentialRefs = record(credentialRefs.provider); + const toolCredentialRefs = record(credentialRefs.tool); + const provenance = record(payload.provenance); + const next = record(payload.next); + const lines = [ + `Name: aipodspec/${displayValue(payload.name)}`, + `State: ${displayValue(payload.state)}`, + "", + "Identity:", + ` Declared: ${displayValue(identity.declaredName)}`, + ` Display: ${displayValue(identity.displayName)}`, + ` Hash: ${displayValue(identity.specHash)}`, + "", + "Runtime:", + ` Queue: ${displayValue(runtime.queue)}`, + ` Lane: ${displayValue(runtime.lane)}`, + ` Profile: ${displayValue(runtime.backendProfile)}`, + ` Provider: ${displayValue(runtime.providerId)}`, + ` Model: ${displayValue(model.name)} reasoning=${displayValue(model.reasoningEffort)}`, + "", + "Workspace:", + ` Kind: ${displayValue(workspaceRef.kind)} path=${displayValue(workspaceRef.path)} branch=${displayValue(workspaceRef.branch)}`, + "", + "Primary resource bundle:", + ` Source: kind=${displayValue(sourceIdentity.kind)} repo=${displayValue(sourceIdentity.repoUrl)} ref=${displayValue(sourceIdentity.ref)} commit=${displayValue(sourceIdentity.commitId)}`, + ` References: bundles=${displayValue(resourceBundleRef.bundleCount)} requiredSkills=${displayValue(resourceBundleRef.requiredSkillCount)} promptRefs=${displayValue(resourceBundleRef.promptRefCount)}`, + ` Credentials: provider=${displayValue(providerCredentialRefs.count)} tool=${displayValue(toolCredentialRefs.count)} total=${displayValue(credentialRefs.totalCount)}`, + ]; + const credentialLines = [ + ...renderAipodSpecCredentialRefLines(arrayRecords(providerCredentialRefs.items), "provider"), + ...renderAipodSpecCredentialRefLines(arrayRecords(toolCredentialRefs.items), "tool"), + ]; + if (credentialLines.length > 0) lines.push("", "SecretRefs:", ...credentialLines.map((line) => ` ${line}`)); + lines.push( + "", + "Provenance:", + ` Source: ${displayValue(provenance.source)}`, + ` Manifest: ${displayValue(provenance.apiVersion)} ${displayValue(provenance.sourceKind)}`, + ` Image source: ${displayValue(provenance.imageSourceIdentity)}`, + ` Updated: ${displayValue(provenance.updatedAt)}`, + " Values printed: false", + "", + "Next:", + ` ${displayValue(next.full)}`, + ` ${displayValue(next.raw)}`, + ); + return lines.join("\n"); +} + +function compactAipodSpecWorkspaceRef(value: Record): Record | null { + if (Object.keys(value).length === 0) return null; + return { + kind: boundedAipodSpecText(value.kind, 80), + path: boundedAipodSpecText(value.path, 320), + branch: boundedAipodSpecText(value.branch, 160), + repo: boundedAipodSpecRepoUrl(value.repo ?? value.repoUrl), + commitId: boundedAipodSpecText(value.commitId, 160), + valuesPrinted: false, + }; +} + +function compactAipodSpecResourceBundleRef(primaryValue: unknown, fallbackValue: unknown): Record | null { + const value = firstNonEmptyRecord(primaryValue, fallbackValue); + if (Object.keys(value).length === 0) return null; + return { + sourceIdentity: { + kind: boundedAipodSpecText(value.kind, 80), + repoUrl: boundedAipodSpecRepoUrl(value.repoUrl), + ref: boundedAipodSpecText(value.ref, 160), + commitId: boundedAipodSpecText(value.commitId, 160), + valuesPrinted: false, + }, + bundleCount: aipodSpecReferenceCount(value.bundles), + requiredSkillCount: aipodSpecReferenceCount(value.requiredSkills), + promptRefCount: aipodSpecReferenceCount(value.promptRefs), + valuesPrinted: false, + }; +} + +function compactAipodSpecCredentialRefs(primaryValue: unknown, fallbackValue: unknown, kind: "provider" | "tool"): { count: number; items: Record[]; omittedCount: number; valuesPrinted: false } { + const primary = record(primaryValue); + const primaryItems = arrayRecords(primary.items ?? primaryValue); + const fallbackItems = arrayRecords(fallbackValue); + const sourceItems = primaryItems.length > 0 ? primaryItems : fallbackItems; + const count = Object.keys(primary).length > 0 ? aipodSpecReferenceCount(primaryValue) : sourceItems.length; + const items = sourceItems.slice(0, 6).map((item) => { + const nestedSecretRef = record(item.secretRef); + const projection = record(item.projection); + return { + ...(kind === "provider" + ? { profile: boundedAipodSpecText(item.profile, 160) } + : { + tool: boundedAipodSpecText(item.tool, 160), + purpose: boundedAipodSpecText(item.purpose, 200), + }), + secretRef: { + namespace: boundedAipodSpecText(item.namespace ?? nestedSecretRef.namespace, 160), + name: boundedAipodSpecText(item.name ?? nestedSecretRef.name, 200), + keys: boundedAipodSpecStrings(item.keys ?? nestedSecretRef.keys, 6, 80), + valuesPrinted: false, + }, + ...(kind === "tool" + ? { + projection: { + kind: boundedAipodSpecText(projection.kind, 80), + envName: boundedAipodSpecText(projection.envName, 160), + secretKey: boundedAipodSpecText(projection.secretKey, 160), + mountPath: boundedAipodSpecText(projection.mountPath, 320), + valuesPrinted: false, + }, + } + : {}), + valuesPrinted: false, + }; + }); + return { + count, + items, + omittedCount: Math.max(0, count - items.length), + valuesPrinted: false, + }; +} + +function renderAipodSpecCredentialRefLines(items: Record[], kind: "provider" | "tool"): string[] { + return items.map((item) => { + const secretRef = record(item.secretRef); + const namespace = stringOrNull(secretRef.namespace); + const name = displayValue(secretRef.name); + const reference = namespace === null ? name : `${namespace}/${name}`; + const keys = Array.isArray(secretRef.keys) ? secretRef.keys.map(String).join(",") : "-"; + const identity = kind === "provider" + ? `profile=${displayValue(item.profile)}` + : `tool=${displayValue(item.tool)} purpose=${displayValue(item.purpose)}`; + return `${kind} ${identity} secret=${reference} keys=${keys}`; + }); +} + +function firstNonEmptyRecord(...values: unknown[]): Record { + for (const value of values) { + const candidate = record(value); + if (Object.keys(candidate).length > 0) return candidate; + } + return {}; +} + +function aipodSpecReferenceCount(value: unknown): number { + if (Array.isArray(value)) return value.length; + const summary = record(value); + const count = summary.count; + if (typeof count === "number" && Number.isFinite(count) && count >= 0) return Math.floor(count); + return arrayRecords(summary.items).length; +} + +function boundedAipodSpecStrings(value: unknown, limit: number, maxChars: number): string[] { + if (!Array.isArray(value)) return []; + return value + .slice(0, limit) + .map((item) => boundedAipodSpecText(item, maxChars)) + .filter((item): item is string => item !== null); +} + +function boundedAipodSpecText(value: unknown, maxChars: number): string | null { + const text = stringOrNull(value); + return text === null ? null : truncateOneLine(text, maxChars); +} + +function boundedAipodSpecRepoUrl(value: unknown): string | null { + const sanitized = taskInputRepoUrl(value); + return typeof sanitized === "string" ? truncateOneLine(sanitized, 320) : null; +} + export function taskInputDescriptionPayload(ref: AgentRunResourceRef, task: Record): Record { const metadata = taskInputMetadata(task.metadata); const aipodImageRef = taskInputImageRef(metadata.aipodImageRef); diff --git a/scripts/src/agentrun/resource-actions.ts b/scripts/src/agentrun/resource-actions.ts index 984559eb..7857c791 100644 --- a/scripts/src/agentrun/resource-actions.ts +++ b/scripts/src/agentrun/resource-actions.ts @@ -37,7 +37,7 @@ import type { AgentRunResourceOptions, AgentRunResourceRef, AgentRunResourceVerb import { agentRunDryRunPlan } from "./config"; import { isHelpArg, renderAgentRunHelp } from "./entry"; import { agentRunExplain, arrayRecords, shortId } from "./options"; -import { agentRunCancelAuthorityDisclosure, agentRunCancelCascadeScope, agentRunCancelFencingDisclosure, agentRunCancelRunnerAbortDisclosure, compactTaskDescriptionPayload, innerData, normalizeAipodSpecItems, normalizeAttemptResources, normalizeEventItems, normalizeLogItems, normalizeQueueAttemptItems, normalizeRunnerJobItems, normalizeSessionItems, normalizeSingleCommand, normalizeTaskItems, parseResourceKind, parseResourceRef, renderDescribe, renderEventLike, renderGenericDescription, renderMachine, renderMutationSummary, renderQueueAttemptList, renderQueueRetrySummary, renderResourceResult, renderResultSummary, renderTaskDescription, renderedCliResult, requiredContext, rerunWithoutDryRun, resolveAgentRunCancelPolicyTarget, resourceApply, resourceCreate, resourceDispatch, resourceSessionPromptCommand, taskInputDescriptionPayload, taskListState, unwrapTaskDetail } from "./render"; +import { agentRunCancelAuthorityDisclosure, agentRunCancelCascadeScope, agentRunCancelFencingDisclosure, agentRunCancelRunnerAbortDisclosure, compactAipodSpecDescriptionPayload, compactTaskDescriptionPayload, innerData, normalizeAipodSpecItems, normalizeAttemptResources, normalizeEventItems, normalizeLogItems, normalizeQueueAttemptItems, normalizeRunnerJobItems, normalizeSessionItems, normalizeSingleCommand, normalizeTaskItems, parseResourceKind, parseResourceRef, renderAipodSpecDescription, renderDescribe, renderEventLike, renderGenericDescription, renderMachine, renderMutationSummary, renderQueueAttemptList, renderQueueRetrySummary, renderResourceResult, renderResultSummary, renderTaskDescription, renderedCliResult, requiredContext, rerunWithoutDryRun, resolveAgentRunCancelPolicyTarget, resourceApply, resourceCreate, resourceDispatch, resourceSessionPromptCommand, taskInputDescriptionPayload, taskListState, unwrapTaskDetail } from "./render"; import { AgentRunRestError, renderAgentRunRestError, resolveAgentRunRestTarget, runAgentRunRestCommand, withAgentRunRestTarget } from "./rest-bridge"; import { runnerJobObservationScript, type RunnerJobObservationIdentity } from "./runner-observation"; import { capture, captureJsonPayload, compactCapture, nonNegativeIntegerOrNull, record, stringOrNull } from "./utils"; @@ -361,7 +361,10 @@ export async function resourceDescribe(config: UniDeskConfig | null, command: st } if (ref.kind === "aipodspec") { const result = await runAgentRunRestCommand(config, "aipod-specs", ["show", ref.name, ...(options.full ? ["--full"] : [])]); - return renderDescribe(command, result, options, ref, renderGenericDescription(ref, innerData(result))); + if (options.raw || options.full) return renderDescribe(command, result, options, ref, renderGenericDescription(ref, innerData(result))); + const payload = compactAipodSpecDescriptionPayload(ref, innerData(result)); + if (options.output === "json" || options.output === "yaml") return renderMachine(command, payload, options.output, result.ok !== false); + return renderedCliResult(result.ok !== false, command, renderAipodSpecDescription(payload)); } throw new Error(`unsupported describe kind: ${ref.kind}`); }