Merge pull request #1832 from pikasTech/fix/agentrun-secret-sync-compact
fix: 收敛 AgentRun Secret 同步预检输出
This commit is contained in:
@@ -13,10 +13,21 @@ bun scripts/cli.ts hwlab g14 secret ensure --lane v02 \
|
||||
|
||||
bun scripts/cli.ts hwlab g14 secret delete --lane v02 \
|
||||
--name <obsolete-secret> [--dry-run|--confirm]
|
||||
|
||||
bun scripts/cli.ts agentrun control-plane secret-sync \
|
||||
--node <node> --lane <lane> [--secret-id <yaml-secret-id>] \
|
||||
[--dry-run|--confirm] [-o json|yaml] [--full|--raw]
|
||||
```
|
||||
|
||||
Secret 只通过 YAML sourceRef/targetKey 和受控 CLI 下发;输出只披露对象名、key 名、presence、fingerprint 和摘要,不打印完整凭据。
|
||||
|
||||
- AgentRun `secret-sync --dry-run` 的默认文本、JSON 和 YAML 使用同一个有界投影:
|
||||
- 直接展示 YAML Secret ID、目标 Secret/key、source presence/fingerprint;
|
||||
- 阻断时展示 activation fence identity、状态、关联 runner/run/command identity 和可执行只读下钻;
|
||||
- 所有模式固定 `valuesPrinted=false`。
|
||||
- `--secret-id <yaml-secret-id>` 是单个声明的稳定下钻;`--full` 与 `--raw` 才请求 exhaustive probe/capture。
|
||||
- `secret-sync` 是 YAML 声明的配置维护入口,不是 source delivery 或 CI/CD 恢复入口。
|
||||
|
||||
## Runtime Migration
|
||||
|
||||
```bash
|
||||
|
||||
@@ -158,6 +158,12 @@ PipelineRun 失败或长时间未完成时,先按定点 `control-plane status
|
||||
- `agentrun get|describe|events|logs|result|ack|cancel|dispatch|create|apply|send` 是当前指挥官新任务和 AgentRun session 控制入口。UniDesk CLI 是 render-only client:客户端保留 k8s 风格命令解析、human 表格、生命周期摘要、下一步命令、分页、`-o json|yaml` 稳定客户端 schema 和错误展示;AgentRun 服务端只提供稳定 RESTful API、鉴权和业务事实,不承载 UniDesk CLI 渲染。日常查看用 `get tasks --queue commander`、`describe task/<taskId>`、`events run/<runId>`、`logs session/<sessionId>`、`result run/<runId> --command <commandId>`;日常写入用 `create task --aipod Artificer --prompt-stdin`、`apply -f -`、`dispatch task/<taskId>`、`send session/<sessionId>`、`ack/cancel task|session/<id>`。用户级 CLI 取消 `turn` 和 `steer` 路径;`send session/<sessionId>` 是唯一 session follow-up 写入口,AgentRun 服务端按 durable session/run/command 状态自动决定内部 `steer` 或新 `turn`,dry-run 必须真实返回这个 decision 且不写状态。兼容 group `queue|runs|commands|runner|sessions|aipod-specs` 也走同一 direct HTTP transport,`--raw` 只披露直连 AgentRun REST envelope。
|
||||
- `agentrun explain session-policy --node <node> --lane <lane>`、`agentrun send session/<id> --node <node> --lane <lane> --dry-run` 和 `agentrun aipod-specs render <name> --node <node> --lane <lane>` 必须使用同一套目标 lane YAML 解析。非默认 lane 的短命令形态和 `--json-stdin -o json` 形态都应显示目标 `backendProfile`、`providerId`、`workspaceRef`、executionPolicy、provider credential SecretRef 和 tool credential SecretRef 摘要;Secret 输出只显示对象名、key 名、presence/fingerprint、projection 和 `valuesPrinted=false`。如果默认 human 输出触发 `/tmp/unidesk-cli-output` dump,应先把命令收敛为 summary/table/drill-down;机器完整输出留给显式 `--full`、`--raw`、`-o json` 或等价机器消费参数。`aipod-specs render` 的残余默认 dump 归 [#862](https://github.com/pikasTech/unidesk/issues/862) 跟踪。
|
||||
- `agentrun` 资源原语的默认 transport 是直连 AgentRun REST API,配置来源是 UniDesk 自有 YAML `config/agentrun.yaml`。不带 `--node`/`--lane` 时按 YAML 的默认 manager `baseUrl` 访问;显式 `--node <node> --lane <lane>` 时按同一 YAML 选中 runtime lane,经 `lane-k8s-service-proxy` 进入 manager `internalBaseUrl`,并用 manager pod env 中声明的 API key metadata 发起请求;输出只披露 node/lane/namespace/baseUrl/auth env metadata 和 `valuesPrinted=false`,不得打印 key value。该模式用于 D601 `agentrun-v02` 等非默认 lane 的资源原语操作与证据采集,尤其是 `get/describe/events/logs/result`,不替代 `agentrun control-plane ...` 发布或运维控制。鉴权可以复用 `HWLAB_API_KEY` 的环境变量/固定文件发现风格,但不得依赖 HWLAB runtime、HWLAB backend-core、HWLAB frontend 代理或 SSH official CLI;多一层转发会增加故障面,不能作为正式路径。`agentrun control-plane ...` 和 `git-mirror ...` 仍属于 G14 source/runtime 运维控制路径,可以继续使用 UniDesk SSH capture bridge;这些控制面路径不得反向成为 queue/session 资源原语的默认 transport。
|
||||
- `agentrun control-plane secret-sync --node <node> --lane <lane> --dry-run` 是 YAML SecretRef 配置维护的有界预检入口:
|
||||
- 默认文本、`-o json` 和 `-o yaml` 由同一个 compact payload 渲染;
|
||||
- payload 展示 YAML Secret ID、目标 Secret/key、source presence/fingerprint、activation fence identity/status 和关联资源 identity;
|
||||
- `--secret-id <yaml-secret-id>` 精确下钻单个声明;
|
||||
- `--full` 与 `--raw` 才展开 exhaustive probe/capture;
|
||||
- 全部输出固定 `valuesPrinted=false`,且该入口不得替代 source PR merge 的自动 CI/CD 链。
|
||||
- `agentrun cancel ... --dry-run` 必须显示 `CancelLifecycle` 摘要:transport/authority、YAML lane、cascade scope、runner abort 窗口、cancel epoch 与 late-write fencing。取消策略来自 `config/agentrun.yaml` 的 `controlPlane.lanes.<lane>.deployment.runner.cancelLifecycle`;字段缺失或 lane 选择错误应暴露为配置错误,不得在 CLI、manifest 或服务里补隐式默认。操作非默认 lane 时先加 `--node <node> --lane <lane> --dry-run` 核对 policy,再移除 `--dry-run` 发起真实取消。
|
||||
- `agentrun control-plane expose --dry-run|--confirm` 按 `config/agentrun.yaml` 维护 AgentRun 公网 HTTPS 入口,模式与 Sub2API 暴露一致:G14 AgentRun runtime 通过 frpc 出到 master `127.0.0.1:<remotePort>`,master Caddy 提供 `https://agentrun.74-48-78-17.nip.io/`。该命令只补 master `frps` allow port 和 Caddy vhost;G14 frpc Deployment/ConfigMap 必须由 AgentRun `deploy/deploy.json` + GitOps render 管理,不能在 UniDesk 侧手写 Kubernetes manifest。
|
||||
- `codex submit/enqueue`、`codex steer`、`codex resume`、`codex queue create`、`codex queue merge`、`codex move`、旧 Web 提交表单、旧队列管理和旧 workdir 管理是冻结的 legacy Code Queue 写入口。CLI 必须返回 `ok=false`、`frozen=true`、`degradedReason=legacy-code-queue-frozen` 和 AgentRun 替代命令;服务端旧 API 写入口必须返回 410。新任务、session follow-up、events/logs/result、ack 和 cancel 走 AgentRun 资源原语,其中 session follow-up 只用 `agentrun send session/<sessionId>`。
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
import { parseSecretSyncOptions } from "./agentrun/control-plane";
|
||||
import type { SecretSyncOptions } from "./agentrun/options";
|
||||
import { compactAgentRunSecretSyncPayload, renderAgentRunSecretSyncResult } from "./agentrun/secret-sync-output";
|
||||
|
||||
const options = (output: SecretSyncOptions["output"], overrides: Partial<SecretSyncOptions> = {}): SecretSyncOptions => ({
|
||||
confirm: false,
|
||||
dryRun: true,
|
||||
node: "NC01",
|
||||
lane: "nc01-v02",
|
||||
secretIds: [],
|
||||
full: false,
|
||||
raw: false,
|
||||
output,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
function blockedFixture(): Record<string, unknown> {
|
||||
const planItems = Array.from({ length: 13 }, (_, index) => {
|
||||
const id = index === 11 ? "tool-github-ssh-private-key" : index === 12 ? "tool-github-ssh-known-hosts" : `fixture-secret-${index}`;
|
||||
return {
|
||||
id,
|
||||
namespace: "agentrun-v02",
|
||||
secret: index >= 11 ? "agentrun-v01-tool-github-ssh" : `fixture-target-${index}`,
|
||||
key: index === 11 ? "id_ed25519" : index === 12 ? "known_hosts" : `key-${index}`,
|
||||
sourceMode: "file",
|
||||
sourcePath: "/root/.unidesk/.state/secrets/<redacted>",
|
||||
present: true,
|
||||
sourceFilePresent: true,
|
||||
fingerprint: `sha256:${String(index).padStart(64, "a")}`,
|
||||
valueBytes: 120 + index,
|
||||
degradedReason: null,
|
||||
valuesPrinted: false,
|
||||
};
|
||||
});
|
||||
return {
|
||||
ok: false,
|
||||
command: "agentrun control-plane secret-sync",
|
||||
mode: "dry-run-blocked-before-mutation",
|
||||
status: "blocked",
|
||||
mutation: false,
|
||||
blockedBeforeMutation: true,
|
||||
secretSyncStarted: false,
|
||||
target: { node: "NC01", lane: "nc01-v02", namespace: "agentrun-v02", valuesPrinted: false },
|
||||
degradedReason: "provider-secret-activation-fence-unavailable",
|
||||
plan: { secretCount: planItems.length, items: planItems, omittedCount: 0, valuesPrinted: false },
|
||||
activationPreflight: {
|
||||
ok: false,
|
||||
status: "blocked",
|
||||
reason: "provider-secret-activation-fence-unavailable",
|
||||
targetSecretCount: 1,
|
||||
targetSecrets: [{
|
||||
namespace: "agentrun-v02",
|
||||
name: "agentrun-v01-provider-codex",
|
||||
desiredKeys: ["auth.json", "config.toml"],
|
||||
queryOk: true,
|
||||
present: true,
|
||||
mutationCount: 1,
|
||||
noOp: false,
|
||||
mutations: [{
|
||||
id: "provider-codex-config",
|
||||
key: "config.toml",
|
||||
desiredFingerprintSuffix: "aaaaaaaaaaaa",
|
||||
runtimeFingerprintSuffix: "bbbbbbbbbbbb",
|
||||
changeKind: "change-key",
|
||||
valuesPrinted: false,
|
||||
}],
|
||||
valuesPrinted: false,
|
||||
}],
|
||||
activationRiskCount: 1,
|
||||
activationRisks: [{
|
||||
pod: "agentrun-runner-rjob_fixture-pod",
|
||||
jobName: "agentrun-runner-rjob_fixture",
|
||||
phase: "Pending",
|
||||
runId: "run_fixture",
|
||||
commandId: "cmd_fixture",
|
||||
runnerJobId: "rjob_fixture",
|
||||
waitingReasons: ["CreateContainerConfigError"],
|
||||
classification: "nonterminal-runner-required-provider-secret-missing",
|
||||
dependencies: [{
|
||||
source: "volume-secret",
|
||||
name: "agentrun-v01-provider-codex",
|
||||
affectedDesiredKeys: ["config.toml"],
|
||||
mutationKinds: ["change-key"],
|
||||
reason: "nonterminal-runner-provider-secret-mutation",
|
||||
valuesPrinted: false,
|
||||
}],
|
||||
valuesPrinted: false,
|
||||
}],
|
||||
requiresManagerFence: true,
|
||||
targetSecretObservationComplete: true,
|
||||
runnerObservationComplete: true,
|
||||
allObservationsComplete: true,
|
||||
providerSecretMutationCount: 1,
|
||||
providerSecretDataNoOp: false,
|
||||
providerSecretMutationAuthorized: false,
|
||||
managerAuthority: {
|
||||
owner: "agentrun-manager-provider-activation-fence",
|
||||
atomicFenceAvailable: false,
|
||||
mutationAuthorized: false,
|
||||
noOpOnly: true,
|
||||
trackingIssue: "https://github.com/pikasTech/agentrun/issues/284",
|
||||
valuesPrinted: false,
|
||||
},
|
||||
capture: {
|
||||
stdoutTail: "FULL_PROBE_BODY_ONLY_VISIBLE_WITH_EXPLICIT_FULL",
|
||||
valuesPrinted: false,
|
||||
},
|
||||
valuesPrinted: false,
|
||||
},
|
||||
execution: {
|
||||
providerSecretClassification: "exact-target-ref",
|
||||
providerSecretWriteBlockedCount: 2,
|
||||
secretSyncStarted: false,
|
||||
valuesPrinted: false,
|
||||
},
|
||||
next: {
|
||||
observeRunners: "bun scripts/cli.ts agentrun control-plane cleanup-runners --node NC01 --lane nc01-v02 --dry-run",
|
||||
retryAfterManagerFence: "bun scripts/cli.ts agentrun control-plane secret-sync --node NC01 --lane nc01-v02 --confirm",
|
||||
},
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
describe("AgentRun secret-sync bounded output", () => {
|
||||
test("parses compact machine output and explicit disclosure options", () => {
|
||||
expect(parseSecretSyncOptions(["--node", "NC01", "--lane", "nc01-v02", "--dry-run", "-o", "yaml"])).toEqual({
|
||||
confirm: false,
|
||||
dryRun: true,
|
||||
node: "NC01",
|
||||
lane: "nc01-v02",
|
||||
secretIds: [],
|
||||
full: false,
|
||||
raw: false,
|
||||
output: "yaml",
|
||||
});
|
||||
expect(parseSecretSyncOptions(["--node", "NC01", "--lane", "nc01-v02", "--secret-id", "tool-github-ssh-private-key", "--full", "--output=json"])).toMatchObject({
|
||||
secretIds: ["tool-github-ssh-private-key"],
|
||||
full: true,
|
||||
raw: false,
|
||||
output: "json",
|
||||
});
|
||||
expect(parseSecretSyncOptions(["--node", "NC01", "--lane", "nc01-v02", "--raw"])).toMatchObject({ full: true, raw: true });
|
||||
expect(() => parseSecretSyncOptions(["--node", "NC01", "--lane", "nc01-v02", "-o", "wide"])).toThrow("must be one of human,json,yaml");
|
||||
});
|
||||
|
||||
test("uses one bounded Secret-safe projection for human, JSON, and YAML", () => {
|
||||
const fixture = blockedFixture();
|
||||
const compact = compactAgentRunSecretSyncPayload(fixture);
|
||||
const human = renderAgentRunSecretSyncResult(fixture, options("human")).renderedText;
|
||||
const jsonText = renderAgentRunSecretSyncResult(fixture, options("json")).renderedText;
|
||||
const yamlText = renderAgentRunSecretSyncResult(fixture, options("yaml")).renderedText;
|
||||
const json = JSON.parse(jsonText) as Record<string, any>;
|
||||
const yaml = Bun.YAML.parse(yamlText) as Record<string, any>;
|
||||
|
||||
expect(json).toEqual(compact);
|
||||
expect(yaml).toEqual(json);
|
||||
expect(json.kind).toBe("AgentRunSecretSyncPlan");
|
||||
expect(json.status).toMatchObject({ status: "blocked", reason: "provider-secret-activation-fence-unavailable", mutation: false, valuesPrinted: false });
|
||||
expect(json.plan.secretCount).toBe(13);
|
||||
expect(json.plan.omittedCount).toBe(0);
|
||||
expect(json.plan.items.some((item: Record<string, unknown>) => item.id === "tool-github-ssh-private-key")).toBe(true);
|
||||
expect(json.plan.items.some((item: Record<string, unknown>) => item.id === "tool-github-ssh-known-hosts")).toBe(true);
|
||||
expect(json.activationFence).toMatchObject({
|
||||
kind: "ProviderSecretActivationFence",
|
||||
identity: "providersecretactivationfence/NC01/nc01-v02",
|
||||
status: "blocked",
|
||||
reason: "provider-secret-activation-fence-unavailable",
|
||||
resourceCount: 1,
|
||||
valuesPrinted: false,
|
||||
});
|
||||
expect(json.activationFence.resources[0]).toMatchObject({
|
||||
identity: "runnerjob/rjob_fixture",
|
||||
status: "Pending",
|
||||
runId: "run_fixture",
|
||||
commandId: "cmd_fixture",
|
||||
runnerJobId: "rjob_fixture",
|
||||
});
|
||||
expect(json.activationFence.resources[0].drillDown.map((item: Record<string, unknown>) => item.identity)).toEqual([
|
||||
"runnerjob/rjob_fixture",
|
||||
"command/cmd_fixture",
|
||||
"run/run_fixture",
|
||||
]);
|
||||
expect(json.next.secretById).toContain("--secret-id <secret-id> --dry-run -o json");
|
||||
expect(human).toContain("ACTIVATION FENCE");
|
||||
expect(human).toContain("runnerjob/rjob_fixture");
|
||||
expect(human).toContain("tool-github-ssh-private-key");
|
||||
expect(human).toContain("VALUES PRINTED false");
|
||||
for (const output of [human, jsonText, yamlText]) {
|
||||
expect(Buffer.byteLength(output, "utf8")).toBeLessThan(10_240);
|
||||
expect(output).not.toContain("FULL_PROBE_BODY_ONLY_VISIBLE_WITH_EXPLICIT_FULL");
|
||||
expect(output).not.toContain("/root/.unidesk/.state/secrets/<redacted>");
|
||||
}
|
||||
});
|
||||
|
||||
test("keeps exhaustive probe details behind explicit full or raw", () => {
|
||||
const fixture = blockedFixture();
|
||||
const full = renderAgentRunSecretSyncResult(fixture, options("json", { full: true })).renderedText;
|
||||
const raw = renderAgentRunSecretSyncResult(fixture, options("human", { full: true, raw: true })).renderedText;
|
||||
expect(JSON.parse(full)).toEqual(fixture);
|
||||
expect(JSON.parse(raw)).toEqual(fixture);
|
||||
expect(full).toContain("FULL_PROBE_BODY_ONLY_VISIBLE_WITH_EXPLICIT_FULL");
|
||||
expect(raw).toContain("FULL_PROBE_BODY_ONLY_VISIBLE_WITH_EXPLICIT_FULL");
|
||||
});
|
||||
});
|
||||
@@ -51,10 +51,36 @@ export function parseSecretSyncOptions(args: string[]): SecretSyncOptions {
|
||||
const base = parseConfirmOptions(args);
|
||||
let node: string | null = null;
|
||||
let lane: string | null = null;
|
||||
let full = false;
|
||||
let raw = false;
|
||||
let output: SecretSyncOptions["output"] = "human";
|
||||
const secretIds: string[] = [];
|
||||
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 === "-o" || arg === "--output") {
|
||||
const value = args[index + 1];
|
||||
if (value === undefined || value.startsWith("--")) throw new Error(`${arg} requires a value`);
|
||||
if (value !== "human" && value !== "json" && value !== "yaml") throw new Error(`${arg} must be one of human,json,yaml`);
|
||||
output = value;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (arg.startsWith("--output=") || arg.startsWith("-o=")) {
|
||||
const value = arg.slice(arg.indexOf("=") + 1);
|
||||
if (value !== "human" && value !== "json" && value !== "yaml") throw new Error(`${arg.split("=")[0]} must be one of human,json,yaml`);
|
||||
output = value;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--node") {
|
||||
const value = args[index + 1];
|
||||
if (value === undefined || value.startsWith("--")) throw new Error("--node requires a value");
|
||||
@@ -79,7 +105,7 @@ export function parseSecretSyncOptions(args: string[]): SecretSyncOptions {
|
||||
}
|
||||
throw new Error(`unsupported secret-sync option: ${arg}`);
|
||||
}
|
||||
return { ...base, node, lane, secretIds };
|
||||
return { ...base, node, lane, secretIds, full, raw, output };
|
||||
}
|
||||
|
||||
export function parseLaneConfirmOptions(args: string[]): LaneConfirmOptions {
|
||||
|
||||
@@ -43,6 +43,7 @@ import { providerProfileHelp, runProviderProfileCommand } from "./provider-profi
|
||||
import { renderedCliResult } from "./render";
|
||||
import { agentRunGetKindHelp, runAgentRunResourceCommand } from "./resource-actions";
|
||||
import { runAgentRunRestCompatCommand, runGitMirrorJob, startAsyncAgentRunJob } from "./rest-bridge";
|
||||
import { renderAgentRunSecretSyncResult } from "./secret-sync-output";
|
||||
import { exposeAgentRun, restartYamlLane, secretSync, triggerCurrent } from "./trigger";
|
||||
import { unsupported } from "./utils";
|
||||
import { cleanupLocalPostgres, cleanupReleasedPvs, cleanupRunners, cleanupRuns, cleanupSessionPvcs, refresh } from "./yaml-lane";
|
||||
@@ -126,7 +127,10 @@ export async function runAgentRunCommand(config: UniDeskConfig | null, args: str
|
||||
const result = await status(config, options);
|
||||
return options.full || options.raw ? result : renderAgentRunControlPlaneStatusSummary(result);
|
||||
}
|
||||
if (action === "secret-sync") return await secretSync(config, parseSecretSyncOptions(actionArgs));
|
||||
if (action === "secret-sync") {
|
||||
const options = parseSecretSyncOptions(actionArgs);
|
||||
return renderAgentRunSecretSyncResult(await secretSync(config, options), options);
|
||||
}
|
||||
if (action === "restart") {
|
||||
const options = parseLaneConfirmOptions(actionArgs);
|
||||
const result = await restartYamlLane(config, options);
|
||||
@@ -333,6 +337,7 @@ export function agentRunHelpText(args: string[]): string {
|
||||
if (kind === "platform-maintenance") {
|
||||
return [
|
||||
"Usage: bun scripts/cli.ts agentrun control-plane <apply|secret-sync|restart|expose> --node <node> --lane <lane> [--dry-run|--confirm]",
|
||||
" bun scripts/cli.ts agentrun control-plane secret-sync --node <node> --lane <lane> [--secret-id <yaml-secret-id>] --dry-run [-o json|yaml] [--full|--raw]",
|
||||
"",
|
||||
"Scope: explicit YAML-declared platform/configuration maintenance only.",
|
||||
"These commands do not replace the PaC source delivery chain and must not be used after a source PR merge to complete or recover rollout.",
|
||||
|
||||
@@ -216,10 +216,11 @@ export interface ConfirmOptions {
|
||||
dryRun: boolean;
|
||||
}
|
||||
|
||||
export interface SecretSyncOptions extends ConfirmOptions {
|
||||
export interface SecretSyncOptions extends ConfirmOptions, DisclosureOptions {
|
||||
node: string | null;
|
||||
lane: string | null;
|
||||
secretIds: string[];
|
||||
output: "human" | "json" | "yaml";
|
||||
}
|
||||
|
||||
export interface LaneConfirmOptions extends ConfirmOptions, DisclosureOptions {
|
||||
|
||||
@@ -0,0 +1,395 @@
|
||||
import type { RenderedCliResult } from "../output";
|
||||
|
||||
import type { SecretSyncOptions } from "./options";
|
||||
import { displayValue, renderTable } from "./options";
|
||||
import { record, stringOrNull } from "./utils";
|
||||
|
||||
const MAX_PLAN_ITEMS = 14;
|
||||
const MAX_TARGET_SECRETS = 4;
|
||||
const MAX_ACTIVATION_RESOURCES = 4;
|
||||
|
||||
export function renderAgentRunSecretSyncResult(result: Record<string, unknown>, options: SecretSyncOptions): RenderedCliResult {
|
||||
const command = "agentrun control-plane secret-sync";
|
||||
if (options.full || options.raw) return renderSecretSyncMachine(command, result, options.output === "yaml" && !options.raw ? "yaml" : "json");
|
||||
const payload = compactAgentRunSecretSyncPayload(result);
|
||||
if (options.output === "json" || options.output === "yaml") return renderSecretSyncMachine(command, payload, options.output);
|
||||
return {
|
||||
ok: result.ok !== false,
|
||||
command,
|
||||
renderedText: `${renderAgentRunSecretSyncText(payload)}\n`,
|
||||
contentType: "text/plain",
|
||||
};
|
||||
}
|
||||
|
||||
export function compactAgentRunSecretSyncPayload(result: Record<string, unknown>): Record<string, unknown> {
|
||||
const target = record(result.target);
|
||||
const targetNode = record(target.node);
|
||||
const identity = {
|
||||
node: boundedText(targetNode.id ?? target.node),
|
||||
lane: boundedText(target.lane),
|
||||
namespace: boundedText(target.namespace ?? record(target.runtime).namespace),
|
||||
valuesPrinted: false,
|
||||
};
|
||||
const plan = record(result.plan);
|
||||
const rawItems = records(plan.items);
|
||||
const planItems = rawItems.slice(0, MAX_PLAN_ITEMS).map((item) => compactSecretPlanItem(item, identity));
|
||||
const requestedSecretIds = record(result.filter).secretIds;
|
||||
const secretIds = Array.isArray(requestedSecretIds)
|
||||
? requestedSecretIds.map((item) => boundedText(item)).filter((item): item is string => item !== null)
|
||||
: [];
|
||||
const baseCommand = secretSyncBaseCommand(identity, secretIds);
|
||||
const activationPreflight = record(result.activationPreflight);
|
||||
return {
|
||||
apiVersion: "unidesk.pikastech.local/v1alpha1",
|
||||
kind: "AgentRunSecretSyncPlan",
|
||||
identity,
|
||||
status: {
|
||||
ok: result.ok !== false,
|
||||
status: boundedText(result.status ?? (result.ok === false ? "blocked" : "ready")),
|
||||
mode: boundedText(result.mode),
|
||||
reason: boundedText(result.degradedReason),
|
||||
blockedBeforeMutation: result.blockedBeforeMutation === true,
|
||||
secretSyncStarted: result.secretSyncStarted === true || result.mutation === true,
|
||||
mutation: result.mutation === true,
|
||||
valuesPrinted: false,
|
||||
},
|
||||
selection: {
|
||||
requestedSecretIds: secretIds,
|
||||
selectedCount: rawItems.length,
|
||||
valuesPrinted: false,
|
||||
},
|
||||
plan: {
|
||||
secretCount: numberOr(plan.secretCount, rawItems.length),
|
||||
readyCount: numberOrNull(plan.readyCount),
|
||||
unavailableCount: numberOrNull(plan.unavailableCount),
|
||||
items: planItems,
|
||||
omittedCount: Math.max(0, numberOr(plan.secretCount, rawItems.length) - planItems.length),
|
||||
valuesPrinted: false,
|
||||
},
|
||||
activationFence: Object.keys(activationPreflight).length === 0
|
||||
? null
|
||||
: compactActivationFence(activationPreflight, identity),
|
||||
execution: compactExecution(record(result.execution)),
|
||||
next: {
|
||||
secretById: `${secretSyncBaseCommand(identity, [])} --secret-id <secret-id> --dry-run -o json`,
|
||||
observeRunners: boundedText(record(result.next).observeRunners),
|
||||
retryDryRun: boundedText(record(result.next).retryDryRun),
|
||||
retryAfterManagerFence: boundedText(record(result.next).retryAfterManagerFence),
|
||||
confirm: boundedText(record(result.next).confirm),
|
||||
status: boundedText(record(result.next).status),
|
||||
full: `${baseCommand} --dry-run --full -o json`,
|
||||
raw: `${baseCommand} --dry-run --raw`,
|
||||
valuesPrinted: false,
|
||||
},
|
||||
redaction: {
|
||||
secretValues: "omitted",
|
||||
runtimeSecretValuesDecoded: false,
|
||||
valuesPrinted: false,
|
||||
},
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function renderAgentRunSecretSyncText(payload: Record<string, unknown>): string {
|
||||
const identity = record(payload.identity);
|
||||
const status = record(payload.status);
|
||||
const plan = record(payload.plan);
|
||||
const planItems = records(plan.items);
|
||||
const fence = record(payload.activationFence);
|
||||
const next = record(payload.next);
|
||||
const lines = [
|
||||
"AGENTRUN SECRET SYNC",
|
||||
renderTable(
|
||||
["NODE", "LANE", "NAMESPACE", "STATUS", "MODE", "MUTATION", "VALUES"],
|
||||
[[
|
||||
displayValue(identity.node),
|
||||
displayValue(identity.lane),
|
||||
displayValue(identity.namespace),
|
||||
displayValue(status.status),
|
||||
displayValue(status.mode),
|
||||
String(status.mutation === true),
|
||||
"false",
|
||||
]],
|
||||
),
|
||||
`Reason: ${displayValue(status.reason)}`,
|
||||
"",
|
||||
`SECRET PLAN count=${displayValue(plan.secretCount)} omitted=${displayValue(plan.omittedCount)}`,
|
||||
planItems.length === 0
|
||||
? "-"
|
||||
: renderTable(["ID", "TARGET", "KEY", "SOURCE", "PRESENT", "FINGERPRINT", "STATUS"], planItems.map((item) => {
|
||||
const source = record(item.source);
|
||||
const target = record(item.target);
|
||||
return [
|
||||
displayValue(item.id),
|
||||
displayValue(target.identity),
|
||||
displayValue(target.key),
|
||||
displayValue(source.mode),
|
||||
displayValue(source.present),
|
||||
shortFingerprint(source.fingerprint),
|
||||
source.present === true ? "ready" : "unavailable",
|
||||
];
|
||||
})),
|
||||
];
|
||||
if (Object.keys(fence).length > 0) {
|
||||
const authority = record(fence.authority);
|
||||
const observation = record(fence.observation);
|
||||
const targetSecrets = records(fence.targetSecrets);
|
||||
const resources = records(fence.resources);
|
||||
lines.push(
|
||||
"",
|
||||
"ACTIVATION FENCE",
|
||||
renderTable(["IDENTITY", "TYPE", "STATUS", "REASON", "RISKS", "ATOMIC"], [[
|
||||
displayValue(fence.identity),
|
||||
displayValue(fence.type),
|
||||
displayValue(fence.status),
|
||||
displayValue(fence.reason),
|
||||
displayValue(fence.resourceCount),
|
||||
displayValue(authority.atomicFenceAvailable),
|
||||
]]),
|
||||
`Observation: targets=${displayValue(observation.targetSecretObservationComplete)} runners=${displayValue(observation.runnerObservationComplete)} mutations=${displayValue(observation.providerSecretMutationCount)} noOp=${displayValue(observation.providerSecretDataNoOp)}`,
|
||||
);
|
||||
if (targetSecrets.length > 0) {
|
||||
lines.push(
|
||||
"",
|
||||
"FENCE TARGET SECRETS",
|
||||
renderTable(["IDENTITY", "KEYS", "PRESENT", "STATUS", "MUTATIONS"], targetSecrets.map((item) => [
|
||||
displayValue(item.identity),
|
||||
stringList(item.desiredKeys),
|
||||
displayValue(item.present),
|
||||
displayValue(item.status),
|
||||
displayValue(item.mutationCount),
|
||||
])),
|
||||
);
|
||||
}
|
||||
if (resources.length > 0) {
|
||||
lines.push(
|
||||
"",
|
||||
"FENCE RESOURCES",
|
||||
renderTable(["IDENTITY", "STATUS", "RUN", "COMMAND", "CLASSIFICATION"], resources.map((item) => [
|
||||
displayValue(item.identity),
|
||||
displayValue(item.status),
|
||||
displayValue(item.runId),
|
||||
displayValue(item.commandId),
|
||||
displayValue(item.classification),
|
||||
])),
|
||||
...resources.flatMap((item) => records(item.drillDown).map((entry) => ` ${displayValue(item.identity)}: ${displayValue(entry.command)}`)),
|
||||
);
|
||||
}
|
||||
}
|
||||
const nextLines = ["secretById", "observeRunners", "retryDryRun", "retryAfterManagerFence", "confirm", "status", "full", "raw"]
|
||||
.map((key) => boundedText(next[key]))
|
||||
.filter((value): value is string => value !== null);
|
||||
lines.push("", "NEXT", ...nextLines.map((line) => ` ${line}`), "", "VALUES PRINTED false");
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function compactSecretPlanItem(item: Record<string, unknown>, identity: Record<string, unknown>): Record<string, unknown> {
|
||||
const id = boundedText(item.id);
|
||||
const namespace = boundedText(item.namespace);
|
||||
const secret = boundedText(item.secret);
|
||||
const key = boundedText(item.key);
|
||||
void identity;
|
||||
return {
|
||||
id,
|
||||
target: {
|
||||
identity: namespace === null || secret === null ? null : `secret/${namespace}/${secret}`,
|
||||
key,
|
||||
},
|
||||
source: {
|
||||
mode: boundedText(item.sourceMode),
|
||||
present: item.present === true,
|
||||
fingerprint: boundedText(item.fingerprint, 96),
|
||||
...(boundedText(item.degradedReason) === null ? {} : { reason: boundedText(item.degradedReason) }),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function compactActivationFence(preflight: Record<string, unknown>, identity: Record<string, unknown>): Record<string, unknown> {
|
||||
const authority = record(preflight.managerAuthority);
|
||||
const nestedAuthority = record(preflight.activationAuthority);
|
||||
const targetSecrets = records(preflight.targetSecrets).slice(0, MAX_TARGET_SECRETS).map(compactFenceTargetSecret);
|
||||
const rawRisks = records(preflight.activationRisks);
|
||||
const resources = rawRisks.slice(0, MAX_ACTIVATION_RESOURCES).map((risk, index) => compactFenceResource(risk, identity, index));
|
||||
return {
|
||||
apiVersion: "unidesk.pikastech.local/v1alpha1",
|
||||
kind: "ProviderSecretActivationFence",
|
||||
identity: `providersecretactivationfence/${displayValue(identity.node)}/${displayValue(identity.lane)}`,
|
||||
type: "provider-secret-activation",
|
||||
status: boundedText(preflight.status ?? (preflight.ok === true ? "ready" : "blocked")),
|
||||
reason: boundedText(preflight.reason),
|
||||
requiresManagerFence: preflight.requiresManagerFence === true,
|
||||
authority: {
|
||||
owner: boundedText(authority.owner ?? nestedAuthority.owner),
|
||||
atomicFenceAvailable: authority.atomicFenceAvailable === true || nestedAuthority.atomicFenceAvailable === true,
|
||||
mutationAuthorized: authority.mutationAuthorized === true || preflight.providerSecretMutationAuthorized === true,
|
||||
noOpOnly: authority.noOpOnly !== false && nestedAuthority.noOpOnly !== false,
|
||||
trackingIssue: boundedText(authority.trackingIssue),
|
||||
},
|
||||
observation: {
|
||||
targetSecretObservationComplete: preflight.targetSecretObservationComplete === true,
|
||||
runnerObservationComplete: preflight.runnerObservationComplete === true,
|
||||
allObservationsComplete: preflight.allObservationsComplete === true,
|
||||
providerSecretMutationCount: numberOrNull(preflight.providerSecretMutationCount),
|
||||
providerSecretDataNoOp: preflight.providerSecretDataNoOp === true,
|
||||
},
|
||||
targetSecretCount: numberOr(preflight.targetSecretCount, targetSecrets.length),
|
||||
targetSecrets,
|
||||
targetSecretOmittedCount: Math.max(0, numberOr(preflight.targetSecretCount, targetSecrets.length) - targetSecrets.length),
|
||||
resourceCount: numberOr(preflight.activationRiskCount, rawRisks.length),
|
||||
resources,
|
||||
resourceOmittedCount: Math.max(0, numberOr(preflight.activationRiskCount, rawRisks.length) - resources.length),
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
function compactFenceTargetSecret(item: Record<string, unknown>): Record<string, unknown> {
|
||||
const namespace = boundedText(item.namespace);
|
||||
const name = boundedText(item.name);
|
||||
const mutations = records(item.mutations).slice(0, 6).map((mutation) => {
|
||||
const id = boundedText(mutation.id);
|
||||
return {
|
||||
id,
|
||||
key: boundedText(mutation.key),
|
||||
changeKind: boundedText(mutation.changeKind),
|
||||
desiredFingerprintSuffix: boundedText(mutation.desiredFingerprintSuffix ?? mutation.desiredEncodedFingerprint, 24)?.slice(-12) ?? null,
|
||||
runtimeFingerprintSuffix: boundedText(mutation.runtimeFingerprintSuffix ?? mutation.runtimeEncodedFingerprint, 24)?.slice(-12) ?? null,
|
||||
};
|
||||
});
|
||||
const queryOk = item.queryOk === true;
|
||||
const mutationCount = numberOr(item.mutationCount, mutations.length);
|
||||
return {
|
||||
kind: "Secret",
|
||||
identity: namespace === null || name === null ? null : `secret/${namespace}/${name}`,
|
||||
namespace,
|
||||
name,
|
||||
desiredKeys: stringArray(item.desiredKeys, 8),
|
||||
queryOk,
|
||||
present: typeof item.present === "boolean" ? item.present : null,
|
||||
status: !queryOk ? "observation-incomplete" : mutationCount === 0 ? "ready-no-op" : "mutation-pending",
|
||||
mutationCount,
|
||||
mutations,
|
||||
};
|
||||
}
|
||||
|
||||
function compactFenceResource(risk: Record<string, unknown>, identity: Record<string, unknown>, index: number): Record<string, unknown> {
|
||||
const namespace = boundedText(identity.namespace);
|
||||
const runId = boundedText(risk.runId);
|
||||
const commandId = boundedText(risk.commandId);
|
||||
const runnerJobId = boundedText(risk.runnerJobId);
|
||||
const jobName = boundedText(risk.jobName);
|
||||
const pod = boundedText(risk.pod);
|
||||
const resourceIdentity = runnerJobId !== null
|
||||
? `runnerjob/${runnerJobId}`
|
||||
: jobName !== null && namespace !== null
|
||||
? `job/${namespace}/${jobName}`
|
||||
: pod !== null && namespace !== null
|
||||
? `pod/${namespace}/${pod}`
|
||||
: `activationrisk/${index + 1}`;
|
||||
const drillDown = fenceResourceDrillDown(identity, { runId, commandId, runnerJobId });
|
||||
return {
|
||||
identity: resourceIdentity,
|
||||
status: boundedText(risk.phase),
|
||||
classification: boundedText(risk.classification),
|
||||
runId,
|
||||
commandId,
|
||||
runnerJobId,
|
||||
waitingReasons: stringArray(risk.waitingReasons, 4),
|
||||
dependencies: records(risk.dependencies).slice(0, 3).map((dependency) => ({
|
||||
identity: boundedText(dependency.name) === null || namespace === null ? null : `secret/${namespace}/${boundedText(dependency.name)}`,
|
||||
source: boundedText(dependency.source),
|
||||
affectedKeys: stringArray(dependency.affectedDesiredKeys, 6),
|
||||
mutationKinds: stringArray(dependency.mutationKinds, 4),
|
||||
reason: boundedText(dependency.reason),
|
||||
})),
|
||||
drillDown,
|
||||
};
|
||||
}
|
||||
|
||||
function fenceResourceDrillDown(
|
||||
identity: Record<string, unknown>,
|
||||
resource: { runId: string | null; commandId: string | null; runnerJobId: string | null },
|
||||
): Record<string, unknown>[] {
|
||||
const nodeLane = nodeLaneArgs(identity);
|
||||
const result: Record<string, unknown>[] = [];
|
||||
if (resource.runId !== null && resource.runnerJobId !== null) {
|
||||
result.push({ kind: "RunnerJob", identity: `runnerjob/${resource.runnerJobId}`, command: `bun scripts/cli.ts agentrun describe runnerjob/${resource.runnerJobId} --run ${resource.runId}${nodeLane}` });
|
||||
}
|
||||
if (resource.runId !== null && resource.commandId !== null) {
|
||||
result.push({ kind: "Command", identity: `command/${resource.commandId}`, command: `bun scripts/cli.ts agentrun describe command/${resource.commandId} --run ${resource.runId}${nodeLane}` });
|
||||
}
|
||||
if (resource.runId !== null) {
|
||||
result.push({ kind: "Run", identity: `run/${resource.runId}`, command: `bun scripts/cli.ts agentrun events run/${resource.runId} --after-seq 0 --limit 100${nodeLane}` });
|
||||
}
|
||||
return result.slice(0, 3);
|
||||
}
|
||||
|
||||
function compactExecution(value: Record<string, unknown>): Record<string, unknown> {
|
||||
return {
|
||||
providerSecretClassification: boundedText(value.providerSecretClassification),
|
||||
providerSecretWritePolicy: boundedText(value.providerSecretWritePolicy),
|
||||
providerSecretWriteBlockedCount: numberOrNull(value.providerSecretWriteBlockedCount),
|
||||
providerSecretWriteSkippedCount: numberOrNull(value.providerSecretWriteSkippedCount),
|
||||
writableNonProviderSecretCount: numberOrNull(value.writableNonProviderSecretCount),
|
||||
secretSyncStarted: value.secretSyncStarted === true,
|
||||
};
|
||||
}
|
||||
|
||||
function secretSyncBaseCommand(identity: Record<string, unknown>, secretIds: readonly string[]): string {
|
||||
const node = boundedText(identity.node) ?? "<node>";
|
||||
const lane = boundedText(identity.lane) ?? "<lane>";
|
||||
return [
|
||||
"bun scripts/cli.ts agentrun control-plane secret-sync",
|
||||
`--node ${node}`,
|
||||
`--lane ${lane}`,
|
||||
...secretIds.map((id) => `--secret-id ${id}`),
|
||||
].join(" ");
|
||||
}
|
||||
|
||||
function nodeLaneArgs(identity: Record<string, unknown>): string {
|
||||
const node = boundedText(identity.node);
|
||||
const lane = boundedText(identity.lane);
|
||||
return node === null || lane === null ? "" : ` --node ${node} --lane ${lane}`;
|
||||
}
|
||||
|
||||
function renderSecretSyncMachine(command: string, value: unknown, mode: "json" | "yaml"): RenderedCliResult {
|
||||
return {
|
||||
ok: record(value).ok !== false && record(record(value).status).ok !== false,
|
||||
command,
|
||||
renderedText: mode === "json" ? `${JSON.stringify(value)}\n` : `${Bun.YAML.stringify(value)}\n`,
|
||||
contentType: mode === "json" ? "application/json" : "application/yaml",
|
||||
};
|
||||
}
|
||||
|
||||
function records(value: unknown): Record<string, unknown>[] {
|
||||
return Array.isArray(value) ? value.map(record).filter((item) => Object.keys(item).length > 0) : [];
|
||||
}
|
||||
|
||||
function boundedText(value: unknown, max = 240): string | null {
|
||||
const text = stringOrNull(value);
|
||||
if (text === null) return null;
|
||||
const oneLine = text.replace(/\s+/gu, " ").trim();
|
||||
return oneLine.length <= max ? oneLine : `${oneLine.slice(0, Math.max(0, max - 3))}...`;
|
||||
}
|
||||
|
||||
function stringArray(value: unknown, max: number): string[] {
|
||||
if (!Array.isArray(value)) return [];
|
||||
return value.map((item) => boundedText(item, 120)).filter((item): item is string => item !== null).slice(0, max);
|
||||
}
|
||||
|
||||
function numberOr(value: unknown, fallback: number): number {
|
||||
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
||||
}
|
||||
|
||||
function numberOrNull(value: unknown): number | null {
|
||||
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
||||
}
|
||||
|
||||
function shortFingerprint(value: unknown): string {
|
||||
const fingerprint = boundedText(value, 96);
|
||||
if (fingerprint === null) return "-";
|
||||
return fingerprint.length <= 20 ? fingerprint : `${fingerprint.slice(0, 7)}…${fingerprint.slice(-12)}`;
|
||||
}
|
||||
|
||||
function stringList(value: unknown): string {
|
||||
return Array.isArray(value) && value.length > 0 ? value.map(String).join(",") : "-";
|
||||
}
|
||||
@@ -298,17 +298,8 @@ export async function secretSync(config: UniDeskConfig, options: SecretSyncOptio
|
||||
filter: options.secretIds.length === 0 ? null : { secretIds: options.secretIds, selected: sources.map((source) => source.id) },
|
||||
plan: {
|
||||
secretCount: plan.length,
|
||||
items: plan.slice(0, 8).map((item) => ({
|
||||
id: item.id,
|
||||
namespace: item.namespace,
|
||||
secret: item.secret,
|
||||
key: item.key,
|
||||
present: item.present,
|
||||
fingerprint: item.fingerprint,
|
||||
valueBytes: item.valueBytes,
|
||||
valuesPrinted: false,
|
||||
})),
|
||||
omittedCount: Math.max(0, plan.length - 8),
|
||||
items: plan,
|
||||
omittedCount: 0,
|
||||
valuesPrinted: false,
|
||||
},
|
||||
activationPreflight,
|
||||
|
||||
Reference in New Issue
Block a user