witnora 0.8.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +275 -12
- package/dist/artifact-manifest.js +37 -0
- package/dist/artifact-validation.js +102 -0
- package/dist/badge.js +31 -0
- package/dist/browser-adapter.js +156 -0
- package/dist/bundle.js +100 -0
- package/dist/cli.js +971 -0
- package/dist/command-help.js +71 -0
- package/dist/companion-artifacts.js +170 -0
- package/dist/conformance.js +115 -0
- package/dist/control-plane.js +409 -0
- package/dist/corpus-governance.js +126 -0
- package/dist/corpus-store.js +239 -0
- package/dist/corpus.js +438 -0
- package/dist/credentials.js +127 -0
- package/dist/evidence-signing.js +85 -0
- package/dist/failure-review.js +188 -0
- package/dist/guided-setup.js +101 -0
- package/dist/index.js +16 -0
- package/dist/lab.js +323 -0
- package/dist/local-server.js +491 -0
- package/dist/monitor.js +100 -0
- package/dist/normalizers.js +265 -0
- package/dist/onboarding-templates.js +158 -0
- package/dist/release-gate.js +457 -0
- package/dist/report.js +165 -0
- package/dist/runner.js +349 -0
- package/dist/sandbox.js +424 -0
- package/dist/schema-validator.js +503 -0
- package/dist/try.js +135 -0
- package/dist/types.js +3 -0
- package/dist/vendor/onegent-runtime/browser-adapter-kit.d.ts +107 -0
- package/dist/vendor/onegent-runtime/browser-adapter-kit.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/browser-adapter-kit.js +187 -0
- package/dist/vendor/onegent-runtime/browser-enforcement-runtime.d.ts +183 -0
- package/dist/vendor/onegent-runtime/browser-enforcement-runtime.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/browser-enforcement-runtime.js +344 -0
- package/dist/vendor/onegent-runtime/controlled-adapter.d.ts +6 -0
- package/dist/vendor/onegent-runtime/controlled-adapter.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/controlled-adapter.js +40 -0
- package/dist/vendor/onegent-runtime/mock-procurement.d.ts +6 -0
- package/dist/vendor/onegent-runtime/mock-procurement.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/mock-procurement.js +33 -0
- package/dist/vendor/onegent-runtime/policies.d.ts +6 -0
- package/dist/vendor/onegent-runtime/policies.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/policies.js +109 -0
- package/dist/vendor/onegent-runtime/risk.d.ts +3 -0
- package/dist/vendor/onegent-runtime/risk.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/risk.js +54 -0
- package/dist/vendor/onegent-runtime/sandbox-adapter-kit.d.ts +45 -0
- package/dist/vendor/onegent-runtime/sandbox-adapter-kit.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/sandbox-adapter-kit.js +148 -0
- package/dist/vendor/onegent-runtime/sandbox-harness.d.ts +167 -0
- package/dist/vendor/onegent-runtime/sandbox-harness.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/sandbox-harness.js +729 -0
- package/dist/vendor/onegent-runtime/sandbox-hosted.d.ts +77 -0
- package/dist/vendor/onegent-runtime/sandbox-hosted.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/sandbox-hosted.js +152 -0
- package/dist/vendor/onegent-runtime/sdk.d.ts +29 -0
- package/dist/vendor/onegent-runtime/sdk.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/sdk.js +140 -0
- package/dist/vendor/onegent-runtime/service.d.ts +27 -0
- package/dist/vendor/onegent-runtime/service.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/service.js +423 -0
- package/dist/vendor/onegent-runtime/store.d.ts +16 -0
- package/dist/vendor/onegent-runtime/store.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/store.js +29 -0
- package/dist/vendor/onegent-runtime/stripe-test-readonly.d.ts +79 -0
- package/dist/vendor/onegent-runtime/stripe-test-readonly.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/stripe-test-readonly.js +163 -0
- package/dist/vendor/onegent-runtime/trust-crypto.d.ts +11 -0
- package/dist/vendor/onegent-runtime/trust-crypto.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/trust-crypto.js +70 -0
- package/dist/vendor/onegent-runtime/trust-types.d.ts +163 -0
- package/dist/vendor/onegent-runtime/trust-types.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/trust-types.js +7 -0
- package/dist/vendor/onegent-runtime/trusted-recorder.d.ts +49 -0
- package/dist/vendor/onegent-runtime/trusted-recorder.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/trusted-recorder.js +347 -0
- package/dist/vendor/onegent-runtime/types.d.ts +287 -0
- package/dist/vendor/onegent-runtime/types.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/types.js +1 -0
- package/dist/vendor/onegent-runtime/vendor-sandbox-egress.d.ts +49 -0
- package/dist/vendor/onegent-runtime/vendor-sandbox-egress.d.ts.map +1 -0
- package/dist/vendor/onegent-runtime/vendor-sandbox-egress.js +152 -0
- package/package.json +33 -13
- package/bin/witnora.js +0 -3
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export function renderCommandHelp(command) {
|
|
2
|
+
if (command === "sandbox" || command === "browser-adapter")
|
|
3
|
+
return undefined;
|
|
4
|
+
if (command === "setup")
|
|
5
|
+
return `Usage:
|
|
6
|
+
witnora setup apply --plan witnora-setup-plan.json --repo .
|
|
7
|
+
witnora setup apply --plan witnora-setup-plan.json --repo . --github-pr
|
|
8
|
+
witnora setup apply --plan witnora-setup-plan.json --repo . --github-pr --configure-ci
|
|
9
|
+
|
|
10
|
+
Applies only allowlisted Witnora templates from a typed setup plan. Commands embedded in the plan are never executed.
|
|
11
|
+
--github-pr requires authenticated git and GitHub CLI, creates a reviewable branch, and never writes API keys.
|
|
12
|
+
--configure-ci additionally reads WITNORA_PROJECT_ID and WITNORA_API_KEY from the environment, then stores them through GitHub Variables and Secrets. Legacy AGENTCERT_* names remain supported. The API key is passed to gh on standard input.
|
|
13
|
+
`;
|
|
14
|
+
if (command === "try")
|
|
15
|
+
return `Usage:
|
|
16
|
+
witnora try [--template <browser|coding|mcp|workflow|data>] [--subject <name>]
|
|
17
|
+
witnora try --template workflow --push
|
|
18
|
+
|
|
19
|
+
Runs a deterministic local sample without an agent, model key, or external system.
|
|
20
|
+
The output demonstrates the Witnora evidence path only; it is not a customer-agent evaluation.
|
|
21
|
+
|
|
22
|
+
Options:
|
|
23
|
+
--template <type> Sample boundary (default: workflow)
|
|
24
|
+
--subject <name> Sample identity (default: witnora-sample-<template>)
|
|
25
|
+
--out <directory> Output directory (default: .witnora/try)
|
|
26
|
+
--push Upload after a hosted project has been connected
|
|
27
|
+
--help, -h Show this help
|
|
28
|
+
`;
|
|
29
|
+
if (command === "init")
|
|
30
|
+
return `Usage:
|
|
31
|
+
witnora init --template <browser|coding|mcp|workflow|data> [--subject <name>]
|
|
32
|
+
witnora init --template browser --github-action
|
|
33
|
+
|
|
34
|
+
Options:
|
|
35
|
+
--template <type> External agent boundary (default: browser)
|
|
36
|
+
--subject <name> Agent identity (default: my-<template>-agent)
|
|
37
|
+
--out <path> Profile output (default: witnora.config.json)
|
|
38
|
+
--adapter-out <path> Envelope adapter for coding/workflow/data
|
|
39
|
+
--github-action Write the browser Tripwire workflow
|
|
40
|
+
--force Replace existing starter files
|
|
41
|
+
--help, -h Show this help without writing files
|
|
42
|
+
`;
|
|
43
|
+
if (command === "conformance")
|
|
44
|
+
return `Usage:
|
|
45
|
+
witnora conformance <evidence.json> --artifact-root <directory> [--implementation <name>] [--out <report.json>]
|
|
46
|
+
|
|
47
|
+
Checks schema identity, v0.1 compatibility, artifact manifest structure, and exact artifact hashes and sizes.
|
|
48
|
+
`;
|
|
49
|
+
if (command !== "push")
|
|
50
|
+
return undefined;
|
|
51
|
+
return `Usage:
|
|
52
|
+
witnora push --evidence .witnora/latest/agentcert-evidence.json
|
|
53
|
+
witnora push --evidence <path> --server <url> --project <project-id> [--api-key <key>]
|
|
54
|
+
|
|
55
|
+
Options:
|
|
56
|
+
--evidence <path> Evidence bundle (default: .witnora/latest/agentcert-evidence.json)
|
|
57
|
+
--connection <name> Saved hosted connection
|
|
58
|
+
--server <url> Hosted Witnora base URL
|
|
59
|
+
--project <id> Hosted project ID
|
|
60
|
+
--api-key <key> Project API key (prefer WITNORA_API_KEY in CI)
|
|
61
|
+
--external-id <id> Idempotent hosted run ID
|
|
62
|
+
--assurance-case <id> Issued assurance case to reconcile
|
|
63
|
+
--assurance-scope <p> Declared agent/model/prompt/tools/policy/suite scope JSON
|
|
64
|
+
--assurance-trigger <t> auto, pull_request, release, or nightly (default: auto)
|
|
65
|
+
--require-current Fail unless Hosted confirms authoritative CURRENT and complete evidence
|
|
66
|
+
--continuous-health-out <path> Write the redacted Hosted health contract
|
|
67
|
+
--artifact-root <dir> Allowed root for companion artifacts (default: current directory)
|
|
68
|
+
--no-artifacts Upload only the evidence bundle
|
|
69
|
+
--help, -h Show this help
|
|
70
|
+
`;
|
|
71
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { lstat, open, realpath, stat } from "node:fs/promises";
|
|
2
|
+
import { basename, extname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
3
|
+
import { collectEvidenceArtifactPaths, isRemoteArtifactPath, } from "./artifact-validation.js";
|
|
4
|
+
export const DEFAULT_COMPANION_ARTIFACT_LIMITS = {
|
|
5
|
+
maxFiles: 25,
|
|
6
|
+
maxFileBytes: 10 * 1024 * 1024,
|
|
7
|
+
maxTotalBytes: 50 * 1024 * 1024,
|
|
8
|
+
};
|
|
9
|
+
export const MAX_REPORTED_COMPANION_ARTIFACT_SKIPS = 50;
|
|
10
|
+
export async function collectCompanionArtifacts(bundle, artifactRoot, limits = DEFAULT_COMPANION_ARTIFACT_LIMITS) {
|
|
11
|
+
validateLimits(limits);
|
|
12
|
+
const root = await realpath(resolve(artifactRoot));
|
|
13
|
+
const artifacts = [];
|
|
14
|
+
const skipped = [];
|
|
15
|
+
const uploadedRealPaths = new Set();
|
|
16
|
+
let totalBytes = 0;
|
|
17
|
+
for (const entry of collectEvidenceArtifactPaths(bundle, root)) {
|
|
18
|
+
if (isRemoteArtifactPath(entry.sourcePath)) {
|
|
19
|
+
skipped.push(skip(entry, "remote_url"));
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
const descriptor = describeArtifact(entry.sourcePath);
|
|
23
|
+
if (!descriptor) {
|
|
24
|
+
skipped.push(skip(entry, "unsupported_type", "accepted formats: PNG, JPEG, WebP, JSON, JSONL, HTML, PDF, ZIP"));
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (artifacts.length >= limits.maxFiles) {
|
|
28
|
+
skipped.push(skip(entry, "file_limit", `maximum ${limits.maxFiles} files`));
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const candidate = isAbsolute(entry.sourcePath)
|
|
32
|
+
? resolve(entry.sourcePath)
|
|
33
|
+
: resolve(entry.root, entry.sourcePath);
|
|
34
|
+
if (!isWithinRoot(root, candidate)) {
|
|
35
|
+
skipped.push(skip(entry, "outside_artifact_root"));
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
let candidateInfo;
|
|
39
|
+
try {
|
|
40
|
+
candidateInfo = await lstat(candidate);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
skipped.push(skip(entry, errorCode(error) === "ENOENT" ? "missing" : "unreadable", errorMessage(error)));
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (candidateInfo.isSymbolicLink()) {
|
|
47
|
+
skipped.push(skip(entry, "symlink"));
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
let resolvedPath;
|
|
51
|
+
try {
|
|
52
|
+
resolvedPath = await realpath(candidate);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
skipped.push(skip(entry, errorCode(error) === "ENOENT" ? "missing" : "unreadable", errorMessage(error)));
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (!isWithinRoot(root, resolvedPath)) {
|
|
59
|
+
skipped.push(skip(entry, "outside_artifact_root"));
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (uploadedRealPaths.has(resolvedPath))
|
|
63
|
+
continue;
|
|
64
|
+
let fileInfo;
|
|
65
|
+
try {
|
|
66
|
+
fileInfo = await stat(resolvedPath);
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
skipped.push(skip(entry, errorCode(error) === "ENOENT" ? "missing" : "unreadable", errorMessage(error)));
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (!fileInfo.isFile()) {
|
|
73
|
+
skipped.push(skip(entry, "not_file"));
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (fileInfo.size > limits.maxFileBytes) {
|
|
77
|
+
skipped.push(skip(entry, "file_too_large", `${fileInfo.size} bytes exceeds ${limits.maxFileBytes}`));
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (totalBytes + fileInfo.size > limits.maxTotalBytes) {
|
|
81
|
+
skipped.push(skip(entry, "total_size_limit", `maximum ${limits.maxTotalBytes} bytes`));
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const remainingBytes = limits.maxTotalBytes - totalBytes;
|
|
85
|
+
let readResult;
|
|
86
|
+
try {
|
|
87
|
+
readResult = await readBounded(resolvedPath, Math.min(limits.maxFileBytes, remainingBytes));
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
skipped.push(skip(entry, "unreadable", errorMessage(error)));
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (readResult.exceeded) {
|
|
94
|
+
const reason = remainingBytes < limits.maxFileBytes ? "total_size_limit" : "file_too_large";
|
|
95
|
+
skipped.push(skip(entry, reason));
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
artifacts.push({
|
|
99
|
+
sourcePath: normalizeSourcePath(entry.sourcePath),
|
|
100
|
+
fileName: basename(resolvedPath),
|
|
101
|
+
kind: descriptor.kind,
|
|
102
|
+
contentType: descriptor.contentType,
|
|
103
|
+
bytes: readResult.bytes,
|
|
104
|
+
});
|
|
105
|
+
uploadedRealPaths.add(resolvedPath);
|
|
106
|
+
totalBytes += readResult.bytes.byteLength;
|
|
107
|
+
}
|
|
108
|
+
return { artifacts, skipped, totalBytes, limits: { ...limits } };
|
|
109
|
+
}
|
|
110
|
+
function validateLimits(limits) {
|
|
111
|
+
for (const [name, value] of Object.entries(limits)) {
|
|
112
|
+
if (!Number.isSafeInteger(value) || value <= 0)
|
|
113
|
+
throw new Error(`${name} must be a positive integer.`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function isWithinRoot(root, candidate) {
|
|
117
|
+
const pathFromRoot = relative(root, candidate);
|
|
118
|
+
return pathFromRoot === "" || (!pathFromRoot.startsWith(`..${sep}`) && pathFromRoot !== ".." && !isAbsolute(pathFromRoot));
|
|
119
|
+
}
|
|
120
|
+
async function readBounded(path, maxBytes) {
|
|
121
|
+
const handle = await open(path, "r");
|
|
122
|
+
const chunks = [];
|
|
123
|
+
let total = 0;
|
|
124
|
+
try {
|
|
125
|
+
while (total <= maxBytes) {
|
|
126
|
+
const capacity = Math.min(64 * 1024, maxBytes + 1 - total);
|
|
127
|
+
const chunk = Buffer.allocUnsafe(capacity);
|
|
128
|
+
const { bytesRead } = await handle.read(chunk, 0, capacity, null);
|
|
129
|
+
if (bytesRead === 0)
|
|
130
|
+
return { bytes: Buffer.concat(chunks, total), exceeded: false };
|
|
131
|
+
chunks.push(chunk.subarray(0, bytesRead));
|
|
132
|
+
total += bytesRead;
|
|
133
|
+
}
|
|
134
|
+
return { bytes: new Uint8Array(), exceeded: true };
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
await handle.close();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function describeArtifact(path) {
|
|
141
|
+
const lower = path.toLowerCase();
|
|
142
|
+
const extension = extname(lower);
|
|
143
|
+
if ([".png", ".jpg", ".jpeg", ".webp"].includes(extension)) {
|
|
144
|
+
const imageType = extension === ".jpg" ? "jpeg" : extension.slice(1);
|
|
145
|
+
return { kind: "screenshot", contentType: `image/${imageType}` };
|
|
146
|
+
}
|
|
147
|
+
if (extension === ".zip")
|
|
148
|
+
return { kind: "trace", contentType: "application/zip" };
|
|
149
|
+
if ([".html", ".htm"].includes(extension))
|
|
150
|
+
return { kind: "dom", contentType: "text/html; charset=utf-8" };
|
|
151
|
+
if (extension === ".json")
|
|
152
|
+
return { kind: lower.includes("trace") ? "trace" : lower.includes("dom") ? "dom" : "json", contentType: "application/json" };
|
|
153
|
+
if (extension === ".jsonl")
|
|
154
|
+
return { kind: lower.includes("trace") ? "trace" : "json", contentType: "application/x-ndjson" };
|
|
155
|
+
if (extension === ".pdf")
|
|
156
|
+
return { kind: "report", contentType: "application/pdf" };
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
function normalizeSourcePath(path) {
|
|
160
|
+
return path.replace(/\\/g, "/").slice(0, 1024);
|
|
161
|
+
}
|
|
162
|
+
function skip(entry, reason, detail) {
|
|
163
|
+
return { sourcePath: normalizeSourcePath(entry.sourcePath), reason, ...(detail ? { detail } : {}) };
|
|
164
|
+
}
|
|
165
|
+
function errorCode(error) {
|
|
166
|
+
return error && typeof error === "object" && "code" in error ? String(error.code) : undefined;
|
|
167
|
+
}
|
|
168
|
+
function errorMessage(error) {
|
|
169
|
+
return error instanceof Error ? error.message : String(error);
|
|
170
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { normalizeManifestPath } from "./artifact-manifest.js";
|
|
5
|
+
import { validateAgentCertSchema } from "./schema-validator.js";
|
|
6
|
+
const SUPPORTED_TOP_LEVEL_FIELDS = new Set([
|
|
7
|
+
"schemaName", "schemaVersion", "schemaSemver", "kind", "runId", "generatedAt", "subject", "verdict", "summary",
|
|
8
|
+
"results", "evidence", "artifacts", "artifactManifest", "standards",
|
|
9
|
+
]);
|
|
10
|
+
export async function runEvidenceConformance(input, options) {
|
|
11
|
+
const checks = [];
|
|
12
|
+
const schema = validateAgentCertSchema("evidence-bundle", input);
|
|
13
|
+
checks.push(check("schema", "Evidence bundle satisfies the AgentCert v0.1 semantic contract.", schema.errors));
|
|
14
|
+
const bundle = object(input);
|
|
15
|
+
const compatibilityErrors = bundle
|
|
16
|
+
? Object.keys(bundle).filter((key) => !SUPPORTED_TOP_LEVEL_FIELDS.has(key)).map((key) => `Unsupported top-level field: ${key}.`)
|
|
17
|
+
: ["Evidence bundle must be an object."];
|
|
18
|
+
checks.push(check("compatibility", "Evidence bundle uses only v0.1-compatible top-level fields.", compatibilityErrors));
|
|
19
|
+
const manifestErrors = [];
|
|
20
|
+
const entries = parseManifest(bundle?.artifactManifest, manifestErrors);
|
|
21
|
+
checks.push(check("manifest", "Artifact manifest declares unique normalized paths, SHA-256, byte size, and kind.", manifestErrors));
|
|
22
|
+
const byteErrors = [];
|
|
23
|
+
if (manifestErrors.length === 0) {
|
|
24
|
+
const root = resolve(options.artifactRoot);
|
|
25
|
+
for (const entry of entries) {
|
|
26
|
+
const path = resolve(root, entry.path);
|
|
27
|
+
if (path !== root && !path.startsWith(`${root}\\`) && !path.startsWith(`${root}/`)) {
|
|
28
|
+
byteErrors.push(`${entry.path}: resolved path escapes artifact root.`);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
const bytes = await readFile(path);
|
|
33
|
+
const digest = createHash("sha256").update(bytes).digest("hex");
|
|
34
|
+
if (bytes.byteLength !== entry.sizeBytes)
|
|
35
|
+
byteErrors.push(`${entry.path}: expected ${entry.sizeBytes} bytes, observed ${bytes.byteLength}.`);
|
|
36
|
+
if (digest !== entry.sha256)
|
|
37
|
+
byteErrors.push(`${entry.path}: SHA-256 does not match the declared digest.`);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
const code = error && typeof error === "object" && "code" in error ? String(error.code) : "read_failed";
|
|
41
|
+
byteErrors.push(`${entry.path}: artifact could not be read (${code}).`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
byteErrors.push("Artifact bytes were not checked because the manifest is invalid.");
|
|
47
|
+
}
|
|
48
|
+
checks.push(check("artifact-bytes", "Every declared artifact matches its exact runtime bytes.", byteErrors));
|
|
49
|
+
const passed = checks.filter((item) => item.status === "passed").length;
|
|
50
|
+
const failed = checks.length - passed;
|
|
51
|
+
return {
|
|
52
|
+
schemaVersion: "agentcert.conformance.v0.1",
|
|
53
|
+
kind: "agentcert.evidence_conformance_report",
|
|
54
|
+
implementation: options.implementation?.trim() || "third-party",
|
|
55
|
+
evidenceFile: resolve(options.evidenceFile),
|
|
56
|
+
artifactRoot: resolve(options.artifactRoot),
|
|
57
|
+
generatedAt: (options.now ?? new Date()).toISOString(),
|
|
58
|
+
valid: failed === 0,
|
|
59
|
+
summary: { passed, failed },
|
|
60
|
+
checks,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function parseManifest(input, errors) {
|
|
64
|
+
const manifest = object(input);
|
|
65
|
+
if (!manifest) {
|
|
66
|
+
errors.push("artifactManifest is required for conformance.");
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
if (manifest.schemaVersion !== "agentcert.artifact_manifest.v0.1")
|
|
70
|
+
errors.push("artifactManifest.schemaVersion must be agentcert.artifact_manifest.v0.1.");
|
|
71
|
+
if (!Array.isArray(manifest.entries)) {
|
|
72
|
+
errors.push("artifactManifest.entries must be an array.");
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
if (manifest.entries.length > 500)
|
|
76
|
+
errors.push("artifactManifest.entries cannot exceed 500 entries.");
|
|
77
|
+
const paths = new Set();
|
|
78
|
+
const entries = [];
|
|
79
|
+
manifest.entries.forEach((inputEntry, index) => {
|
|
80
|
+
const entry = object(inputEntry);
|
|
81
|
+
if (!entry) {
|
|
82
|
+
errors.push(`artifactManifest.entries[${index}] must be an object.`);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
let path;
|
|
86
|
+
try {
|
|
87
|
+
path = typeof entry.path === "string" ? normalizeManifestPath(entry.path) : undefined;
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
errors.push(`artifactManifest.entries[${index}].path: ${error.message}`);
|
|
91
|
+
}
|
|
92
|
+
if (!path)
|
|
93
|
+
errors.push(`artifactManifest.entries[${index}].path must be a normalized relative path.`);
|
|
94
|
+
else if (paths.has(path))
|
|
95
|
+
errors.push(`artifactManifest contains duplicate path ${path}.`);
|
|
96
|
+
else
|
|
97
|
+
paths.add(path);
|
|
98
|
+
if (typeof entry.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(entry.sha256))
|
|
99
|
+
errors.push(`artifactManifest.entries[${index}].sha256 must be 64 lowercase hex characters.`);
|
|
100
|
+
if (!Number.isInteger(entry.sizeBytes) || entry.sizeBytes < 0)
|
|
101
|
+
errors.push(`artifactManifest.entries[${index}].sizeBytes must be a non-negative integer.`);
|
|
102
|
+
if (typeof entry.kind !== "string" || !/^[a-z][a-z0-9_-]{0,63}$/.test(entry.kind))
|
|
103
|
+
errors.push(`artifactManifest.entries[${index}].kind must be a stable lowercase identifier.`);
|
|
104
|
+
if (path && typeof entry.sha256 === "string" && Number.isInteger(entry.sizeBytes) && typeof entry.kind === "string") {
|
|
105
|
+
entries.push({ path, sha256: entry.sha256, sizeBytes: entry.sizeBytes, kind: entry.kind });
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
return entries;
|
|
109
|
+
}
|
|
110
|
+
function check(id, message, errors) {
|
|
111
|
+
return { id, status: errors.length === 0 ? "passed" : "failed", message, errors };
|
|
112
|
+
}
|
|
113
|
+
function object(input) {
|
|
114
|
+
return input && typeof input === "object" && !Array.isArray(input) ? input : undefined;
|
|
115
|
+
}
|