Merge pull request #1823 from pikasTech/fix/1822-agentrun-aipodspec-summary
Pipelines as Code CI / hwlab-web-probe-sentinel-nc01- Success
Pipelines as Code CI / platform-infra-gitea-nc01- Success
Pipelines as Code CI / unidesk-host- Success

fix: 有界渲染 AgentRun AipodSpec describe
This commit is contained in:
Lyon
2026-07-12 13:20:17 +08:00
committed by GitHub
5 changed files with 454 additions and 2 deletions
@@ -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/<taskId>
bun scripts/cli.ts agentrun describe task/<taskId> --input -o json
bun scripts/cli.ts agentrun describe aipodspec/<name>
bun scripts/cli.ts agentrun events run/<runId> --limit 20
bun scripts/cli.ts agentrun logs session/<sessionId> --tail 120
bun scripts/cli.ts agentrun result command/<commandId> --run <runId>
@@ -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/<name>` 的 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/<taskId>`:保持同一 task,新增不可变 attempt;先用 `--dry-run` 完成服务端全校验,再以同一幂等键执行真实重试。
- retry 只接受 Queue 已处于 `failed``blocked``completed``cancelled` 和其他状态必须 fail closed。不得复制 task、重置 task、引入本地 scheduler/backoff/judge 或直接创建 runner Job 来模拟重试。
@@ -5,6 +5,7 @@
- `agentrun create|apply`:创建 task 或应用 AipodSpec manifest
- `agentrun get|describe`:有界读取列表和详情;
- `agentrun describe task/<taskId> --input -o json`:读取可重建 task 输入、AipodSpec identity、资源引用和脱敏 provenance
- `agentrun describe aipodspec/<name>`:读取同构、有界且 SecretRef-only 的 AipodSpec 摘要;
- `agentrun events|logs|result`:渐进披露运行证据;
- `agentrun get attempts --task <taskId>`:读取同一 task 的不可变 attempt 历史;
- `agentrun retry task/<taskId> --idempotency-key <key> --reason <text> [--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/<taskId>``Next` 必须给出 `--input -o json` 语义化命令;
+181
View File
@@ -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<string, any>;
const yamlText = renderedTextOf(await runAgentRunCommand(null, ["describe", `aipodspec/${aipodName}`, "-o", "yaml"]));
const yaml = Bun.YAML.parse(yamlText) as Record<string, any>;
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 = {
+246
View File
@@ -1387,6 +1387,252 @@ export function compactTaskDescriptionPayload(ref: AgentRunResourceRef, task: Re
};
}
export function compactAipodSpecDescriptionPayload(ref: AgentRunResourceRef, data: unknown): Record<string, unknown> {
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, unknown>): 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<string, unknown>): Record<string, unknown> | 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<string, unknown> | 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<string, unknown>[]; 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<string, unknown>[], 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<string, unknown> {
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<string, unknown>): Record<string, unknown> {
const metadata = taskInputMetadata(task.metadata);
const aipodImageRef = taskInputImageRef(metadata.aipodImageRef);
+5 -2
View File
@@ -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}`);
}