fix(cicd): harden PaC source artifact verification
This commit is contained in:
@@ -108,6 +108,7 @@ export interface AgentRunLaneSpec {
|
||||
readonly source: {
|
||||
readonly statusMode: "host-worktree" | "k3s-git-mirror";
|
||||
readonly repository: string;
|
||||
readonly worktreeRemote: string;
|
||||
readonly branch: string;
|
||||
readonly sourceAuthority: AgentRunSourceAuthoritySpec | null;
|
||||
readonly sourceSnapshot: AgentRunSourceSnapshotSpec | null;
|
||||
@@ -595,6 +596,7 @@ function parseLane(lane: string, node: AgentRunNodeSpec, input: Record<string, u
|
||||
source: {
|
||||
statusMode,
|
||||
repository: stringField(source, "repository", `${path}.source`),
|
||||
worktreeRemote: stringField(source, "worktreeRemote", `${path}.source`),
|
||||
branch: stringField(source, "branch", `${path}.source`),
|
||||
sourceAuthority: sourceAuthorityConfig(source.sourceAuthority, `${path}.source.sourceAuthority`),
|
||||
sourceSnapshot: sourceSnapshotConfig(source.sourceSnapshot, `${path}.source.sourceSnapshot`),
|
||||
|
||||
@@ -7,12 +7,22 @@ import { resolve } from "node:path";
|
||||
import { rootPath } from "./config";
|
||||
import { applyNodeRuntimeDeployYamlOverlay, nodeRuntimeDeployYamlOverlayShellScript } from "./hwlab-node/deploy-overlay";
|
||||
import {
|
||||
assertPacSourceArtifactVerifyWorktree,
|
||||
canonicalSha256,
|
||||
canonicalizePacPipelineSpec,
|
||||
assertHwlabPipelineContracts,
|
||||
firstPacSourceArtifactDrift,
|
||||
pacValueEvidence,
|
||||
pacSourceArtifactYaml,
|
||||
parsePacSourceArtifactOptions,
|
||||
sourceArtifactRuntimeAlignment,
|
||||
} from "./platform-infra-pipelines-as-code-source-artifact";
|
||||
import { renderPacSourceArtifactRuntimeObserverShell } from "./platform-infra-pipelines-as-code";
|
||||
import {
|
||||
pacRuntimeSafeReason,
|
||||
pacRuntimeSafePath,
|
||||
pacRuntimeSourceArtifactItem,
|
||||
pacRuntimeTransportFailureReason,
|
||||
renderPacSourceArtifactRuntimeObserverShell,
|
||||
} from "./platform-infra-pipelines-as-code";
|
||||
import {
|
||||
canonicalPacPipelineSha256 as nativeCanonicalSha256,
|
||||
canonicalizePacPipelineSpec as nativeCanonicalize,
|
||||
@@ -25,14 +35,24 @@ const provenance = {
|
||||
configRef: "config/example.yaml#lanes.nc01",
|
||||
effectiveConfigSha256: "sha256:config",
|
||||
renderer: "agentrun-control-plane",
|
||||
mode: "embedded-pipeline-spec",
|
||||
};
|
||||
const desiredSpec = { params: [], tasks: [] };
|
||||
const runtimeWrapperSpec = {
|
||||
taskRunTemplate: {
|
||||
serviceAccountName: "agentrun-nc01-v02-tekton-runner",
|
||||
podTemplate: { hostNetwork: true, dnsPolicy: "ClusterFirstWithHostNet", securityContext: { fsGroup: 1000 } },
|
||||
},
|
||||
params: [],
|
||||
workspaces: [],
|
||||
};
|
||||
|
||||
function manifestAnnotations(): Record<string, string> {
|
||||
return {
|
||||
"unidesk.ai/owning-config-ref": provenance.configRef,
|
||||
"unidesk.ai/effective-config-sha256": provenance.effectiveConfigSha256,
|
||||
"unidesk.ai/source-artifact-renderer": provenance.renderer,
|
||||
"unidesk.ai/source-artifact-mode": provenance.mode,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -51,7 +71,7 @@ function pipelineRun(name: string, creationTimestamp: string, spec: Record<strin
|
||||
labels: { "pipelinesascode.tekton.dev/sha": sourceCommit },
|
||||
annotations: manifestAnnotations(),
|
||||
},
|
||||
spec: { pipelineSpec: spec },
|
||||
spec: { pipelineSpec: spec, ...structuredClone(runtimeWrapperSpec) },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -61,6 +81,7 @@ function runtimeInput(commit: string | null = sourceCommit): Record<string, unkn
|
||||
pipeline: "agentrun-nc01-v02-ci-image-publish",
|
||||
pipelineRunPrefix: "agentrun-nc01-v02-ci",
|
||||
desiredSpec,
|
||||
desiredWrapper: { mode: "embedded-pipeline-spec", executionMode: "pipeline-spec", spec: runtimeWrapperSpec },
|
||||
provenance,
|
||||
sourceCommit: commit,
|
||||
};
|
||||
@@ -70,7 +91,7 @@ describe("PaC source artifact CLI contract", () => {
|
||||
test("verify-runtime requires and normalizes a full source commit", () => {
|
||||
expect(() => parsePacSourceArtifactOptions([
|
||||
"verify-runtime", "--target", "NC01", "--consumer", "agentrun-nc01-v02", "--source-worktree", "/tmp/repo",
|
||||
])).toThrow("requires explicit --source-commit");
|
||||
])).toThrow("source-commit-required");
|
||||
const parsed = parsePacSourceArtifactOptions([
|
||||
"verify-runtime", "--target", "NC01", "--consumer", "agentrun-nc01-v02", "--source-worktree", "/tmp/repo", "--source-commit", sourceCommit.toUpperCase(),
|
||||
]);
|
||||
@@ -100,20 +121,87 @@ describe("PaC source artifact CLI contract", () => {
|
||||
"--target", "NC01", "--consumer", "agentrun-nc01-v02", "--source-worktree", rootPath(),
|
||||
], { cwd: rootPath(), encoding: "utf8", timeout: 30_000 });
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.stdout).toContain("origin does not match PaC repository pikasTech-agentrun");
|
||||
expect(result.stdout).toContain("pikasTech/unidesk.git");
|
||||
expect(result.stdout).toContain("code=source-worktree-origin-mismatch");
|
||||
expect(result.stdout).toContain("observedUrlFingerprint=sha256:");
|
||||
expect(result.stdout).not.toContain("observedOwnerRepo");
|
||||
expect(result.stdout).not.toContain("stack");
|
||||
expect(result.stdout).not.toContain("taskSpec");
|
||||
expect(result.stdout.length).toBeLessThan(1500);
|
||||
});
|
||||
|
||||
test("default, JSON, and full failures never disclose a malicious origin", () => {
|
||||
const temporary = mkdtempSync(resolve(tmpdir(), "unidesk-pac-origin-test-"));
|
||||
const secrets = [
|
||||
"review-user",
|
||||
"REVIEW_SUPER_SECRET",
|
||||
"QUERY_SUPER_SECRET",
|
||||
"Authorization",
|
||||
"https://review-user:REVIEW_SUPER_SECRET@example.invalid/pikasTech/agentrun.git?token=QUERY_SUPER_SECRET",
|
||||
];
|
||||
try {
|
||||
for (const args of [
|
||||
["init"],
|
||||
["config", "user.email", "pac-test@example.invalid"],
|
||||
["config", "user.name", "PaC Test"],
|
||||
]) expect(spawnSync("git", args, { cwd: temporary, encoding: "utf8" }).status).toBe(0);
|
||||
writeFileSync(resolve(temporary, "README.md"), "fixture\n");
|
||||
expect(spawnSync("git", ["add", "README.md"], { cwd: temporary, encoding: "utf8" }).status).toBe(0);
|
||||
expect(spawnSync("git", ["commit", "-m", "fixture"], { cwd: temporary, encoding: "utf8" }).status).toBe(0);
|
||||
expect(spawnSync("git", ["remote", "add", "origin", secrets[4] as string], { cwd: temporary, encoding: "utf8" }).status).toBe(0);
|
||||
const origins = [
|
||||
secrets[4] as string,
|
||||
"audit@example.invalid:pikasTech/agentrun.git?token=SCP_QUERY_SUPER_SECRET",
|
||||
"https://review-user:REVIEW_SUPER_SECRET@github.com/pikasTech/agentrun.git",
|
||||
"https://github.com/pikasTech/agentrun.git?token=QUERY_SUPER_SECRET",
|
||||
"https://github.com/pikasTech/agentrun.git#QUERY_SUPER_SECRET",
|
||||
];
|
||||
for (const origin of origins) {
|
||||
expect(spawnSync("git", ["remote", "set-url", "origin", origin], { cwd: temporary, encoding: "utf8" }).status).toBe(0);
|
||||
for (const outputFlag of [null, "--json", "--full"] as const) {
|
||||
const args = [
|
||||
"scripts/cli.ts", "platform-infra", "pipelines-as-code", "source-artifact", "plan",
|
||||
"--target", "NC01", "--consumer", "agentrun-nc01-v02", "--source-worktree", temporary,
|
||||
];
|
||||
if (outputFlag !== null) args.push(outputFlag);
|
||||
const result = spawnSync("bun", args, { cwd: rootPath(), encoding: "utf8", timeout: 30_000 });
|
||||
const output = `${result.stdout}\n${result.stderr}`;
|
||||
expect(result.status).toBe(1);
|
||||
expect(output).toContain("source-worktree-origin-mismatch");
|
||||
expect(output).toContain("observedUrlFingerprint");
|
||||
expect(output).not.toContain("observedOwnerRepo");
|
||||
for (const secret of [...secrets, "SCP_QUERY_SUPER_SECRET"]) expect(output).not.toContain(secret);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
rmSync(temporary, { recursive: true, force: true });
|
||||
}
|
||||
}, 15_000);
|
||||
|
||||
test("verification requires a clean worktree at the exact requested commit", () => {
|
||||
const exact = { head: sourceCommit, clean: true, matchesSourceCommit: true };
|
||||
expect(() => assertPacSourceArtifactVerifyWorktree("verify-runtime", sourceCommit, exact)).not.toThrow();
|
||||
expect(() => assertPacSourceArtifactVerifyWorktree("status", sourceCommit, { ...exact, clean: false })).not.toThrow();
|
||||
expect(() => assertPacSourceArtifactVerifyWorktree("verify-runtime", sourceCommit, { ...exact, clean: false })).toThrow("source-worktree-not-clean");
|
||||
expect(() => assertPacSourceArtifactVerifyWorktree("verify-runtime", sourceCommit, { ...exact, head: "b".repeat(40), matchesSourceCommit: false })).toThrow("source-worktree-commit-mismatch");
|
||||
});
|
||||
|
||||
test("taskRunTemplate operational facts are declared in owning YAML", () => {
|
||||
const document = Bun.YAML.parse(readFileSync(rootPath("config", "platform-infra", "pipelines-as-code.yaml"), "utf8")) as Record<string, any>;
|
||||
const consumers = document.consumers as Array<Record<string, any>>;
|
||||
for (const template of ["templates.consumers.agentrunV02", "templates.consumers.hwlabV03"]) {
|
||||
const sourceArtifact = consumers.find((item) => item.extends === template && item.variables?.NODE === "NC01")?.sourceArtifact;
|
||||
expect(sourceArtifact?.taskRunTemplate).toEqual({ hostNetwork: true, dnsPolicy: "ClusterFirstWithHostNet", fsGroup: 1000 });
|
||||
}
|
||||
});
|
||||
|
||||
test("undeclared JD01 source artifact owner fails closed", () => {
|
||||
const result = spawnSync("bun", [
|
||||
"scripts/cli.ts", "platform-infra", "pipelines-as-code", "source-artifact", "plan",
|
||||
"--target", "JD01", "--consumer", "agentrun-jd01-v02", "--source-worktree", rootPath(),
|
||||
], { cwd: rootPath(), encoding: "utf8", timeout: 30_000 });
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.stdout).toContain("has no sourceArtifact owner");
|
||||
expect(result.stdout).toContain("code=source-artifact-operation-failed");
|
||||
expect(result.stdout).toContain("evidence=type=string");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -167,6 +255,23 @@ describe("Tekton admission canonicalization", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("source artifact serialization", () => {
|
||||
test("remote Pipeline artifacts are deterministic reviewable multi-line YAML", () => {
|
||||
const manifest = {
|
||||
apiVersion: "tekton.dev/v1",
|
||||
kind: "Pipeline",
|
||||
metadata: { name: "fixture" },
|
||||
spec: { params: [{ name: "revision", type: "string" }], tasks: [{ name: "build", taskSpec: { steps: [{ name: "build", script: "set -eu\necho ok\n" }] } }] },
|
||||
};
|
||||
const first = pacSourceArtifactYaml(manifest);
|
||||
const second = pacSourceArtifactYaml(manifest);
|
||||
expect(first).toBe(second);
|
||||
expect(first.split("\n").length).toBeGreaterThan(10);
|
||||
expect(first).toContain("apiVersion: tekton.dev/v1\nkind: Pipeline\n");
|
||||
expect(Bun.YAML.parse(first)).toEqual(manifest);
|
||||
});
|
||||
});
|
||||
|
||||
describe("runtime source-commit selection", () => {
|
||||
test("same-commit retries select the newest only when spec and provenance agree", () => {
|
||||
const oldRun = pipelineRun("agentrun-nc01-v02-ci-old", "2026-07-10T00:00:00Z");
|
||||
@@ -195,6 +300,51 @@ describe("runtime source-commit selection", () => {
|
||||
expect(observed.embedded.candidateCount).toBe(2);
|
||||
});
|
||||
|
||||
test("embedded mode compares the complete PipelineRun wrapper", () => {
|
||||
const run = pipelineRun("agentrun-nc01-v02-ci-wrapper", "2026-07-11T00:00:00Z");
|
||||
(run.spec as Record<string, any>).taskRunTemplate.serviceAccountName = "wrong-service-account";
|
||||
const observed = observePacSourceArtifactRuntime(runtimeInput(), (_args: string[], label: string) => label === "pipeline-get"
|
||||
? { kind: "ok", value: pipeline() }
|
||||
: { kind: "ok", value: { items: [run] } });
|
||||
expect(observed.embedded.status).toBe("drifted");
|
||||
expect(observed.embedded.firstDrift?.path).toBe("$.spec.taskRunTemplate.serviceAccountName");
|
||||
});
|
||||
|
||||
test("remote mode validates Pipeline spec live and wrapper on the exact-commit Run", () => {
|
||||
const remoteProvenance = { ...provenance, mode: "remote-pipeline-annotation" };
|
||||
const remoteAnnotations = {
|
||||
...manifestAnnotations(),
|
||||
"unidesk.ai/source-artifact-mode": "remote-pipeline-annotation",
|
||||
};
|
||||
const wrapper = {
|
||||
mode: "remote-pipeline-annotation",
|
||||
executionMode: "pipeline-spec",
|
||||
spec: { ...runtimeWrapperSpec, timeouts: { pipeline: "1h0m0s" } },
|
||||
};
|
||||
const run = {
|
||||
metadata: {
|
||||
name: "hwlab-nc01-v03-ci-poll-remote",
|
||||
creationTimestamp: "2026-07-11T00:00:00Z",
|
||||
labels: { "pipelinesascode.tekton.dev/sha": sourceCommit },
|
||||
annotations: remoteAnnotations,
|
||||
},
|
||||
spec: { pipelineSpec: desiredSpec, ...wrapper.spec },
|
||||
};
|
||||
const input = {
|
||||
...runtimeInput(),
|
||||
pipeline: "hwlab-nc01-v03-ci-image-publish",
|
||||
pipelineRunPrefix: "hwlab-nc01-v03-ci-poll",
|
||||
provenance: remoteProvenance,
|
||||
desiredWrapper: wrapper,
|
||||
};
|
||||
const observed = observePacSourceArtifactRuntime(input, (_args: string[], label: string) => label === "pipeline-get"
|
||||
? { kind: "ok", value: { metadata: { name: input.pipeline, annotations: remoteAnnotations }, spec: desiredSpec } }
|
||||
: { kind: "ok", value: { items: [run] } });
|
||||
expect(observed.live.status).toBe("aligned");
|
||||
expect(observed.embedded.status).toBe("aligned");
|
||||
expect(observed.embedded.firstDrift).toBeNull();
|
||||
});
|
||||
|
||||
test("NotFound, missing commit, and transport failures remain distinct", () => {
|
||||
const noCommit = observePacSourceArtifactRuntime(runtimeInput(null), () => ({ kind: "not-found", reason: "pipeline-get-not-found" }));
|
||||
expect(noCommit.live.status).toBe("missing");
|
||||
@@ -202,14 +352,170 @@ describe("runtime source-commit selection", () => {
|
||||
expect(noCommit.embedded.status).toBe("unavailable");
|
||||
expect(noCommit.embedded.reason).toBe("source-commit-not-specified");
|
||||
|
||||
const transport = observePacSourceArtifactRuntime(runtimeInput(), (_args: string[], label: string) => ({ kind: "unavailable", reason: `${label}-command-failed:126:permission denied` }));
|
||||
const secret = "Authorization: Bearer REVIEW_RUNTIME_SECRET https://review:password@example.invalid/path?token=QUERY_SECRET";
|
||||
const transport = observePacSourceArtifactRuntime(runtimeInput(), () => ({ kind: "unavailable", reason: secret }));
|
||||
expect(transport.live.status).toBe("unavailable");
|
||||
expect(transport.embedded.status).toBe("unavailable");
|
||||
expect(transport.live.reason).toContain("permission denied");
|
||||
expect(transport.embedded.reason).toContain("permission denied");
|
||||
expect(transport.live.reason).toMatch(/^runtime-observer-reason:type=string,length=\d+,sha256=[0-9a-f]{64}$/u);
|
||||
expect(transport.embedded.reason).toMatch(/^runtime-observer-reason:type=string,length=\d+,sha256=[0-9a-f]{64}$/u);
|
||||
expect(JSON.stringify(transport)).not.toContain(secret);
|
||||
});
|
||||
|
||||
test("first drift reports structural evidence without script or token values", () => {
|
||||
const expectedSecret = "Authorization: Bearer EXPECTED_SUPER_SECRET";
|
||||
const actualSecret = "https://review-user:ACTUAL_SUPER_SECRET@example.invalid/path?token=QUERY_SUPER_SECRET";
|
||||
const input = {
|
||||
...runtimeInput(),
|
||||
desiredSpec: { tasks: [{ taskSpec: { steps: [{ script: expectedSecret }] } }] },
|
||||
};
|
||||
const observed = observePacSourceArtifactRuntime(input, (_args: string[], label: string) => label === "pipeline-get"
|
||||
? { kind: "ok", value: { metadata: { name: "pipeline", annotations: manifestAnnotations() }, spec: { tasks: [{ taskSpec: { steps: [{ script: actualSecret }] } }] } } }
|
||||
: { kind: "ok", value: { items: [pipelineRun("agentrun-nc01-v02-ci-secret", "2026-07-11T00:00:00Z", { tasks: [{ taskSpec: { steps: [{ script: actualSecret }] } }] })] } });
|
||||
const serialized = JSON.stringify(observed);
|
||||
expect(observed.live.firstDrift?.path).toBe("$.tasks[0].taskSpec.steps[0].script");
|
||||
expect(observed.live.firstDrift?.expected).toBe(pacValueEvidence(expectedSecret));
|
||||
expect(observed.live.firstDrift?.actual).toBe(pacValueEvidence(actualSecret));
|
||||
expect(serialized).not.toContain(expectedSecret);
|
||||
expect(serialized).not.toContain(actualSecret);
|
||||
expect(serialized).not.toContain("Authorization");
|
||||
expect(serialized).not.toContain("QUERY_SUPER_SECRET");
|
||||
});
|
||||
|
||||
test("unknown runtime keys and forged paths are fingerprinted", () => {
|
||||
const maliciousKey = "HEADER_QUERY_SUPER_SECRET";
|
||||
const local = firstPacSourceArtifactDrift({ tasks: [], [maliciousKey]: "expected" }, { tasks: [], [maliciousKey]: "actual" });
|
||||
expect(local?.path).not.toContain(maliciousKey);
|
||||
expect(local?.path).toContain("type=string,length=");
|
||||
|
||||
const maliciousInput = { ...runtimeInput(), desiredSpec: { ...desiredSpec, [maliciousKey]: "expected" } };
|
||||
const observed = observePacSourceArtifactRuntime(maliciousInput, (_args: string[], label: string) => label === "pipeline-get"
|
||||
? { kind: "ok", value: { ...pipeline(), spec: { ...desiredSpec, [maliciousKey]: "actual" } } }
|
||||
: { kind: "ok", value: { items: [pipelineRun("agentrun-nc01-v02-ci-key", "2026-07-11T00:00:00Z", { ...desiredSpec, [maliciousKey]: "actual" })] } });
|
||||
expect(JSON.stringify(observed)).not.toContain(maliciousKey);
|
||||
expect(pacRuntimeSafePath(`$.${maliciousKey}`)).toBe(`path(${pacValueEvidence(`$.${maliciousKey}`)})`);
|
||||
});
|
||||
|
||||
test("untrusted runtime annotations and identities are never echoed", () => {
|
||||
const secret = "Authorization: Bearer OBSERVER_SUPER_SECRET https://u:p@example.invalid/x?token=QUERY_SECRET";
|
||||
const annotations = {
|
||||
...manifestAnnotations(),
|
||||
"unidesk.ai/owning-config-ref": secret,
|
||||
"unidesk.ai/effective-config-sha256": secret,
|
||||
"unidesk.ai/source-artifact-renderer": secret,
|
||||
};
|
||||
const observed = observePacSourceArtifactRuntime(runtimeInput(), (_args: string[], label: string) => label === "pipeline-get"
|
||||
? { kind: "ok", value: { metadata: { name: secret, annotations }, spec: desiredSpec } }
|
||||
: { kind: "ok", value: { items: [{ ...pipelineRun("agentrun-nc01-v02-ci-annotation", "2026-07-11T00:00:00Z"), metadata: { name: secret, creationTimestamp: "2026-07-11T00:00:00Z", labels: { "pipelinesascode.tekton.dev/sha": sourceCommit }, annotations } }] } });
|
||||
const serialized = JSON.stringify(observed);
|
||||
expect(observed.live.provenanceAligned).toBe(false);
|
||||
expect(observed.live.configRef).toBeNull();
|
||||
expect(observed.live.effectiveConfigSha256).toBeNull();
|
||||
expect(observed.live.name).toBeNull();
|
||||
expect(serialized).not.toContain(secret);
|
||||
expect(serialized).not.toContain("OBSERVER_SUPER_SECRET");
|
||||
expect(serialized).not.toContain("QUERY_SECRET");
|
||||
|
||||
const forged = pacRuntimeSourceArtifactItem({
|
||||
status: "drifted",
|
||||
name: "observer-super-secret",
|
||||
canonicalSha256: `sha256:${"c".repeat(64)}`,
|
||||
firstDrift: { path: "$.HEADER_QUERY_SUPER_SECRET", expected: secret, actual: secret },
|
||||
provenanceAligned: false,
|
||||
configRef: "SECRET/TOKEN#QUERY",
|
||||
effectiveConfigSha256: `sha256:${"d".repeat(64)}`,
|
||||
renderer: provenance.renderer,
|
||||
mode: provenance.mode,
|
||||
sourceCommit: "e".repeat(40),
|
||||
reason: secret,
|
||||
candidateCount: 1,
|
||||
}, {
|
||||
configRef: provenance.configRef,
|
||||
effectiveConfigSha256: `sha256:${"f".repeat(64)}`,
|
||||
renderer: provenance.renderer,
|
||||
mode: provenance.mode,
|
||||
sourceCommit,
|
||||
exactName: null,
|
||||
namePrefix: "agentrun-nc01-v02-ci",
|
||||
});
|
||||
const forgedSerialized = JSON.stringify(forged);
|
||||
expect(forged.name).toBeNull();
|
||||
expect(forged.configRef).toBeNull();
|
||||
expect(forged.effectiveConfigSha256).toBeNull();
|
||||
expect(forged.provenanceAligned).toBe(false);
|
||||
expect(forged.sourceCommit).toBeNull();
|
||||
expect(forged.firstDrift?.path).not.toContain("HEADER_QUERY_SUPER_SECRET");
|
||||
expect(forgedSerialized).not.toContain(secret);
|
||||
expect(forgedSerialized).not.toContain("SECRET/TOKEN#QUERY");
|
||||
|
||||
const forgedAligned = pacRuntimeSourceArtifactItem({
|
||||
status: "aligned",
|
||||
provenanceAligned: true,
|
||||
configRef: provenance.configRef,
|
||||
effectiveConfigSha256: provenance.effectiveConfigSha256,
|
||||
renderer: "forged-renderer",
|
||||
mode: provenance.mode,
|
||||
sourceCommit,
|
||||
candidateCount: 1,
|
||||
}, {
|
||||
configRef: provenance.configRef,
|
||||
effectiveConfigSha256: provenance.effectiveConfigSha256,
|
||||
renderer: provenance.renderer,
|
||||
mode: provenance.mode,
|
||||
sourceCommit,
|
||||
exactName: null,
|
||||
namePrefix: null,
|
||||
});
|
||||
expect(forgedAligned.provenanceAligned).toBe(false);
|
||||
});
|
||||
|
||||
test("typed runtime alignment never reports a synthetic pending state", () => {
|
||||
const aligned = observePacSourceArtifactRuntime(runtimeInput(), (_args: string[], label: string) => label === "pipeline-get"
|
||||
? { kind: "ok", value: pipeline() }
|
||||
: { kind: "ok", value: { items: [pipelineRun("agentrun-nc01-v02-ci-aligned", "2026-07-11T00:00:00Z")] } });
|
||||
const drifted = structuredClone(aligned);
|
||||
drifted.embedded.status = "drifted";
|
||||
const exact = { head: sourceCommit, clean: true, matchesSourceCommit: true };
|
||||
expect(sourceArtifactRuntimeAlignment(null, exact, sourceCommit)).toBe("not-requested");
|
||||
expect(sourceArtifactRuntimeAlignment(aligned, exact, sourceCommit)).toBe("aligned");
|
||||
expect(sourceArtifactRuntimeAlignment(drifted, exact, sourceCommit)).toBe("drifted");
|
||||
expect(sourceArtifactRuntimeAlignment(aligned, { ...exact, clean: false }, sourceCommit)).toBe("unavailable");
|
||||
expect(JSON.stringify([aligned, drifted])).not.toContain("pending");
|
||||
});
|
||||
|
||||
test("local transport and remote observer failures retain only bounded evidence", () => {
|
||||
const secret = "Authorization: Bearer TRANSPORT_SUPER_SECRET https://review:password@example.invalid/path?token=QUERY_SECRET";
|
||||
const localReason = pacRuntimeTransportFailureReason({ exitCode: 126, stdout: secret, stderr: secret });
|
||||
expect(localReason).toContain(pacValueEvidence(secret));
|
||||
expect(localReason).not.toContain(secret);
|
||||
expect(pacRuntimeSafeReason(secret)).toBe(`runtime-observer-reason:${pacValueEvidence(secret)}`);
|
||||
|
||||
const temporary = mkdtempSync(resolve(tmpdir(), "unidesk-pac-runtime-redaction-test-"));
|
||||
try {
|
||||
const bin = resolve(temporary, "bin");
|
||||
mkdirSync(bin);
|
||||
const kubectl = resolve(bin, "kubectl");
|
||||
writeFileSync(kubectl, `#!/bin/sh\nprintf '%s\\n' '${secret}' >&2\nexit 126\n`);
|
||||
chmodSync(kubectl, 0o755);
|
||||
const shell = renderPacSourceArtifactRuntimeObserverShell(runtimeInput());
|
||||
const result = spawnSync("sh", [], {
|
||||
input: shell,
|
||||
encoding: "utf8",
|
||||
timeout: 30_000,
|
||||
env: { ...process.env, PATH: `${bin}:${process.env.PATH ?? ""}` },
|
||||
});
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.stdout).toContain("command-failed:126:type=string");
|
||||
expect(result.stdout).not.toContain(secret);
|
||||
expect(result.stdout).not.toContain("Authorization");
|
||||
expect(result.stdout).not.toContain("QUERY_SECRET");
|
||||
} finally {
|
||||
rmSync(temporary, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("temp-file transport handles a payload larger than the current 203 KiB HWLAB Pipeline", () => {
|
||||
const observerSource = readFileSync(rootPath("scripts", "native", "cicd", "pac-source-artifact-runtime.mjs"), "utf8");
|
||||
expect(observerSource).not.toMatch(/process\.env\.PAC_SOURCE_ARTIFACT_INPUT(?!_FILE)/u);
|
||||
const temporary = mkdtempSync(resolve(tmpdir(), "unidesk-pac-runtime-test-"));
|
||||
try {
|
||||
const bin = resolve(temporary, "bin");
|
||||
@@ -220,6 +526,7 @@ describe("runtime source-commit selection", () => {
|
||||
pipeline: "hwlab-nc01-v03-ci-image-publish",
|
||||
pipelineRunPrefix: "hwlab-nc01-v03-ci-poll",
|
||||
desiredSpec: hugeSpec,
|
||||
desiredWrapper: { mode: "embedded-pipeline-spec", executionMode: "pipeline-spec", spec: runtimeWrapperSpec },
|
||||
provenance,
|
||||
sourceCommit,
|
||||
};
|
||||
@@ -258,28 +565,6 @@ describe("runtime source-commit selection", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("HWLAB domain Pipeline contract", () => {
|
||||
const requiredMarkers = [
|
||||
"unidesk-runtime-gitops-postprocess",
|
||||
"unidesk-runtime-gitops-verify",
|
||||
"HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_ENABLED",
|
||||
"HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_GROUP_PREFIX",
|
||||
"HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_TIMEOUT_MS",
|
||||
"HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_SCAN_LIMIT",
|
||||
"HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_MATCHED_EVENT_LIMIT",
|
||||
"HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_LIVE_BUFFER_LIMIT",
|
||||
];
|
||||
|
||||
test("accepts the complete renderer contract and rejects every missing critical marker", () => {
|
||||
const complete = { tasks: [{ taskSpec: { steps: [{ script: requiredMarkers.join("\n") }] } }] };
|
||||
expect(() => assertHwlabPipelineContracts(complete)).not.toThrow();
|
||||
for (const marker of requiredMarkers) {
|
||||
const missing = { tasks: [{ taskSpec: { steps: [{ script: requiredMarkers.filter((item) => item !== marker).join("\n") }] } }] };
|
||||
expect(() => assertHwlabPipelineContracts(missing)).toThrow(marker);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("shared HWLAB deploy overlay", () => {
|
||||
test("preserves exact undefined/null semantics without mutating its input", () => {
|
||||
const document = {
|
||||
|
||||
@@ -14,11 +14,18 @@ import { nodeRuntimeRenderOverlay } from "./hwlab-node/web-probe";
|
||||
import { applyNodeRuntimeDeployYamlOverlay } from "./hwlab-node/deploy-overlay";
|
||||
import type { RenderedCliResult } from "./output";
|
||||
import { renderedCliResult } from "./agentrun/render";
|
||||
import { stableJsonSha256 } from "./stable-json";
|
||||
import { stableJsonSha256, stableJsonValue } from "./stable-json";
|
||||
|
||||
export type PacSourceArtifactMode = "embedded-pipeline-spec" | "remote-pipeline-annotation";
|
||||
export type PacSourceArtifactRenderer = "agentrun-control-plane" | "hwlab-runtime-lane";
|
||||
export type PacSourceArtifactAction = "plan" | "check" | "write" | "status" | "verify-runtime";
|
||||
export type PacSourceArtifactRuntimeAlignment = "not-requested" | "aligned" | "drifted" | "missing" | "unavailable";
|
||||
|
||||
export interface PacSourceArtifactTaskRunTemplateSpec {
|
||||
readonly hostNetwork: boolean;
|
||||
readonly dnsPolicy: "ClusterFirst" | "ClusterFirstWithHostNet" | "Default" | "None";
|
||||
readonly fsGroup: number;
|
||||
}
|
||||
|
||||
export interface PacSourceArtifactSpec {
|
||||
readonly mode: PacSourceArtifactMode;
|
||||
@@ -27,6 +34,7 @@ export interface PacSourceArtifactSpec {
|
||||
readonly pipelineRunPath: string;
|
||||
readonly pipelinePath: string | null;
|
||||
readonly maxKeepRuns: number;
|
||||
readonly taskRunTemplate: PacSourceArtifactTaskRunTemplateSpec;
|
||||
}
|
||||
|
||||
export interface PacSourceArtifactBinding {
|
||||
@@ -42,6 +50,7 @@ export interface PacSourceArtifactBinding {
|
||||
};
|
||||
readonly repository: {
|
||||
readonly id: string;
|
||||
readonly url: string;
|
||||
readonly cloneUrl: string;
|
||||
readonly owner: string;
|
||||
readonly repo: string;
|
||||
@@ -87,6 +96,7 @@ export interface PacSourceArtifactDrift {
|
||||
export type PacSourceArtifactRuntimeObserver = (input: {
|
||||
readonly binding: PacSourceArtifactBinding;
|
||||
readonly desiredSpec: Record<string, unknown>;
|
||||
readonly desiredWrapper: Record<string, unknown> | null;
|
||||
readonly provenance: PacSourceArtifactProvenance;
|
||||
readonly sourceCommit: string | null;
|
||||
}) => Promise<PacSourceArtifactRuntimeObservation>;
|
||||
@@ -95,6 +105,7 @@ interface PacSourceArtifactProvenance {
|
||||
readonly configRef: string;
|
||||
readonly effectiveConfigSha256: string;
|
||||
readonly renderer: PacSourceArtifactRenderer;
|
||||
readonly mode: PacSourceArtifactMode;
|
||||
}
|
||||
|
||||
interface DesiredArtifact {
|
||||
@@ -114,6 +125,48 @@ interface SourceInspection {
|
||||
readonly provenanceAligned: boolean;
|
||||
}
|
||||
|
||||
export interface SourceWorktreeState {
|
||||
readonly head: string;
|
||||
readonly clean: boolean;
|
||||
readonly matchesSourceCommit: boolean | null;
|
||||
}
|
||||
|
||||
export class PacSourceArtifactError extends Error {
|
||||
readonly code: string;
|
||||
readonly safeDetails: Readonly<Record<string, string>>;
|
||||
|
||||
constructor(code: string, safeDetails: Readonly<Record<string, string>> = {}) {
|
||||
super(code);
|
||||
this.name = "PacSourceArtifactError";
|
||||
this.code = code;
|
||||
this.safeDetails = safeDetails;
|
||||
}
|
||||
}
|
||||
|
||||
export interface PacSourceArtifactSafeError {
|
||||
readonly code: string;
|
||||
readonly evidence: string;
|
||||
readonly details: Readonly<Record<string, string>>;
|
||||
}
|
||||
|
||||
export function pacSourceArtifactSafeError(error: unknown): PacSourceArtifactSafeError {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
const allowedDetailKeys = new Set([
|
||||
"expectedUrlFingerprint",
|
||||
"observedUrlFingerprint",
|
||||
"head",
|
||||
"requestedSourceCommit",
|
||||
]);
|
||||
const details = error instanceof PacSourceArtifactError
|
||||
? Object.fromEntries(Object.entries(error.safeDetails).map(([key, value]) => [key, allowedDetailKeys.has(key) ? value : pacValueEvidence(value)]))
|
||||
: {};
|
||||
return {
|
||||
code: error instanceof PacSourceArtifactError ? error.code : "source-artifact-operation-failed",
|
||||
evidence: pacValueEvidence(message),
|
||||
details,
|
||||
};
|
||||
}
|
||||
|
||||
const provenanceKeys = {
|
||||
configRef: "unidesk.ai/owning-config-ref",
|
||||
effectiveConfigSha256: "unidesk.ai/effective-config-sha256",
|
||||
@@ -121,6 +174,15 @@ const provenanceKeys = {
|
||||
mode: "unidesk.ai/source-artifact-mode",
|
||||
} as const;
|
||||
|
||||
const knownDriftPathKeys = new Set([
|
||||
"accessModes", "annotations", "apiVersion", "args", "command", "computeResources", "default", "description", "dnsPolicy", "env", "envFrom",
|
||||
"executionMode", "finally", "fsGroup", "generateName", "hostNetwork", "image", "kind", "labels", "length", "metadata", "mode", "mountPath", "name", "namespace", "onError", "operator",
|
||||
"optional", "params", "pipeline", "pipelineRef", "pipelineSpec", "podTemplate", "readinessProbe", "readOnly", "requests", "resources",
|
||||
"results", "retries", "runAfter", "script", "secret", "secretName", "securityContext", "serviceAccountName", "sidecars", "spec", "steps",
|
||||
"storage", "subPath", "taskRunTemplate", "taskSpec", "tasks", "timeout", "timeouts", "type", "value", "values", "volumeClaimTemplate",
|
||||
"volumeMounts", "volumes", "when", "workspaces", "workingDir", "wrapper",
|
||||
]);
|
||||
|
||||
export function parsePacSourceArtifactOptions(args: readonly string[]): PacSourceArtifactOptions {
|
||||
const action = args[0];
|
||||
if (!isSourceArtifactAction(action)) {
|
||||
@@ -165,7 +227,7 @@ export function parsePacSourceArtifactOptions(args: readonly string[]): PacSourc
|
||||
if (!isAbsolute(sourceWorktree)) throw new Error("--source-worktree must be an absolute path");
|
||||
if (sourceCommit !== null && !/^[0-9a-f]{40}$/u.test(sourceCommit)) throw new Error("--source-commit must be a full 40-character git SHA");
|
||||
if (sourceCommit !== null && action !== "status" && action !== "verify-runtime") throw new Error("--source-commit is accepted only by source-artifact status or verify-runtime");
|
||||
if (action === "verify-runtime" && sourceCommit === null) throw new Error("source-artifact verify-runtime requires explicit --source-commit");
|
||||
if (action === "verify-runtime" && sourceCommit === null) throw new PacSourceArtifactError("source-commit-required");
|
||||
if (action === "write" && !confirm) throw new Error("source-artifact write requires --confirm");
|
||||
if (action !== "write" && confirm) throw new Error("--confirm is accepted only by source-artifact write");
|
||||
return { action, targetId, consumerId, sourceWorktree, sourceCommit, confirm, json, full };
|
||||
@@ -183,6 +245,13 @@ export async function runPacSourceArtifact(
|
||||
throw new Error(`source-artifact consumer ${options.consumerId} does not match resolved consumer ${binding.consumer.id}`);
|
||||
}
|
||||
const sourceWorktree = validateSourceWorktree(options.sourceWorktree, binding);
|
||||
const sourceHead = git(sourceWorktree, ["rev-parse", "HEAD"]).toLowerCase();
|
||||
const sourceWorktreeState: SourceWorktreeState = {
|
||||
head: sourceHead,
|
||||
clean: git(sourceWorktree, ["status", "--porcelain=v1", "--untracked-files=all"]).length === 0,
|
||||
matchesSourceCommit: options.sourceCommit === null ? null : sourceHead === options.sourceCommit,
|
||||
};
|
||||
assertPacSourceArtifactVerifyWorktree(options.action, options.sourceCommit, sourceWorktreeState);
|
||||
const desired = renderDesiredArtifact(binding, sourceWorktree);
|
||||
let source = inspectSourceArtifact(sourceWorktree, binding, desired);
|
||||
const written: string[] = [];
|
||||
@@ -194,14 +263,17 @@ export async function runPacSourceArtifact(
|
||||
const runtime = needsRuntime
|
||||
? observeRuntime === undefined
|
||||
? unavailableRuntimeObservation()
|
||||
: await observeRuntime({ binding, desiredSpec: desired.desiredSpec, provenance: desired.provenance, sourceCommit: options.sourceCommit })
|
||||
: await observeRuntime({
|
||||
binding,
|
||||
desiredSpec: desired.desiredSpec,
|
||||
desiredWrapper: options.sourceCommit === null ? null : desiredRuntimePipelineRunWrapper(binding, desired.pipelineRun, options.sourceCommit),
|
||||
provenance: desired.provenance,
|
||||
sourceCommit: options.sourceCommit,
|
||||
})
|
||||
: null;
|
||||
const sourceOk = source.aligned && source.provenanceAligned;
|
||||
const runtimeOk = runtime !== null
|
||||
&& runtime.live.status === "aligned"
|
||||
&& runtime.live.provenanceAligned
|
||||
&& runtime.embedded.status === "aligned"
|
||||
&& runtime.embedded.provenanceAligned;
|
||||
const runtimeAlignment = sourceArtifactRuntimeAlignment(runtime, sourceWorktreeState, options.sourceCommit);
|
||||
const runtimeOk = runtimeAlignment === "aligned";
|
||||
const ok = options.action === "check"
|
||||
? sourceOk
|
||||
: options.action === "verify-runtime"
|
||||
@@ -233,17 +305,10 @@ export async function runPacSourceArtifact(
|
||||
namespace: binding.consumer.namespace,
|
||||
},
|
||||
source,
|
||||
sourceWorktreeState,
|
||||
runtime,
|
||||
runtimeTarget: { sourceCommit: options.sourceCommit },
|
||||
runtimeAlignment: runtime === null
|
||||
? "not-requested"
|
||||
: runtimeOk
|
||||
? "aligned"
|
||||
: runtime.live.status === "missing" || runtime.embedded.status === "missing"
|
||||
? "missing"
|
||||
: runtime.live.status === "unavailable" || runtime.embedded.status === "unavailable"
|
||||
? "unavailable"
|
||||
: "pending",
|
||||
runtimeAlignment,
|
||||
boundary: {
|
||||
desiredAuthority: "owning YAML plus domain renderer",
|
||||
liveExportAllowed: false,
|
||||
@@ -258,32 +323,90 @@ export async function runPacSourceArtifact(
|
||||
],
|
||||
valuesPrinted: false,
|
||||
},
|
||||
next: sourceOk
|
||||
? options.action === "verify-runtime"
|
||||
? null
|
||||
: `After the consumer PR rolls out, run source-artifact verify-runtime for ${binding.consumer.id} with --source-commit <full-sha>.`
|
||||
: options.action === "write"
|
||||
? "Generated files are still inconsistent; inspect firstDrift and do not publish."
|
||||
: `Run source-artifact write --confirm against the explicit ${sourceWorktree} worktree.`,
|
||||
next: sourceArtifactNext(binding, options, sourceOk, sourceWorktreeState, runtime, runtimeAlignment),
|
||||
};
|
||||
}
|
||||
|
||||
export function assertPacSourceArtifactVerifyWorktree(
|
||||
action: PacSourceArtifactAction,
|
||||
sourceCommit: string | null,
|
||||
worktree: SourceWorktreeState,
|
||||
): void {
|
||||
if (action !== "verify-runtime") return;
|
||||
if (sourceCommit === null) throw new PacSourceArtifactError("source-commit-required");
|
||||
if (!worktree.clean) throw new PacSourceArtifactError("source-worktree-not-clean", { head: worktree.head });
|
||||
if (worktree.matchesSourceCommit !== true) {
|
||||
throw new PacSourceArtifactError("source-worktree-commit-mismatch", {
|
||||
head: worktree.head,
|
||||
requestedSourceCommit: sourceCommit,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function sourceArtifactRuntimeAlignment(
|
||||
runtime: PacSourceArtifactRuntimeObservation | null,
|
||||
worktree: SourceWorktreeState,
|
||||
sourceCommit: string | null,
|
||||
): PacSourceArtifactRuntimeAlignment {
|
||||
if (runtime === null) return "not-requested";
|
||||
if (sourceCommit !== null && (!worktree.clean || worktree.matchesSourceCommit !== true)) return "unavailable";
|
||||
if (runtime.live.status === "unavailable" || runtime.embedded.status === "unavailable") return "unavailable";
|
||||
if (runtime.live.status === "missing" || runtime.embedded.status === "missing") return "missing";
|
||||
if (runtime.live.status === "drifted" || runtime.embedded.status === "drifted") return "drifted";
|
||||
if (!runtime.live.provenanceAligned || !runtime.embedded.provenanceAligned) return "drifted";
|
||||
return "aligned";
|
||||
}
|
||||
|
||||
function sourceArtifactNext(
|
||||
binding: PacSourceArtifactBinding,
|
||||
options: PacSourceArtifactOptions,
|
||||
sourceOk: boolean,
|
||||
worktree: SourceWorktreeState,
|
||||
runtime: PacSourceArtifactRuntimeObservation | null,
|
||||
runtimeAlignment: PacSourceArtifactRuntimeAlignment,
|
||||
): string {
|
||||
if (!sourceOk) {
|
||||
return options.action === "write"
|
||||
? "Generated files remain inconsistent; inspect the safe firstDrift evidence and do not publish."
|
||||
: `Run source-artifact write --confirm for ${binding.consumer.id}, then rerun check.`;
|
||||
}
|
||||
if (runtime === null) return `After the consumer PR rolls out, use a clean exact-commit worktree and run verify-runtime for ${binding.consumer.id} with --source-commit <full-sha>.`;
|
||||
if (!worktree.clean || (options.sourceCommit !== null && worktree.matchesSourceCommit !== true)) {
|
||||
return "Create a clean worktree at the exact GitHub merge commit, then rerun status or verify-runtime with that full source commit.";
|
||||
}
|
||||
if (runtime.live.status === "unavailable") return "Repair the owning control-plane runtime observer or RBAC, then rerun status before verification.";
|
||||
if (runtime.live.status === "missing") return "Apply the owning YAML control plane, confirm the live Pipeline exists, then rerun verify-runtime.";
|
||||
if (runtime.live.status === "drifted" || !runtime.live.provenanceAligned) return "Apply the owning YAML control plane so the live Pipeline spec and provenance align, then rerun verify-runtime.";
|
||||
if (runtime.embedded.status === "unavailable") return "Inspect the exact-commit PaC PipelineRun identity and observer access, then rerun verify-runtime.";
|
||||
if (runtime.embedded.status === "missing") return "Wait for or repair the exact-commit PaC PipelineRun; do not validate a prefix-selected run.";
|
||||
if (runtime.embedded.status === "drifted" || !runtime.embedded.provenanceAligned) return "Regenerate and merge the consumer source artifact, then verify the new exact-commit PipelineRun.";
|
||||
return runtimeAlignment === "aligned"
|
||||
? "Exact source, live Pipeline, embedded PipelineRun, and provenance are aligned; continue consumer closeout."
|
||||
: "Resolve the reported runtime layer and rerun verify-runtime.";
|
||||
}
|
||||
|
||||
export function renderPacSourceArtifactResult(result: Record<string, unknown>): RenderedCliResult {
|
||||
const files = record(result.files);
|
||||
const desired = record(result.desired);
|
||||
const source = record(result.source);
|
||||
const sourceWorktreeState = record(result.sourceWorktreeState);
|
||||
const runtime = result.runtime === null ? null : record(result.runtime);
|
||||
const live = runtime === null ? null : record(runtime.live);
|
||||
const embedded = runtime === null ? null : record(runtime.embedded);
|
||||
const firstDrift = source.firstDrift === null ? null : record(source.firstDrift);
|
||||
const liveDrift = live?.firstDrift === null ? null : record(live?.firstDrift);
|
||||
const embeddedDrift = embedded?.firstDrift === null ? null : record(embedded?.firstDrift);
|
||||
const lines = [
|
||||
"PAC SOURCE ARTIFACT",
|
||||
`target=${text(result.target)} consumer=${text(result.consumer)} node=${text(result.node)} lane=${text(result.lane)}`,
|
||||
`mode=${text(result.mode)} renderer=${text(result.renderer)} mutation=${text(result.mutation)}`,
|
||||
`pipelineRun=${text(files.pipelineRun)} pipeline=${text(files.pipeline ?? "-")}`,
|
||||
`desired=${shortSha(desired.canonicalSha256)} source=${shortSha(source.pipelineCanonicalSha256)} aligned=${text(source.aligned)} provenance=${text(source.provenanceAligned)}`,
|
||||
`firstDrift=${firstDrift === null ? "-" : `${text(firstDrift.path)} expected=${text(firstDrift.expected)} actual=${text(firstDrift.actual)}`}`,
|
||||
`runtime=${text(result.runtimeAlignment)} live=${text(live?.name)}@${shortSha(live?.canonicalSha256)} embedded=${text(embedded?.name)}@${shortSha(embedded?.canonicalSha256)} commit=${text(embedded?.sourceCommit ?? record(result.runtimeTarget).sourceCommit)} candidates=${text(embedded?.candidateCount)}`,
|
||||
`sourceWorktree=head:${shortSha(sourceWorktreeState.head)} clean:${text(sourceWorktreeState.clean)} commitMatch:${text(sourceWorktreeState.matchesSourceCommit)}`,
|
||||
`sourceDrift=${formatDrift(firstDrift)}`,
|
||||
`runtime=${text(result.runtimeAlignment)} commit=${text(embedded?.sourceCommit ?? record(result.runtimeTarget).sourceCommit)} candidates=${text(embedded?.candidateCount)}`,
|
||||
`live=status:${text(live?.status)} name:${text(live?.name)} hash:${shortSha(live?.canonicalSha256)} provenance:${text(live?.provenanceAligned)} drift:${formatDrift(liveDrift)}`,
|
||||
`embedded=status:${text(embedded?.status)} name:${text(embedded?.name)} hash:${shortSha(embedded?.canonicalSha256)} provenance:${text(embedded?.provenanceAligned)} drift:${formatDrift(embeddedDrift)}`,
|
||||
`runtimeReason=live:${text(live?.reason)} embedded:${text(embedded?.reason)}`,
|
||||
`written=${Array.isArray(files.written) && files.written.length > 0 ? files.written.join(",") : "-"}`,
|
||||
];
|
||||
@@ -319,18 +442,17 @@ function renderDesiredArtifact(binding: PacSourceArtifactBinding, sourceWorktree
|
||||
const rendered = sourceArtifact.renderer === "agentrun-control-plane"
|
||||
? renderAgentRunDesiredPipeline(binding)
|
||||
: renderHwlabDesiredPipeline(binding, sourceWorktree);
|
||||
const pipeline = withPipelineProvenance(rendered.pipeline, rendered.provenance, sourceArtifact.mode);
|
||||
const pipeline = withPipelineProvenance(rendered.pipeline, rendered.provenance);
|
||||
const desiredSpec = requiredRecord(pipeline.spec, "rendered Pipeline.spec");
|
||||
assertPipelineIdentity(pipeline, binding);
|
||||
if (sourceArtifact.renderer === "hwlab-runtime-lane") assertHwlabPipelineContracts(desiredSpec);
|
||||
const pipelineRun = sourceArtifact.mode === "embedded-pipeline-spec"
|
||||
? embeddedPipelineRun(binding, desiredSpec, rendered.provenance)
|
||||
: remotePipelineRun(binding, pipeline, rendered.provenance);
|
||||
const files = sourceArtifact.mode === "embedded-pipeline-spec"
|
||||
? [{ path: sourceArtifact.pipelineRunPath, content: yaml(pipelineRun) }]
|
||||
? [{ path: sourceArtifact.pipelineRunPath, content: pacSourceArtifactYaml(pipelineRun) }]
|
||||
: [
|
||||
{ path: requiredString(sourceArtifact.pipelinePath, "sourceArtifact.pipelinePath"), content: `${JSON.stringify(pipeline)}\n` },
|
||||
{ path: sourceArtifact.pipelineRunPath, content: yaml(pipelineRun) },
|
||||
{ path: requiredString(sourceArtifact.pipelinePath, "sourceArtifact.pipelinePath"), content: pacSourceArtifactYaml(pipeline) },
|
||||
{ path: sourceArtifact.pipelineRunPath, content: pacSourceArtifactYaml(pipelineRun) },
|
||||
];
|
||||
return { pipeline, pipelineRun, provenance: rendered.provenance, desiredSpec, files };
|
||||
}
|
||||
@@ -355,6 +477,7 @@ function renderAgentRunDesiredPipeline(binding: PacSourceArtifactBinding): { pip
|
||||
configRef: expectedConfigRef,
|
||||
effectiveConfigSha256: stableJsonSha256(spec),
|
||||
renderer: "agentrun-control-plane",
|
||||
mode: binding.consumer.sourceArtifact.mode,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -381,6 +504,7 @@ function renderHwlabDesiredPipeline(binding: PacSourceArtifactBinding, sourceWor
|
||||
configRef: expectedConfigRef,
|
||||
effectiveConfigSha256: stableJsonSha256(spec),
|
||||
renderer: "hwlab-runtime-lane",
|
||||
mode: binding.consumer.sourceArtifact.mode,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -540,16 +664,52 @@ function pipelineRunLabels(binding: PacSourceArtifactBinding, partOf: "agentrun"
|
||||
}
|
||||
|
||||
function taskRunTemplate(binding: PacSourceArtifactBinding): Record<string, unknown> {
|
||||
const declared = binding.consumer.sourceArtifact.taskRunTemplate;
|
||||
return {
|
||||
serviceAccountName: `{{ service_account }}`,
|
||||
podTemplate: {
|
||||
hostNetwork: true,
|
||||
dnsPolicy: "ClusterFirstWithHostNet",
|
||||
securityContext: { fsGroup: 1000 },
|
||||
hostNetwork: declared.hostNetwork,
|
||||
dnsPolicy: declared.dnsPolicy,
|
||||
securityContext: { fsGroup: declared.fsGroup },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function desiredRuntimePipelineRunWrapper(
|
||||
binding: PacSourceArtifactBinding,
|
||||
pipelineRun: Record<string, unknown>,
|
||||
sourceCommit: string,
|
||||
): Record<string, unknown> {
|
||||
const sourceSpec = structuredClone(requiredRecord(pipelineRun.spec, "generated PipelineRun.spec"));
|
||||
delete sourceSpec.pipelineSpec;
|
||||
delete sourceSpec.pipelineRef;
|
||||
return {
|
||||
mode: binding.consumer.sourceArtifact.mode,
|
||||
executionMode: "pipeline-spec",
|
||||
spec: resolvePacRuntimeTemplates(sourceSpec, binding, sourceCommit),
|
||||
};
|
||||
}
|
||||
|
||||
function resolvePacRuntimeTemplates(value: unknown, binding: PacSourceArtifactBinding, sourceCommit: string): unknown {
|
||||
if (Array.isArray(value)) return value.map((item) => resolvePacRuntimeTemplates(item, binding, sourceCommit));
|
||||
if (isRecord(value)) {
|
||||
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, resolvePacRuntimeTemplates(item, binding, sourceCommit)]));
|
||||
}
|
||||
if (typeof value !== "string") return value;
|
||||
const templateValues: Readonly<Record<string, string>> = {
|
||||
...binding.repository.params,
|
||||
revision: sourceCommit,
|
||||
repo_url: binding.repository.url,
|
||||
};
|
||||
const resolved = value.replace(/\{\{\s*([A-Za-z0-9_]+)\s*\}\}/gu, (_match, key: string) => {
|
||||
const replacement = templateValues[key];
|
||||
if (replacement === undefined) throw new PacSourceArtifactError("source-artifact-runtime-template-unresolved", { template: pacValueEvidence(key) });
|
||||
return replacement;
|
||||
});
|
||||
if (/\{\{|\}\}/u.test(resolved)) throw new PacSourceArtifactError("source-artifact-runtime-template-invalid");
|
||||
return resolved;
|
||||
}
|
||||
|
||||
function pipelineRunParams(binding: PacSourceArtifactBinding, desiredSpec: Record<string, unknown>): Record<string, unknown>[] {
|
||||
const params = arrayRecords(desiredSpec.params, "Pipeline.spec.params");
|
||||
return params.map((param) => {
|
||||
@@ -585,7 +745,7 @@ function pipelineRunWorkspaces(binding: PacSourceArtifactBinding, desiredSpec: R
|
||||
});
|
||||
}
|
||||
|
||||
function withPipelineProvenance(pipeline: Record<string, unknown>, provenance: PacSourceArtifactProvenance, mode: PacSourceArtifactMode): Record<string, unknown> {
|
||||
function withPipelineProvenance(pipeline: Record<string, unknown>, provenance: PacSourceArtifactProvenance): Record<string, unknown> {
|
||||
const next = structuredClone(pipeline);
|
||||
const metadata = requiredRecord(next.metadata, "rendered Pipeline.metadata");
|
||||
const annotations = isRecord(metadata.annotations) ? metadata.annotations : {};
|
||||
@@ -594,7 +754,7 @@ function withPipelineProvenance(pipeline: Record<string, unknown>, provenance: P
|
||||
[provenanceKeys.configRef]: provenance.configRef,
|
||||
[provenanceKeys.effectiveConfigSha256]: provenance.effectiveConfigSha256,
|
||||
[provenanceKeys.renderer]: provenance.renderer,
|
||||
[provenanceKeys.mode]: mode,
|
||||
[provenanceKeys.mode]: provenance.mode,
|
||||
};
|
||||
return next;
|
||||
}
|
||||
@@ -604,7 +764,7 @@ function inspectSourceArtifact(sourceWorktree: string, binding: PacSourceArtifac
|
||||
const pipelineRunPath = safeArtifactPath(sourceWorktree, sourceArtifact.pipelineRunPath);
|
||||
const pipelinePath = sourceArtifact.pipelinePath === null ? null : safeArtifactPath(sourceWorktree, sourceArtifact.pipelinePath);
|
||||
if (!existsSync(pipelineRunPath) || (pipelinePath !== null && !existsSync(pipelinePath))) {
|
||||
return { status: "missing", aligned: false, pipelineCanonicalSha256: null, pipelineRunCanonicalSha256: null, firstDrift: { path: "$", expected: "generated artifact", actual: "missing" }, provenanceAligned: false };
|
||||
return { status: "missing", aligned: false, pipelineCanonicalSha256: null, pipelineRunCanonicalSha256: null, firstDrift: drift("$", { artifact: "generated" }, undefined), provenanceAligned: false };
|
||||
}
|
||||
const actualPipelineRun = parseYamlRecord(readFileSync(pipelineRunPath, "utf8"), sourceArtifact.pipelineRunPath);
|
||||
const actualPipeline = sourceArtifact.mode === "remote-pipeline-annotation"
|
||||
@@ -628,7 +788,7 @@ function inspectSourceArtifact(sourceWorktree: string, binding: PacSourceArtifac
|
||||
aligned,
|
||||
pipelineCanonicalSha256: canonicalSha256(requiredRecord(actualPipeline.spec, "source Pipeline.spec")),
|
||||
pipelineRunCanonicalSha256: canonicalSha256(actualPipelineRun),
|
||||
firstDrift: pipelineDrift ?? pipelineRunDrift ?? (provenanceAligned ? null : { path: "$.metadata.annotations.provenance", expected: JSON.stringify(desired.provenance), actual: JSON.stringify(actualProvenance) }),
|
||||
firstDrift: pipelineDrift ?? pipelineRunDrift ?? (provenanceAligned ? null : drift("$.metadata.annotations.provenance", desired.provenance, actualProvenance)),
|
||||
provenanceAligned,
|
||||
};
|
||||
}
|
||||
@@ -639,34 +799,51 @@ function validateSourceWorktree(input: string, binding: PacSourceArtifactBinding
|
||||
const top = realpathSync(git(worktree, ["rev-parse", "--show-toplevel"]));
|
||||
if (top !== worktree) throw new Error(`--source-worktree must be the git worktree root; resolved ${top}`);
|
||||
const remote = git(worktree, ["remote", "get-url", "origin"]);
|
||||
const repoIdentity = binding.repository.repo.toLowerCase().replace(/\.git$/u, "");
|
||||
const canonicalIdentity = repoIdentity.replace(/^pikastech-/u, "pikastech/");
|
||||
const allowedIdentities = new Set([
|
||||
canonicalIdentity,
|
||||
`${binding.repository.owner}/${repoIdentity}`.toLowerCase(),
|
||||
remoteRepositoryIdentity(binding.repository.cloneUrl),
|
||||
]);
|
||||
const expectedRemote = owningSourceRemote(binding);
|
||||
const expectedIdentity = remoteRepositoryIdentity(expectedRemote);
|
||||
const observedIdentity = remoteRepositoryIdentity(remote);
|
||||
if (!allowedIdentities.has(observedIdentity)) {
|
||||
throw new Error(`source worktree origin does not match PaC repository ${binding.repository.repo}; observed ${remote}`);
|
||||
if (expectedIdentity === null || observedIdentity === null
|
||||
|| expectedIdentity.host !== observedIdentity.host
|
||||
|| expectedIdentity.ownerRepo !== observedIdentity.ownerRepo) {
|
||||
throw new PacSourceArtifactError("source-worktree-origin-mismatch", {
|
||||
expectedUrlFingerprint: fingerprint(expectedRemote),
|
||||
observedUrlFingerprint: fingerprint(remote),
|
||||
});
|
||||
}
|
||||
safeArtifactPath(worktree, binding.consumer.sourceArtifact.pipelineRunPath);
|
||||
if (binding.consumer.sourceArtifact.pipelinePath !== null) safeArtifactPath(worktree, binding.consumer.sourceArtifact.pipelinePath);
|
||||
return worktree;
|
||||
}
|
||||
|
||||
function remoteRepositoryIdentity(remote: string): string {
|
||||
function owningSourceRemote(binding: PacSourceArtifactBinding): string {
|
||||
if (binding.consumer.sourceArtifact.renderer === "agentrun-control-plane") {
|
||||
return resolveAgentRunLaneTarget({ node: binding.consumer.node, lane: binding.consumer.lane }).spec.source.worktreeRemote;
|
||||
}
|
||||
if (!isHwlabRuntimeLane(binding.consumer.lane)) throw new PacSourceArtifactError("owning-source-lane-unresolved");
|
||||
return hwlabRuntimeLaneSpecForNode(binding.consumer.lane, binding.consumer.node).gitUrl;
|
||||
}
|
||||
|
||||
function remoteRepositoryIdentity(remote: string): { readonly host: string; readonly ownerRepo: string } | null {
|
||||
const trimmed = remote.trim();
|
||||
const scp = /^[^/@\s]+@[^:\s]+:(.+)$/u.exec(trimmed);
|
||||
let path = scp?.[1] ?? "";
|
||||
if (path.length === 0) {
|
||||
const scp = /^[^/@\s]+@([^:\s]+):(.+)$/u.exec(trimmed);
|
||||
let host = scp?.[1] ?? "";
|
||||
let path = scp?.[2] ?? "";
|
||||
if (host.length === 0 || path.length === 0) {
|
||||
try {
|
||||
path = new URL(trimmed).pathname;
|
||||
const parsed = new URL(trimmed);
|
||||
if (parsed.username.length > 0 || parsed.password.length > 0 || parsed.search.length > 0 || parsed.hash.length > 0) return null;
|
||||
host = parsed.hostname;
|
||||
path = parsed.pathname;
|
||||
} catch {
|
||||
throw new Error(`git remote must be an SSH or URL repository identity; observed ${remote}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return path.replace(/^\/+|\/+$/gu, "").replace(/\.git$/iu, "").toLowerCase();
|
||||
const segments = path.replace(/^\/+|\/+$/gu, "").split("/").filter(Boolean);
|
||||
if (segments.length !== 2) return null;
|
||||
const owner = segments[0]?.toLowerCase();
|
||||
const repo = segments[1]?.replace(/\.git$/iu, "").toLowerCase();
|
||||
if (!host || !owner || !repo || !/^[a-z0-9_.-]+$/u.test(owner) || !/^[a-z0-9_.-]+$/u.test(repo)) return null;
|
||||
return { host: host.toLowerCase(), ownerRepo: `${owner}/${repo}` };
|
||||
}
|
||||
|
||||
function writeArtifactFilesAtomically(sourceWorktree: string, files: readonly { readonly path: string; readonly content: string }[]): string[] {
|
||||
@@ -754,37 +931,23 @@ function assertPipelineIdentity(pipeline: Record<string, unknown>, binding: PacS
|
||||
if (metadata.namespace !== binding.consumer.namespace) throw new Error(`rendered Pipeline namespace ${String(metadata.namespace)} does not match consumer ${binding.consumer.namespace}`);
|
||||
}
|
||||
|
||||
export function assertHwlabPipelineContracts(spec: Record<string, unknown>): void {
|
||||
const text = JSON.stringify(spec);
|
||||
const required = [
|
||||
"unidesk-runtime-gitops-postprocess",
|
||||
"unidesk-runtime-gitops-verify",
|
||||
"HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_ENABLED",
|
||||
"HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_GROUP_PREFIX",
|
||||
"HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_TIMEOUT_MS",
|
||||
"HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_SCAN_LIMIT",
|
||||
"HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_MATCHED_EVENT_LIMIT",
|
||||
"HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_LIVE_BUFFER_LIMIT",
|
||||
];
|
||||
const missing = required.filter((value) => !text.includes(value));
|
||||
if (missing.length > 0) throw new Error(`HWLAB domain renderer missing required postprocess/verify refresh contract: ${missing.join(", ")}`);
|
||||
}
|
||||
|
||||
function provenanceFromManifest(manifest: Record<string, unknown>): PacSourceArtifactProvenance | null {
|
||||
const metadata = isRecord(manifest.metadata) ? manifest.metadata : {};
|
||||
const annotations = isRecord(metadata.annotations) ? metadata.annotations : {};
|
||||
const configRef = annotations[provenanceKeys.configRef];
|
||||
const effectiveConfigSha256 = annotations[provenanceKeys.effectiveConfigSha256];
|
||||
const renderer = annotations[provenanceKeys.renderer];
|
||||
if (typeof configRef !== "string" || typeof effectiveConfigSha256 !== "string" || (renderer !== "agentrun-control-plane" && renderer !== "hwlab-runtime-lane")) return null;
|
||||
return { configRef, effectiveConfigSha256, renderer };
|
||||
const mode = annotations[provenanceKeys.mode];
|
||||
if (typeof configRef !== "string" || typeof effectiveConfigSha256 !== "string" || (renderer !== "agentrun-control-plane" && renderer !== "hwlab-runtime-lane") || (mode !== "embedded-pipeline-spec" && mode !== "remote-pipeline-annotation")) return null;
|
||||
return { configRef, effectiveConfigSha256, renderer, mode };
|
||||
}
|
||||
|
||||
function provenanceEquals(actual: PacSourceArtifactProvenance | null, expected: PacSourceArtifactProvenance): boolean {
|
||||
return actual !== null
|
||||
&& actual.configRef === expected.configRef
|
||||
&& actual.effectiveConfigSha256 === expected.effectiveConfigSha256
|
||||
&& actual.renderer === expected.renderer;
|
||||
&& actual.renderer === expected.renderer
|
||||
&& actual.mode === expected.mode;
|
||||
}
|
||||
|
||||
function firstCanonicalDrift(expected: unknown, actual: unknown, path: string): PacSourceArtifactDrift | null {
|
||||
@@ -802,8 +965,11 @@ function firstCanonicalDrift(expected: unknown, actual: unknown, path: string):
|
||||
if (!isRecord(expected) || !isRecord(actual)) return drift(path, expected, actual);
|
||||
const keys = [...new Set([...Object.keys(expected), ...Object.keys(actual)])].sort();
|
||||
for (const key of keys) {
|
||||
if (!Object.prototype.hasOwnProperty.call(expected, key) || !Object.prototype.hasOwnProperty.call(actual, key)) return drift(`${path}.${key}`, expected[key], actual[key]);
|
||||
const child = firstCanonicalDrift(expected[key], actual[key], `${path}.${key}`);
|
||||
const expectedOwnsKey = Object.prototype.hasOwnProperty.call(expected, key);
|
||||
const actualOwnsKey = Object.prototype.hasOwnProperty.call(actual, key);
|
||||
const childPath = knownDriftPathKeys.has(key) ? `${path}.${key}` : `${path}.[key:${pacValueEvidence(key)}]`;
|
||||
if (!expectedOwnsKey || !actualOwnsKey) return drift(childPath, expected[key], actual[key]);
|
||||
const child = firstCanonicalDrift(expected[key], actual[key], childPath);
|
||||
if (child !== null) return child;
|
||||
}
|
||||
return null;
|
||||
@@ -829,13 +995,29 @@ function isProvenTektonAdmissionDefault(path: readonly (string | number)[], valu
|
||||
}
|
||||
|
||||
function drift(path: string, expected: unknown, actual: unknown): PacSourceArtifactDrift {
|
||||
return { path, expected: compactValue(expected), actual: compactValue(actual) };
|
||||
return { path, expected: pacValueEvidence(expected), actual: pacValueEvidence(actual) };
|
||||
}
|
||||
|
||||
function compactValue(value: unknown): string {
|
||||
const raw = JSON.stringify(value);
|
||||
if (raw === undefined) return String(value);
|
||||
return raw.length <= 160 ? raw : `${raw.slice(0, 157)}...`;
|
||||
export function pacValueEvidence(value: unknown): string {
|
||||
const type = value === null ? "null" : Array.isArray(value) ? "array" : typeof value;
|
||||
const stable = stableJsonValue(value);
|
||||
const serialized = JSON.stringify(stable) ?? String(stable);
|
||||
const length = typeof value === "string"
|
||||
? Buffer.byteLength(value, "utf8")
|
||||
: Array.isArray(value)
|
||||
? value.length
|
||||
: isRecord(value)
|
||||
? Object.keys(value).length
|
||||
: Buffer.byteLength(serialized, "utf8");
|
||||
return `type=${type},length=${length},sha256=${fingerprint(serialized).replace(/^sha256:/u, "")}`;
|
||||
}
|
||||
|
||||
function fingerprint(value: string): string {
|
||||
return `sha256:${createHash("sha256").update(value).digest("hex")}`;
|
||||
}
|
||||
|
||||
function formatDrift(value: Record<string, unknown> | null): string {
|
||||
return value === null ? "-" : `${text(value.path)} expected:${text(value.expected)} actual:${text(value.actual)}`;
|
||||
}
|
||||
|
||||
function unavailableRuntimeObservation(): PacSourceArtifactRuntimeObservation {
|
||||
@@ -849,8 +1031,8 @@ function parseYamlRecord(text: string, label: string): Record<string, unknown> {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function yaml(value: Record<string, unknown>): string {
|
||||
return `${Bun.YAML.stringify(value).trim()}\n`;
|
||||
export function pacSourceArtifactYaml(value: Record<string, unknown>): string {
|
||||
return `${Bun.YAML.stringify(value, null, 2).trim()}\n`;
|
||||
}
|
||||
|
||||
function requiredParam(binding: PacSourceArtifactBinding, key: string): string {
|
||||
|
||||
@@ -19,6 +19,8 @@ import {
|
||||
import { materializeYamlComposition } from "./yaml-composition";
|
||||
import {
|
||||
canonicalizePacPipelineSpec,
|
||||
pacSourceArtifactSafeError,
|
||||
pacValueEvidence,
|
||||
parsePacSourceArtifactOptions,
|
||||
renderPacSourceArtifactResult,
|
||||
runPacSourceArtifact,
|
||||
@@ -251,17 +253,26 @@ export async function runPlatformInfraPipelinesAsCodeCommand(config: UniDeskConf
|
||||
},
|
||||
repository: {
|
||||
id: repository.id,
|
||||
url: repository.url,
|
||||
cloneUrl: repository.cloneUrl,
|
||||
owner: repository.owner,
|
||||
repo: repository.repo,
|
||||
params: repository.params,
|
||||
},
|
||||
};
|
||||
const result = await runPacSourceArtifact(binding, options, async ({ desiredSpec, provenance, sourceCommit }) => await observeSourceArtifactRuntime(config, target, consumer, desiredSpec, provenance, sourceCommit));
|
||||
const result = await runPacSourceArtifact(binding, options, async ({ desiredSpec, desiredWrapper, provenance, sourceCommit }) => await observeSourceArtifactRuntime(config, target, consumer, desiredSpec, desiredWrapper, provenance, sourceCommit));
|
||||
return options.json || options.full ? result : renderPacSourceArtifactResult(result);
|
||||
} catch (error) {
|
||||
if (structuredError) throw error;
|
||||
return sourceArtifactValidationError(error);
|
||||
const safeError = pacSourceArtifactSafeError(error);
|
||||
if (structuredError) {
|
||||
return {
|
||||
ok: false,
|
||||
action: "platform-infra-pipelines-as-code-source-artifact-validation",
|
||||
error: safeError,
|
||||
next: "Fix the declared owner, exact source identity, worktree proof, or CLI arguments; rerun plan before write.",
|
||||
};
|
||||
}
|
||||
return sourceArtifactValidationError(safeError);
|
||||
}
|
||||
}
|
||||
return { ok: false, error: "unsupported-platform-infra-pipelines-as-code-command", args, help: help() };
|
||||
@@ -286,12 +297,12 @@ function sourceArtifactHelp(): RenderedCliResult {
|
||||
return { ok: true, command: "platform-infra-pipelines-as-code-source-artifact-help", renderedText: `${lines.join("\n")}\n`, contentType: "text/plain" };
|
||||
}
|
||||
|
||||
function sourceArtifactValidationError(error: unknown): RenderedCliResult {
|
||||
const reason = String(error instanceof Error ? error.message : error).replace(/[\r\n]+/gu, " ").slice(0, 1000);
|
||||
function sourceArtifactValidationError(error: ReturnType<typeof pacSourceArtifactSafeError>): RenderedCliResult {
|
||||
const details = Object.entries(error.details).map(([key, value]) => `${key}=${value}`).join(" ");
|
||||
return {
|
||||
ok: false,
|
||||
command: "platform-infra-pipelines-as-code-source-artifact-validation",
|
||||
renderedText: `PAC SOURCE ARTIFACT ERROR\nreason=${reason}\nnext=Fix the explicit target, consumer, source worktree, or owning YAML declaration; rerun plan before write.\n`,
|
||||
renderedText: `PAC SOURCE ARTIFACT ERROR\ncode=${error.code} evidence=${error.evidence}${details ? ` details=${details}` : ""}\nnext=Fix the declared owner, exact source identity, worktree proof, or CLI arguments; rerun plan before write.\n`,
|
||||
contentType: "text/plain",
|
||||
};
|
||||
}
|
||||
@@ -442,6 +453,7 @@ function parseSourceArtifact(value: Record<string, unknown>, path: string): PacS
|
||||
const renderer = y.enumField(value, "renderer", path, ["agentrun-control-plane", "hwlab-runtime-lane"] as const);
|
||||
const pipelineRunPath = sourceArtifactPath(y.stringField(value, "pipelineRunPath", path), `${path}.pipelineRunPath`);
|
||||
const pipelinePath = value.pipelinePath === undefined ? null : sourceArtifactPath(y.stringField(value, "pipelinePath", path), `${path}.pipelinePath`);
|
||||
const taskRunTemplate = y.objectField(value, "taskRunTemplate", path);
|
||||
if (mode === "embedded-pipeline-spec" && pipelinePath !== null) throw new Error(`${path}.pipelinePath is forbidden for embedded-pipeline-spec`);
|
||||
if (mode === "remote-pipeline-annotation" && pipelinePath === null) throw new Error(`${path}.pipelinePath is required for remote-pipeline-annotation`);
|
||||
if (renderer === "agentrun-control-plane" && mode !== "embedded-pipeline-spec") throw new Error(`${path}.renderer agentrun-control-plane requires embedded-pipeline-spec`);
|
||||
@@ -453,6 +465,11 @@ function parseSourceArtifact(value: Record<string, unknown>, path: string): PacS
|
||||
pipelineRunPath,
|
||||
pipelinePath,
|
||||
maxKeepRuns: positiveInteger(value, "maxKeepRuns", path),
|
||||
taskRunTemplate: {
|
||||
hostNetwork: y.booleanField(taskRunTemplate, "hostNetwork", `${path}.taskRunTemplate`),
|
||||
dnsPolicy: y.enumField(taskRunTemplate, "dnsPolicy", `${path}.taskRunTemplate`, ["ClusterFirst", "ClusterFirstWithHostNet", "Default", "None"] as const),
|
||||
fsGroup: positiveInteger(taskRunTemplate, "fsGroup", `${path}.taskRunTemplate`),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -466,7 +483,8 @@ async function observeSourceArtifactRuntime(
|
||||
target: PacTarget,
|
||||
consumer: PacConsumer,
|
||||
desiredSpec: Record<string, unknown>,
|
||||
provenance: { readonly configRef: string; readonly effectiveConfigSha256: string; readonly renderer: string },
|
||||
desiredWrapper: Record<string, unknown> | null,
|
||||
provenance: { readonly configRef: string; readonly effectiveConfigSha256: string; readonly renderer: string; readonly mode: string },
|
||||
sourceCommit: string | null,
|
||||
): Promise<PacSourceArtifactRuntimeObservation> {
|
||||
const script = renderPacSourceArtifactRuntimeObserverShell({
|
||||
@@ -474,24 +492,43 @@ async function observeSourceArtifactRuntime(
|
||||
pipeline: consumer.pipeline,
|
||||
pipelineRunPrefix: consumer.pipelineRunPrefix,
|
||||
desiredSpec: canonicalizePacPipelineSpec(desiredSpec),
|
||||
desiredWrapper: desiredWrapper === null ? null : canonicalizePacPipelineSpec(desiredWrapper),
|
||||
provenance,
|
||||
sourceCommit,
|
||||
});
|
||||
|
||||
const captured = await capture(config, target.route, ["sh"], script);
|
||||
if (captured.exitCode !== 0) {
|
||||
const compact = compactCapture(captured);
|
||||
const detail = String(compact.stderrTail ?? compact.stdoutTail ?? "").trim().split(/\r?\n/u).filter(Boolean)[0]?.replace(/\s+/gu, " ").slice(0, 240);
|
||||
return unavailableSourceArtifactRuntime(`runtime-transport-exit-${captured.exitCode}${detail ? `:${detail}` : ""}`, sourceCommit);
|
||||
return unavailableSourceArtifactRuntime(pacRuntimeTransportFailureReason(captured), sourceCommit);
|
||||
}
|
||||
const parsed = parseJsonOutput(captured.stdout);
|
||||
if (parsed === null) return unavailableSourceArtifactRuntime("runtime-observer-invalid-json", sourceCommit);
|
||||
return {
|
||||
live: runtimeSourceArtifactItem(parsed.live),
|
||||
embedded: runtimeSourceArtifactItem(parsed.embedded),
|
||||
live: pacRuntimeSourceArtifactItem(parsed.live, {
|
||||
configRef: provenance.configRef,
|
||||
effectiveConfigSha256: provenance.effectiveConfigSha256,
|
||||
renderer: provenance.renderer,
|
||||
mode: provenance.mode,
|
||||
sourceCommit: null,
|
||||
exactName: consumer.pipeline,
|
||||
namePrefix: null,
|
||||
}),
|
||||
embedded: pacRuntimeSourceArtifactItem(parsed.embedded, {
|
||||
configRef: provenance.configRef,
|
||||
effectiveConfigSha256: provenance.effectiveConfigSha256,
|
||||
renderer: provenance.renderer,
|
||||
mode: provenance.mode,
|
||||
sourceCommit,
|
||||
exactName: null,
|
||||
namePrefix: consumer.pipelineRunPrefix,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export function pacRuntimeTransportFailureReason(captured: { readonly exitCode: number; readonly stdout: string; readonly stderr: string }): string {
|
||||
return `runtime-transport-exit-${captured.exitCode}:stderr(${pacValueEvidence(captured.stderr)}):stdout(${pacValueEvidence(captured.stdout)})`;
|
||||
}
|
||||
|
||||
export function renderPacSourceArtifactRuntimeObserverShell(inputValue: Record<string, unknown>): string {
|
||||
const input = JSON.stringify(inputValue);
|
||||
const observer = readFileSync(sourceArtifactRuntimeObserverFile, "utf8").trimEnd();
|
||||
@@ -508,32 +545,97 @@ NODE_PAC_SOURCE_ARTIFACT_STATUS
|
||||
`;
|
||||
}
|
||||
|
||||
function runtimeSourceArtifactItem(value: unknown): PacSourceArtifactRuntimeObservation["live"] {
|
||||
export function pacRuntimeSourceArtifactItem(value: unknown, expected: {
|
||||
readonly configRef: string;
|
||||
readonly effectiveConfigSha256: string;
|
||||
readonly renderer: string;
|
||||
readonly mode: string;
|
||||
readonly sourceCommit: string | null;
|
||||
readonly exactName: string | null;
|
||||
readonly namePrefix: string | null;
|
||||
}): PacSourceArtifactRuntimeObservation["live"] {
|
||||
if (typeof value !== "object" || value === null || Array.isArray(value)) return unavailableSourceArtifactRuntime("runtime-observer-invalid-item", null).live;
|
||||
const item = value as Record<string, unknown>;
|
||||
const status = item.status;
|
||||
if (status !== "aligned" && status !== "drifted" && status !== "missing" && status !== "unavailable") return unavailableSourceArtifactRuntime("runtime-observer-invalid-status", null).live;
|
||||
const drift = item.firstDrift;
|
||||
const observedName = typeof item.name === "string" && item.name.length <= 253 && /^[a-z0-9](?:[-a-z0-9.]*[a-z0-9])?$/u.test(item.name) ? item.name : null;
|
||||
const name = observedName !== null
|
||||
&& (expected.exactName === null || observedName === expected.exactName)
|
||||
&& (expected.namePrefix === null || observedName.startsWith(expected.namePrefix))
|
||||
? observedName
|
||||
: null;
|
||||
const provenanceAligned = item.configRef === expected.configRef
|
||||
&& item.effectiveConfigSha256 === expected.effectiveConfigSha256
|
||||
&& item.renderer === expected.renderer
|
||||
&& item.mode === expected.mode;
|
||||
return {
|
||||
status,
|
||||
name: typeof item.name === "string" ? item.name : null,
|
||||
canonicalSha256: typeof item.canonicalSha256 === "string" ? item.canonicalSha256 : null,
|
||||
name,
|
||||
canonicalSha256: typeof item.canonicalSha256 === "string" && /^sha256:[0-9a-f]{64}$/u.test(item.canonicalSha256) ? item.canonicalSha256 : null,
|
||||
firstDrift: typeof drift === "object" && drift !== null && !Array.isArray(drift)
|
||||
? {
|
||||
path: String((drift as Record<string, unknown>).path ?? "$"),
|
||||
expected: String((drift as Record<string, unknown>).expected ?? ""),
|
||||
actual: String((drift as Record<string, unknown>).actual ?? ""),
|
||||
path: pacRuntimeSafePath((drift as Record<string, unknown>).path),
|
||||
expected: pacRuntimeSafeEvidence((drift as Record<string, unknown>).expected),
|
||||
actual: pacRuntimeSafeEvidence((drift as Record<string, unknown>).actual),
|
||||
}
|
||||
: null,
|
||||
provenanceAligned: item.provenanceAligned === true,
|
||||
configRef: typeof item.configRef === "string" ? item.configRef : null,
|
||||
effectiveConfigSha256: typeof item.effectiveConfigSha256 === "string" ? item.effectiveConfigSha256 : null,
|
||||
sourceCommit: typeof item.sourceCommit === "string" ? item.sourceCommit : null,
|
||||
reason: typeof item.reason === "string" ? item.reason : null,
|
||||
provenanceAligned,
|
||||
configRef: item.configRef === expected.configRef && expected.configRef.length <= 512 && /^[A-Za-z0-9_./-]+#[A-Za-z0-9_.-]+$/u.test(expected.configRef) ? expected.configRef : null,
|
||||
effectiveConfigSha256: item.effectiveConfigSha256 === expected.effectiveConfigSha256 && /^sha256:[0-9a-f]{64}$/u.test(expected.effectiveConfigSha256) ? expected.effectiveConfigSha256 : null,
|
||||
sourceCommit: item.sourceCommit === expected.sourceCommit && (expected.sourceCommit === null || /^[0-9a-f]{40}$/u.test(expected.sourceCommit)) ? expected.sourceCommit : null,
|
||||
reason: typeof item.reason === "string" ? pacRuntimeSafeReason(item.reason) : null,
|
||||
candidateCount: typeof item.candidateCount === "number" && Number.isInteger(item.candidateCount) && item.candidateCount >= 0 ? item.candidateCount : 0,
|
||||
};
|
||||
}
|
||||
|
||||
export function pacRuntimeSafePath(value: unknown): string {
|
||||
const path = typeof value === "string" ? value : "$";
|
||||
const allowedSegments = new Set([
|
||||
"accessModes", "annotations", "apiVersion", "args", "command", "computeResources", "default", "description", "dnsPolicy", "env", "envFrom",
|
||||
"executionMode", "finally", "fsGroup", "generateName", "hostNetwork", "image", "kind", "labels", "length", "metadata", "mode", "mountPath", "name", "namespace", "onError", "operator",
|
||||
"optional", "params", "pipeline", "pipelineRef", "pipelineSpec", "podTemplate", "readinessProbe", "readOnly", "requests", "resources",
|
||||
"results", "retries", "runAfter", "script", "secret", "secretName", "securityContext", "serviceAccountName", "sidecars", "spec", "steps",
|
||||
"storage", "subPath", "taskRunTemplate", "taskSpec", "tasks", "timeout", "timeouts", "type", "value", "values", "volumeClaimTemplate",
|
||||
"volumeMounts", "volumes", "when", "workspaces", "workingDir", "wrapper",
|
||||
]);
|
||||
if (!path.startsWith("$")) return `path(${pacValueEvidence(path)})`;
|
||||
const token = /(?:\.([A-Za-z0-9_-]+))|(?:\[(\d+)\])/gu;
|
||||
let offset = 1;
|
||||
for (const match of path.slice(1).matchAll(token)) {
|
||||
if (match.index !== offset - 1) return `path(${pacValueEvidence(path)})`;
|
||||
if (match[1] !== undefined && !allowedSegments.has(match[1])) return `path(${pacValueEvidence(path)})`;
|
||||
offset = 1 + (match.index ?? 0) + match[0].length;
|
||||
}
|
||||
return offset === path.length ? path : `path(${pacValueEvidence(path)})`;
|
||||
}
|
||||
|
||||
function pacRuntimeSafeEvidence(value: unknown): string {
|
||||
return typeof value === "string" && /^type=[a-z]+,length=\d+,sha256=[0-9a-f]{64}$/u.test(value)
|
||||
? value
|
||||
: pacValueEvidence(value);
|
||||
}
|
||||
|
||||
export function pacRuntimeSafeReason(value: string): string {
|
||||
const fixed = new Set([
|
||||
"source-commit-not-specified",
|
||||
"source-commit-run-not-found",
|
||||
"source-commit-identity-conflict",
|
||||
"pipeline-get-not-found",
|
||||
"pipelinerun-list-not-found",
|
||||
"pipelinerun-list-invalid-shape",
|
||||
"runtime-observer-invalid-json",
|
||||
"runtime-observer-invalid-item",
|
||||
"runtime-observer-invalid-status",
|
||||
"runtime-observer-not-configured",
|
||||
]);
|
||||
if (fixed.has(value)) return value;
|
||||
if (/^source-commit-run-conflict:\d+$/u.test(value)) return value;
|
||||
if (/^(?:pipeline-get|pipelinerun-list)-command-failed:(?:unknown|\d+):type=string,length=\d+,sha256=[0-9a-f]{64}$/u.test(value)) return value;
|
||||
if (/^(?:runtime-observer-input-error|runtime-observer-reason):type=string,length=\d+,sha256=[0-9a-f]{64}$/u.test(value)) return value;
|
||||
return `runtime-observer-reason:${pacValueEvidence(value)}`;
|
||||
}
|
||||
|
||||
function unavailableSourceArtifactRuntime(reason: string, sourceCommit: string | null): PacSourceArtifactRuntimeObservation {
|
||||
const item = { status: "unavailable" as const, name: null, canonicalSha256: null, firstDrift: null, provenanceAligned: false, configRef: null, effectiveConfigSha256: null, sourceCommit, reason, candidateCount: 0 };
|
||||
return { live: item, embedded: item };
|
||||
|
||||
Reference in New Issue
Block a user