witnora 0.13.0 → 0.13.2

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/dist/cli.js CHANGED
@@ -37,6 +37,7 @@ import { importGenericEval, renderGenericEvalReport } from "./generic-eval.js";
37
37
  import { runDesignPartnerCommand } from "./design-partner-v02.js";
38
38
  import { runOnboard } from "./onboard.js";
39
39
  import { inspectRepository } from "./onboard.js";
40
+ import { renderReleaseEvaluation, runReleaseEvaluation } from "./release-evaluation.js";
40
41
  import { runPrivateCapabilityDiscovery } from "./private-discovery.js";
41
42
  import { doctorCustomerGateway, initializeCustomerGateway, readManagedCustomerGatewayLogs, renderGatewayDoctor, renderManagedGatewayStatus, restartManagedCustomerGateway, runCustomerGateway, startManagedCustomerGateway, statusManagedCustomerGateway, stopManagedCustomerGateway, } from "./gateway.js";
42
43
  import { verifyEvidencePacketV02 } from "./evidence-v02.js";
@@ -158,6 +159,29 @@ else if (command === "onboard") {
158
159
  openBrowser: readBoolFlag("--no-browser") ? async () => { } : undefined,
159
160
  });
160
161
  }
162
+ else if (command === "release") {
163
+ const action = process.argv[3] ?? "help";
164
+ if (action !== "evaluate")
165
+ throw new Error("Use witnora release evaluate --case <assurance-case-id>.");
166
+ const assuranceCaseId = readFlag("--case");
167
+ if (!assuranceCaseId)
168
+ throw new Error("--case <assurance-case-id> is required.");
169
+ const connection = await resolveConnection({
170
+ name: readFlag("--connection"),
171
+ server: readFlag("--server"),
172
+ projectId: readFlag("--project"),
173
+ apiKey: readFlag("--api-key"),
174
+ });
175
+ const result = await runReleaseEvaluation({
176
+ connection,
177
+ assuranceCaseId,
178
+ repository: readFlag("--repo") ?? process.cwd(),
179
+ outDir: readFlag("--out"),
180
+ });
181
+ process.stdout.write(renderReleaseEvaluation(result));
182
+ if (!result.passed)
183
+ process.exitCode = 1;
184
+ }
161
185
  else if (command === "gateway") {
162
186
  const action = process.argv[3] ?? "help";
163
187
  if (action === "init") {
@@ -1,6 +1,27 @@
1
1
  export function renderCommandHelp(command) {
2
2
  if (command === "sandbox" || command === "browser-adapter")
3
3
  return undefined;
4
+ if (command === "release")
5
+ return `Usage:
6
+ witnora release evaluate --case <assurance-case-id>
7
+
8
+ Runs one allowlisted release-evaluation script already defined by this repository,
9
+ writes a Witnora evidence bundle, and attaches it to the selected self-service
10
+ release assurance case. Hosted configuration cannot supply executable commands.
11
+
12
+ Script selection order:
13
+ witnora:release, evals:run, test:ci, test
14
+
15
+ Options:
16
+ --case <id> Self-service release assurance case (required)
17
+ --connection <name> Saved Hosted connection
18
+ --repo <directory> Agent repository (default: current directory)
19
+ --out <directory> Evidence output directory
20
+ --server <url> Hosted Witnora base URL
21
+ --project <id> Hosted project ID
22
+ --api-key <key> Project API key (prefer the saved connection or secret manager)
23
+ --help, -h Show this help
24
+ `;
4
25
  if (command === "discover")
5
26
  return `Usage:
6
27
  witnora discover [--connection <name>] [--repo <directory>]
@@ -100,6 +100,8 @@ export async function pushEvidenceToControlPlane(options) {
100
100
  schemaVersion: bundle.schemaVersion,
101
101
  runId: run.id,
102
102
  });
103
+ if (options.releaseAssuranceCaseId)
104
+ query.set("assuranceCaseId", options.releaseAssuranceCaseId);
103
105
  const evidence = await requestJson(request, `${projectUrl}/evidence?${query}`, {
104
106
  method: "POST",
105
107
  headers: { ...headers, "content-type": "application/json" },
@@ -0,0 +1,331 @@
1
+ import { spawn } from "node:child_process";
2
+ import { createHash, randomUUID } from "node:crypto";
3
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
4
+ import { join, relative, resolve } from "node:path";
5
+ import { withArtifactManifest } from "./artifact-manifest.js";
6
+ import { buildEvidenceBundle } from "./bundle.js";
7
+ import { collectCompanionArtifacts } from "./companion-artifacts.js";
8
+ import { pushEvidenceToControlPlane } from "./control-plane.js";
9
+ const RELEASE_SCRIPT_CANDIDATES = ["witnora:release", "evals:run", "test:ci", "test"];
10
+ const DEFAULT_TIMEOUT_MS = 10 * 60 * 1000;
11
+ const MAX_OUTPUT_BYTES = 1024 * 1024;
12
+ export async function runReleaseEvaluation(options) {
13
+ const repository = resolve(options.repository ?? process.cwd());
14
+ const assuranceCase = await loadReleaseAssuranceCase(options.connection, options.assuranceCaseId, options.fetch ?? fetch);
15
+ assertEvaluatingSelfServiceCase(assuranceCase);
16
+ const script = await detectReleaseScript(repository);
17
+ const timeoutMs = normalizeTimeout(options.timeoutMs);
18
+ const executedAt = (options.now ?? (() => new Date()))();
19
+ const execution = await (options.runner ?? runNpmScript)({ repository, script, timeoutMs });
20
+ const outputDirectory = resolve(options.outDir ?? join(repository, ".witnora", "release", assuranceCase.id));
21
+ await mkdir(outputDirectory, { recursive: true });
22
+ const logPath = join(outputDirectory, "evaluation.log");
23
+ const evaluationArtifactPath = join(outputDirectory, "evaluation-output.json");
24
+ const evidencePath = join(outputDirectory, "agentcert-evidence.json");
25
+ await writeFile(logPath, renderEvaluationLog(script, execution), "utf8");
26
+ await writeFile(evaluationArtifactPath, `${JSON.stringify(renderEvaluationArtifact(script, execution), null, 2)}\n`, "utf8");
27
+ const result = releaseEvaluationResult({
28
+ assuranceCase,
29
+ script,
30
+ execution,
31
+ timestamp: executedAt.toISOString(),
32
+ runId: (options.randomId ?? randomUUID)(),
33
+ artifactPath: relative(repository, evaluationArtifactPath).replaceAll("\\", "/"),
34
+ });
35
+ let bundle = buildEvidenceBundle([result], assuranceCase.subject.name, assuranceCase.subject.kind);
36
+ bundle.runId = `witnora_release_${result.runId}`;
37
+ bundle.generatedAt = executedAt.toISOString();
38
+ const companions = await collectCompanionArtifacts(bundle, repository);
39
+ bundle = withArtifactManifest(bundle, companions.artifacts);
40
+ const evidenceBytes = new TextEncoder().encode(`${JSON.stringify(bundle, null, 2)}\n`);
41
+ await writeFile(evidencePath, evidenceBytes);
42
+ const hosted = await (options.push ?? pushEvidenceToControlPlane)({
43
+ baseUrl: options.connection.server,
44
+ projectId: options.connection.projectId,
45
+ apiKey: options.connection.apiKey,
46
+ bundle,
47
+ evidenceBytes,
48
+ fileName: "agentcert-evidence.json",
49
+ companionArtifacts: companions.artifacts,
50
+ skippedCompanionArtifacts: companions.skipped,
51
+ releaseAssuranceCaseId: assuranceCase.id,
52
+ });
53
+ return {
54
+ assuranceCaseId: assuranceCase.id,
55
+ script,
56
+ passed: execution.exitCode === 0 && !execution.timedOut,
57
+ exitCode: execution.exitCode,
58
+ timedOut: execution.timedOut,
59
+ evidencePath,
60
+ logPath,
61
+ hostedRunId: hosted.runId,
62
+ hostedEvidenceId: hosted.evidenceId,
63
+ };
64
+ }
65
+ export function renderReleaseEvaluation(outcome) {
66
+ const status = outcome.passed ? "PASSED" : "FAILED";
67
+ return [
68
+ `Release evaluation: ${status}`,
69
+ `Local script: npm run ${outcome.script}`,
70
+ `Evidence: ${outcome.evidencePath}`,
71
+ `Log: ${outcome.logPath}`,
72
+ `Hosted run: ${outcome.hostedRunId}`,
73
+ `Attached to release assurance case: ${outcome.assuranceCaseId}`,
74
+ outcome.passed
75
+ ? "Next: return to Release Assurance. Witnora will enable customer review after the attached evidence is verified."
76
+ : "Next: inspect the log, fix the failing evaluation, and run the same command again. The failed evidence remains part of the audit trail.",
77
+ "",
78
+ ].join("\n");
79
+ }
80
+ async function loadReleaseAssuranceCase(connection, caseId, request) {
81
+ const response = await request(`${connection.server.replace(/\/$/, "")}/v1/projects/${encodeURIComponent(connection.projectId)}/assurance-cases/${encodeURIComponent(caseId)}`, { headers: { authorization: `Bearer ${connection.apiKey}` } });
82
+ if (!response.ok) {
83
+ const body = await response.text();
84
+ let message = body || `HTTP ${response.status}`;
85
+ try {
86
+ const parsed = JSON.parse(body);
87
+ message = parsed.error ?? parsed.message ?? message;
88
+ }
89
+ catch {
90
+ // Preserve the plain response body.
91
+ }
92
+ throw new Error(`Could not load release assurance case ${caseId}: ${message}`);
93
+ }
94
+ const payload = await response.json();
95
+ if (!payload.assuranceCase)
96
+ throw new Error(`Release assurance case ${caseId} was not returned by Hosted.`);
97
+ return payload.assuranceCase;
98
+ }
99
+ function assertEvaluatingSelfServiceCase(assuranceCase) {
100
+ if (assuranceCase.engagement) {
101
+ throw new Error("This case is managed by an independent review engagement. Use the reviewer evidence workflow instead.");
102
+ }
103
+ if (assuranceCase.status !== "evaluating") {
104
+ throw new Error(`Release assurance case ${assuranceCase.id} is ${assuranceCase.status}, not evaluating.`);
105
+ }
106
+ }
107
+ async function detectReleaseScript(repository) {
108
+ let raw;
109
+ try {
110
+ raw = await readFile(join(repository, "package.json"), "utf8");
111
+ }
112
+ catch (error) {
113
+ if (error.code === "ENOENT") {
114
+ throw new Error("No package.json was found. Add a local `witnora:release` npm script that runs the repository's existing release evaluation.");
115
+ }
116
+ throw error;
117
+ }
118
+ let packageJson;
119
+ try {
120
+ packageJson = JSON.parse(raw);
121
+ }
122
+ catch {
123
+ throw new Error("package.json is not valid JSON.");
124
+ }
125
+ const script = RELEASE_SCRIPT_CANDIDATES.find((candidate) => typeof packageJson.scripts?.[candidate] === "string");
126
+ if (!script) {
127
+ throw new Error("No supported release evaluation script was found. Add `witnora:release`, or expose an existing `evals:run`, `test:ci`, or `test` npm script.");
128
+ }
129
+ return script;
130
+ }
131
+ async function runNpmScript(input) {
132
+ return new Promise((resolveRun, rejectRun) => {
133
+ const launch = buildReleaseEvaluationLaunch(input.script);
134
+ const child = spawn(launch.command, launch.arguments, {
135
+ cwd: input.repository,
136
+ detached: launch.detached,
137
+ shell: false,
138
+ windowsHide: true,
139
+ stdio: ["ignore", "pipe", "pipe"],
140
+ });
141
+ let stdout = "";
142
+ let stderr = "";
143
+ let timedOut = false;
144
+ let settled = false;
145
+ let forceTimer;
146
+ const finish = (result) => {
147
+ if (settled)
148
+ return;
149
+ settled = true;
150
+ clearTimeout(timer);
151
+ if (forceTimer)
152
+ clearTimeout(forceTimer);
153
+ resolveRun(result);
154
+ };
155
+ const timer = setTimeout(() => {
156
+ timedOut = true;
157
+ terminateProcessTree(child.pid);
158
+ forceTimer = setTimeout(() => {
159
+ terminateProcessTree(child.pid, true);
160
+ finish({ exitCode: 1, stdout, stderr, timedOut: true });
161
+ }, 5_000);
162
+ }, input.timeoutMs);
163
+ child.stdout.on("data", (chunk) => { stdout = appendBounded(stdout, chunk); });
164
+ child.stderr.on("data", (chunk) => { stderr = appendBounded(stderr, chunk); });
165
+ child.once("error", (error) => {
166
+ if (settled)
167
+ return;
168
+ settled = true;
169
+ clearTimeout(timer);
170
+ if (forceTimer)
171
+ clearTimeout(forceTimer);
172
+ rejectRun(error);
173
+ });
174
+ child.once("close", (code) => {
175
+ finish({ exitCode: code ?? 1, stdout, stderr, timedOut });
176
+ });
177
+ });
178
+ }
179
+ export function buildReleaseEvaluationLaunch(script, platform = process.platform, env = process.env) {
180
+ if (!RELEASE_SCRIPT_CANDIDATES.includes(script)) {
181
+ throw new Error(`Unsupported release evaluation script: ${script}`);
182
+ }
183
+ if (platform === "win32") {
184
+ const command = env.ComSpec ?? env.COMSPEC;
185
+ if (!command) {
186
+ throw new Error("Windows command processor is unavailable because ComSpec is not set.");
187
+ }
188
+ return {
189
+ command,
190
+ arguments: ["/d", "/s", "/c", "npm.cmd", "run", script],
191
+ detached: false,
192
+ };
193
+ }
194
+ return {
195
+ command: "npm",
196
+ arguments: ["run", script],
197
+ detached: true,
198
+ };
199
+ }
200
+ function terminateProcessTree(pid, force = false) {
201
+ if (!pid)
202
+ return;
203
+ if (process.platform === "win32") {
204
+ const taskkill = spawn("taskkill", ["/pid", String(pid), "/t", "/f"], {
205
+ shell: false,
206
+ windowsHide: true,
207
+ stdio: "ignore",
208
+ });
209
+ taskkill.on("error", () => undefined);
210
+ return;
211
+ }
212
+ try {
213
+ process.kill(-pid, force ? "SIGKILL" : "SIGTERM");
214
+ }
215
+ catch {
216
+ // The process may have exited between the timeout and termination attempt.
217
+ }
218
+ }
219
+ function appendBounded(current, chunk) {
220
+ if (Buffer.byteLength(current) >= MAX_OUTPUT_BYTES)
221
+ return current;
222
+ const remaining = MAX_OUTPUT_BYTES - Buffer.byteLength(current);
223
+ return current + chunk.subarray(0, remaining).toString("utf8");
224
+ }
225
+ function normalizeTimeout(value) {
226
+ const timeout = value ?? DEFAULT_TIMEOUT_MS;
227
+ if (!Number.isSafeInteger(timeout) || timeout < 1_000 || timeout > 30 * 60 * 1000) {
228
+ throw new Error("Release evaluation timeout must be between 1 second and 30 minutes.");
229
+ }
230
+ return timeout;
231
+ }
232
+ function renderEvaluationLog(script, result) {
233
+ return [
234
+ `command=npm run ${script}`,
235
+ `exitCode=${result.exitCode}`,
236
+ `timedOut=${result.timedOut}`,
237
+ "",
238
+ "[stdout]",
239
+ result.stdout,
240
+ "",
241
+ "[stderr]",
242
+ result.stderr,
243
+ "",
244
+ ].join("\n");
245
+ }
246
+ function renderEvaluationArtifact(script, result) {
247
+ const stdout = redactEvaluationOutput(result.stdout);
248
+ const stderr = redactEvaluationOutput(result.stderr);
249
+ return {
250
+ schemaVersion: "witnora.release_evaluation_output.v0.1",
251
+ command: { executable: "npm", arguments: ["run", script] },
252
+ exitCode: result.exitCode,
253
+ timedOut: result.timedOut,
254
+ stdout: outputDescriptor(result.stdout, stdout.value),
255
+ stderr: outputDescriptor(result.stderr, stderr.value),
256
+ redaction: {
257
+ policyVersion: "witnora.release_output_redaction.v0.1",
258
+ replacements: stdout.replacements + stderr.replacements,
259
+ rawOutputRetainedLocally: true,
260
+ },
261
+ };
262
+ }
263
+ function outputDescriptor(raw, redacted) {
264
+ return {
265
+ sha256: createHash("sha256").update(raw).digest("hex"),
266
+ byteLength: Buffer.byteLength(raw),
267
+ excerpt: redacted,
268
+ };
269
+ }
270
+ function redactEvaluationOutput(value) {
271
+ const patterns = [
272
+ /\bsk-(?:proj-|live_|test_)?[A-Za-z0-9_-]{16,}\b/g,
273
+ /\brk_(?:live|test)_[A-Za-z0-9]{12,}\b/g,
274
+ /\bnpm_[A-Za-z0-9]{16,}\b/g,
275
+ /\bac_(?:live|test)_[A-Za-z0-9_-]{12,}\b/g,
276
+ /\bBearer\s+[A-Za-z0-9._~+/=-]{12,}\b/gi,
277
+ ];
278
+ let redacted = value;
279
+ let replacements = 0;
280
+ for (const pattern of patterns) {
281
+ redacted = redacted.replace(pattern, () => {
282
+ replacements += 1;
283
+ return "[REDACTED_SECRET]";
284
+ });
285
+ }
286
+ return { value: redacted, replacements };
287
+ }
288
+ function releaseEvaluationResult(input) {
289
+ const passed = input.execution.exitCode === 0 && !input.execution.timedOut;
290
+ return {
291
+ schemaVersion: "1",
292
+ product: "agentcert-cli",
293
+ runId: input.runId,
294
+ timestamp: input.timestamp,
295
+ phase: "pre-release",
296
+ score: passed ? 100 : 0,
297
+ passed,
298
+ summary: passed
299
+ ? `The repository's existing ${input.script} evaluation completed successfully.`
300
+ : `The repository's existing ${input.script} evaluation failed${input.execution.timedOut ? " after timing out" : ""}.`,
301
+ evidenceStrength: {
302
+ schemaVersion: "agentcert.evidence_strength.v0.1",
303
+ level: "reported",
304
+ claims: ["A customer-owned repository evaluation produced a captured process result."],
305
+ limitations: ["This local command result is not independent outcome verification or an independent Witnora review."],
306
+ trustVector: {
307
+ schemaVersion: "witnora.evidence_trust_vector.v0.1",
308
+ capture: "self_reported",
309
+ mediation: "none",
310
+ outcome: "unverified",
311
+ completeness: "partial",
312
+ attestation: "unsigned",
313
+ review: "none",
314
+ limitations: ["The repository controls the evaluated script and its reported outcome."],
315
+ },
316
+ },
317
+ artifacts: { evaluationOutput: input.artifactPath },
318
+ evidence: [{
319
+ id: `release-evaluation-${input.runId}`,
320
+ kind: "release_evaluation",
321
+ severity: passed ? "info" : "high",
322
+ message: passed
323
+ ? `npm run ${input.script} completed with exit code 0.`
324
+ : `npm run ${input.script} completed with exit code ${input.execution.exitCode}${input.execution.timedOut ? " after timing out" : ""}.`,
325
+ source: "witnora-cli",
326
+ artifactPath: input.artifactPath,
327
+ suggestedFix: passed ? undefined : "Inspect the complete local evaluation log and the uploaded redacted summary, correct the failure, and rerun this release evaluation.",
328
+ metadata: { assuranceCaseId: input.assuranceCase.id, script: input.script, timedOut: input.execution.timedOut },
329
+ }],
330
+ };
331
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "witnora",
3
- "version": "0.13.0",
3
+ "version": "0.13.2",
4
4
  "description": "Independent assurance for covered agent action paths across models and frameworks.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",