witnora 0.12.1 → 0.13.1

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 CHANGED
@@ -13,21 +13,22 @@ outcome. It writes portable reports and accumulates a local failure corpus.
13
13
 
14
14
  ## 5-minute hosted path
15
15
 
16
- Create a Hosted project. The Setup Wizard pre-fills a metadata-only plan and
17
- asks for two decisions: **Confirm plan** and **Authorize install**. Then open a
18
- terminal in the agent repository and run the one command shown by Witnora:
16
+ Create a Hosted project. The Setup Wizard pre-fills a metadata-only plan. Open
17
+ a terminal in the Agent repository and run the one command shown by Witnora:
19
18
 
20
19
  ```bash
21
20
  npx witnora@latest onboard --project your-project-id
22
21
  ```
23
22
 
24
- The browser approval creates one restricted project credential, then the CLI
25
- saves it outside the repository, detects capability groups, and installs only
26
- missing Gateway, outcome-probe, CI, policy, review, configuration, and boundary
27
- files. It verifies each component and reports progress to Hosted. A failed
28
- attempt removes only files created by that attempt. The onboarding receipt is
29
- synthetic: it cannot create a run, evidence object, release decision, or
30
- `CURRENT` assurance.
23
+ One browser **Approve setup** action authorizes the bounded plan and one
24
+ reversible installation. The CLI saves a restricted credential outside the
25
+ repository, detects capability groups, installs only missing Gateway,
26
+ outcome-probe, CI, policy, review, configuration, and boundary files, and
27
+ starts the customer-owned Gateway in the background. It health-checks each
28
+ component and reports progress to Hosted. A failed attempt stops only the
29
+ Gateway process it started and removes only files created by that attempt. The
30
+ onboarding receipt is synthetic: it cannot create a run, evidence object,
31
+ release decision, or `CURRENT` assurance.
31
32
 
32
33
  Onboarding also performs the default private capability discovery locally. It
33
34
  does not read source-file contents or upload source code, prompt text,
@@ -45,11 +46,15 @@ explicit GitHub authorization and is never enabled implicitly.
45
46
 
46
47
  ### Customer-owned Gateway
47
48
 
48
- The default Setup Wizard installs a durable customer-owned Gateway beside the
49
- Agent automatically. Start it, then run the Agent's normal sandbox workflow:
49
+ The default Setup Wizard installs and starts a customer-owned Gateway beside
50
+ the Agent automatically. Run the Agent's normal sandbox workflow. Routine
51
+ Gateway operations are available without keeping another terminal open:
50
52
 
51
53
  ```bash
52
- npx witnora@latest gateway run
54
+ npx witnora@latest gateway status
55
+ npx witnora@latest gateway logs
56
+ npx witnora@latest gateway restart
57
+ npx witnora@latest gateway stop
53
58
  ```
54
59
 
55
60
  Initialization writes a reusable `.witnora/gateway/client.mjs`. Import its
@@ -69,8 +74,8 @@ read-only probe checks the target state.
69
74
 
70
75
  Enterprises that require self-hosted or manual component control can use the
71
76
  individual `gateway init`, `gateway doctor`, policy, probe, CI, and review
72
- commands under Hosted **Advanced**. They are not part of the default customer
73
- journey.
77
+ commands under Hosted **Advanced**. `gateway run` remains available there for
78
+ foreground debugging. They are not part of the default customer journey.
74
79
 
75
80
  After the connection self-test, Hosted shows one template-specific next step.
76
81
  Place the generated Gateway client at one meaningful sandbox boundary and run
package/dist/cli.js CHANGED
@@ -37,8 +37,9 @@ 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
- import { doctorCustomerGateway, initializeCustomerGateway, renderGatewayDoctor, runCustomerGateway } from "./gateway.js";
42
+ import { doctorCustomerGateway, initializeCustomerGateway, readManagedCustomerGatewayLogs, renderGatewayDoctor, renderManagedGatewayStatus, restartManagedCustomerGateway, runCustomerGateway, startManagedCustomerGateway, statusManagedCustomerGateway, stopManagedCustomerGateway, } from "./gateway.js";
42
43
  import { verifyEvidencePacketV02 } from "./evidence-v02.js";
43
44
  process.on("uncaughtException", reportFatalError);
44
45
  process.on("unhandledRejection", reportFatalError);
@@ -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") {
@@ -180,11 +204,35 @@ else if (command === "gateway") {
180
204
  if (result.overall !== "READY_TO_RECORD")
181
205
  process.exitCode = 1;
182
206
  }
207
+ else if (action === "start") {
208
+ const result = await startManagedCustomerGateway({ repository: readFlag("--repo") ?? process.cwd(), dir: readFlag("--dir") });
209
+ process.stdout.write(readBoolFlag("--json") ? `${JSON.stringify(result, null, 2)}\n` : renderManagedGatewayStatus(result));
210
+ }
211
+ else if (action === "status") {
212
+ const result = await statusManagedCustomerGateway({ repository: readFlag("--repo") ?? process.cwd(), dir: readFlag("--dir") });
213
+ process.stdout.write(readBoolFlag("--json") ? `${JSON.stringify(result, null, 2)}\n` : renderManagedGatewayStatus(result));
214
+ if (!result.healthy)
215
+ process.exitCode = 1;
216
+ }
217
+ else if (action === "logs") {
218
+ const lines = Number(readFlag("--lines") ?? 100);
219
+ if (!Number.isSafeInteger(lines) || lines < 1 || lines > 10_000)
220
+ throw new Error("--lines must be an integer between 1 and 10000.");
221
+ process.stdout.write(`${await readManagedCustomerGatewayLogs({ repository: readFlag("--repo") ?? process.cwd(), dir: readFlag("--dir"), lines })}\n`);
222
+ }
223
+ else if (action === "restart") {
224
+ const result = await restartManagedCustomerGateway({ repository: readFlag("--repo") ?? process.cwd(), dir: readFlag("--dir") });
225
+ process.stdout.write(readBoolFlag("--json") ? `${JSON.stringify(result, null, 2)}\n` : renderManagedGatewayStatus(result));
226
+ }
227
+ else if (action === "stop") {
228
+ const result = await stopManagedCustomerGateway({ repository: readFlag("--repo") ?? process.cwd(), dir: readFlag("--dir") });
229
+ process.stdout.write(readBoolFlag("--json") ? `${JSON.stringify(result, null, 2)}\n` : renderManagedGatewayStatus(result));
230
+ }
183
231
  else if (action === "run") {
184
232
  await runCustomerGateway({ repository: readFlag("--repo") ?? process.cwd(), dir: readFlag("--dir") });
185
233
  }
186
234
  else {
187
- throw new Error("Use witnora gateway init|doctor|run.");
235
+ throw new Error("Use witnora gateway init|doctor|start|status|logs|restart|stop|run.");
188
236
  }
189
237
  }
190
238
  else if (command === "discover") {
@@ -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>]
@@ -22,8 +43,9 @@ Options:
22
43
  witnora onboard --project <project-id> --template <browser|coding|mcp|workflow|data>
23
44
 
24
45
  Opens one browser authorization, saves a restricted project credential, detects the repository,
25
- writes missing starter files, and records an isolated synthetic self-test receipt. The self-test
26
- does not create assurance evidence or establish CURRENT status.
46
+ writes missing starter files, starts a customer-owned Gateway in the background, and records an
47
+ isolated synthetic self-test receipt. The self-test does not create assurance evidence or establish
48
+ CURRENT status.
27
49
 
28
50
  Options:
29
51
  --server <url> Hosted server (default: https://witnora.com)
@@ -37,12 +59,18 @@ Options:
37
59
  return `Usage:
38
60
  witnora gateway init --project <project-id>
39
61
  witnora gateway doctor
62
+ witnora gateway start
63
+ witnora gateway status
64
+ witnora gateway logs [--lines 100]
65
+ witnora gateway restart
66
+ witnora gateway stop
40
67
  witnora gateway run
41
68
 
42
- Initializes and runs a customer-owned, metadata-only collector beside the Agent.
69
+ Initializes and manages a customer-owned, metadata-only collector beside the Agent.
43
70
  The browser approval issues a collector-scoped credential; no API key is copied into
44
71
  the repository or exposed to the Agent. The local queue and source signing key remain
45
- under customer control.
72
+ under customer control. Onboard uses background start by default. The run command is
73
+ the foreground debugging path.
46
74
 
47
75
  The reference Gateway establishes RECORDED evidence only. ENFORCED requires target
48
76
  write credentials behind a controlled execution adapter. OUTCOME VERIFIED requires a
@@ -56,7 +84,8 @@ Options:
56
84
  --name <name> Saved collector connection name
57
85
  --no-browser Print the approval URL without opening it
58
86
  --force Replace an existing reviewed local setup
59
- --json JSON doctor output
87
+ --json JSON status output
88
+ --lines <count> Number of recent log lines (default: 100)
60
89
  `;
61
90
  if (command === "design-partner")
62
91
  return `Usage:
@@ -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" },
package/dist/gateway.js CHANGED
@@ -1,10 +1,14 @@
1
1
  import { randomBytes } from "node:crypto";
2
+ import { spawn } from "node:child_process";
3
+ import { closeSync, openSync } from "node:fs";
2
4
  import { access, chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
3
5
  import { basename, dirname, join, resolve } from "node:path";
6
+ import { fileURLToPath } from "node:url";
4
7
  import { loadConnection } from "./credentials.js";
5
8
  import { authorizeProjectConnection } from "./device-authorization.js";
6
9
  const CONFIG_SCHEMA = "witnora.customer_gateway_setup.v0.1";
7
10
  const SECRETS_SCHEMA = "witnora.customer_gateway_local_secrets.v0.1";
11
+ const RUNTIME_SCHEMA = "witnora.managed_gateway_runtime.v0.1";
8
12
  export async function initializeCustomerGateway(options) {
9
13
  const repository = resolve(options.repository ?? process.cwd());
10
14
  const outDir = resolve(repository, options.outDir ?? ".witnora/gateway");
@@ -59,7 +63,7 @@ export async function initializeCustomerGateway(options) {
59
63
  for (const [path, content, mode] of [
60
64
  [configPath, `${JSON.stringify(config, null, 2)}\n`, 0o644],
61
65
  [secretsPath, `${JSON.stringify(secrets, null, 2)}\n`, 0o600],
62
- [gitignorePath, "secrets.json\ndata/\n", 0o644],
66
+ [gitignorePath, "secrets.json\ndata/\nruntime/\n", 0o644],
63
67
  [clientPath, gatewayClient(config), 0o644],
64
68
  [readmePath, gatewayReadme(config), 0o644],
65
69
  ]) {
@@ -78,10 +82,9 @@ export async function initializeCustomerGateway(options) {
78
82
  output("\nCustomer-owned Gateway initialized.\n");
79
83
  output(`Configuration: ${configPath}\nLocal secret: ${secretsPath} (never commit or upload)\n`);
80
84
  output("Next:\n");
81
- output(" 1. Run: npx witnora@latest gateway doctor\n");
82
- output(" 2. Run: npx witnora@latest gateway run\n");
83
- output(" 3. Import .witnora/gateway/client.mjs at one sandbox workflow boundary.\n");
84
- output(" 4. Run that workflow normally; start, event, and complete are recorded locally.\n");
85
+ output(" 1. Run: npx witnora@latest gateway start\n");
86
+ output(" 2. Import .witnora/gateway/client.mjs at one sandbox workflow boundary.\n");
87
+ output(" 3. Run that workflow normally; start, event, and complete are recorded locally.\n");
85
88
  output("This reference path establishes RECORDED evidence only. ENFORCED requires target write credentials behind a controlled execution adapter; OUTCOME VERIFIED requires a separate read-only probe.\n");
86
89
  return { configPath, secretsPath, config, generatedFiles };
87
90
  }
@@ -136,7 +139,7 @@ export async function doctorCustomerGateway(options) {
136
139
  checks.push({ id: "process", status: "PASS", message: `Gateway is listening at http://${config.host}:${config.port}.` });
137
140
  }
138
141
  catch {
139
- checks.push({ id: "process", status: "WARN", message: "Gateway is not running yet. Start it with `witnora gateway run`." });
142
+ checks.push({ id: "process", status: "WARN", message: "Gateway is not running yet. Start managed mode with `witnora gateway start`." });
140
143
  }
141
144
  }
142
145
  if (config && secrets) {
@@ -149,7 +152,7 @@ export async function doctorCustomerGateway(options) {
149
152
  overall: failed ? "SETUP_INCOMPLETE" : "READY_TO_RECORD",
150
153
  checks,
151
154
  evidenceCeiling: "recorded",
152
- nextAction: failed ? "Run `witnora gateway init --project <project-id>` again after resolving failed checks." : "Start the Gateway, then send one sandbox run through its local event API.",
155
+ nextAction: failed ? "Run `witnora gateway init --project <project-id>` again after resolving failed checks." : "Ensure the managed Gateway is healthy, then send one sandbox run through its local event API.",
153
156
  };
154
157
  }
155
158
  export async function runCustomerGateway(options) {
@@ -181,6 +184,187 @@ export async function runCustomerGateway(options) {
181
184
  process.once(signal, () => void gateway.close().finally(() => process.exit(0)));
182
185
  }
183
186
  }
187
+ export async function startManagedCustomerGateway(options = {}) {
188
+ const repository = resolve(options.repository ?? process.cwd());
189
+ const directory = resolve(repository, options.dir ?? ".witnora/gateway");
190
+ const config = parseConfig(await readFile(join(directory, "gateway.json"), "utf8"));
191
+ const current = await statusManagedCustomerGateway(options);
192
+ if (current.state === "RUNNING_MANAGED" || current.state === "RUNNING_EXTERNAL") {
193
+ return { ...current, started: false };
194
+ }
195
+ if (current.state === "CONFLICT")
196
+ throw new Error(current.detail);
197
+ if (current.state === "STALE")
198
+ await rm(runtimePath(directory), { force: true });
199
+ const cliEntry = resolve(options.cliEntry ?? fileURLToPath(new URL("./cli.js", import.meta.url)));
200
+ if (!await exists(cliEntry))
201
+ throw new Error(`Managed Gateway CLI entry was not found at ${cliEntry}. Reinstall Witnora and run onboard again.`);
202
+ const runtimeDirectory = join(directory, "runtime");
203
+ const logPath = join(runtimeDirectory, "gateway.log");
204
+ await mkdir(runtimeDirectory, { recursive: true });
205
+ const log = openSync(logPath, "a");
206
+ let launchError;
207
+ let child;
208
+ try {
209
+ const args = [cliEntry, "gateway", "run", "--repo", repository];
210
+ if (options.dir)
211
+ args.push("--dir", options.dir);
212
+ child = spawn(process.execPath, args, {
213
+ cwd: repository,
214
+ detached: true,
215
+ windowsHide: true,
216
+ stdio: ["ignore", log, log],
217
+ env: {
218
+ ...process.env,
219
+ ...(options.configHome ? { WITNORA_CONFIG_HOME: options.configHome } : {}),
220
+ WITNORA_GATEWAY_MANAGED: "1",
221
+ },
222
+ });
223
+ child.once("error", (error) => { launchError = error; });
224
+ child.unref();
225
+ }
226
+ finally {
227
+ closeSync(log);
228
+ }
229
+ if (!child.pid)
230
+ throw new Error("Witnora could not obtain a process ID for the managed Gateway.");
231
+ const runtime = {
232
+ schemaVersion: RUNTIME_SCHEMA,
233
+ pid: child.pid,
234
+ projectId: config.projectId,
235
+ server: config.server,
236
+ collectorId: config.collectorId,
237
+ host: config.host,
238
+ port: config.port,
239
+ startedAt: new Date().toISOString(),
240
+ cliEntry,
241
+ logPath,
242
+ };
243
+ await writeFile(runtimePath(directory), `${JSON.stringify(runtime, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
244
+ await chmod(runtimePath(directory), 0o600).catch(() => undefined);
245
+ const sleep = options.sleep ?? wait;
246
+ const deadline = Date.now() + (options.timeoutMs ?? 12_000);
247
+ while (Date.now() < deadline) {
248
+ if (launchError)
249
+ break;
250
+ const status = await statusManagedCustomerGateway(options);
251
+ if (status.state === "RUNNING_MANAGED") {
252
+ options.output?.(`Managed customer-owned Gateway started in the background at ${status.baseUrl}.\nLog: ${logPath}\n`);
253
+ return { ...status, started: true };
254
+ }
255
+ if (status.state === "CONFLICT") {
256
+ await stopManagedCustomerGateway({ ...options, timeoutMs: 2_000 }).catch(() => undefined);
257
+ throw new Error(status.detail);
258
+ }
259
+ await sleep(120);
260
+ }
261
+ await stopManagedCustomerGateway({ ...options, timeoutMs: 2_000 }).catch(() => undefined);
262
+ const tail = await readManagedCustomerGatewayLogs({ repository, dir: options.dir, lines: 20 }).catch(() => "");
263
+ throw new Error(`Managed Gateway did not become healthy${launchError ? `: ${launchError.message}` : " before the startup deadline"}.${tail ? `\nRecent Gateway log:\n${tail}` : ""}`);
264
+ }
265
+ export async function statusManagedCustomerGateway(options = {}) {
266
+ const repository = resolve(options.repository ?? process.cwd());
267
+ const directory = resolve(repository, options.dir ?? ".witnora/gateway");
268
+ const config = parseConfig(await readFile(join(directory, "gateway.json"), "utf8"));
269
+ const baseUrl = `http://${config.host}:${config.port}`;
270
+ const runtime = await readRuntime(directory);
271
+ const health = await gatewayHealth(baseUrl, options.fetch ?? fetch);
272
+ const base = {
273
+ schemaVersion: "witnora.managed_gateway_status.v0.1",
274
+ baseUrl,
275
+ projectId: config.projectId,
276
+ collectorId: config.collectorId,
277
+ pid: runtime?.pid,
278
+ logPath: runtime?.logPath,
279
+ };
280
+ if (health && health.collectorId !== config.collectorId) {
281
+ return { ...base, state: "CONFLICT", healthy: false, managed: false, detail: `Port ${config.port} is occupied by collector ${health.collectorId}, not ${config.collectorId}.` };
282
+ }
283
+ if (runtime && !runtimeMatches(runtime, config)) {
284
+ return { ...base, state: "CONFLICT", healthy: false, managed: false, detail: "Managed Gateway runtime metadata belongs to a different project or collector." };
285
+ }
286
+ if (health) {
287
+ const managed = Boolean(runtime && pidRunning(runtime.pid));
288
+ return {
289
+ ...base,
290
+ state: managed ? "RUNNING_MANAGED" : "RUNNING_EXTERNAL",
291
+ healthy: true,
292
+ managed,
293
+ detail: managed ? "The customer-owned Gateway is healthy and managed by Witnora." : "A compatible Gateway is healthy but was started outside Witnora process management.",
294
+ };
295
+ }
296
+ if (runtime) {
297
+ const running = pidRunning(runtime.pid);
298
+ return {
299
+ ...base,
300
+ state: running ? "STARTING" : "STALE",
301
+ healthy: false,
302
+ managed: running,
303
+ detail: running ? "The managed Gateway process is starting but has not passed its health check." : "Managed Gateway runtime metadata is stale; the recorded process is no longer running.",
304
+ };
305
+ }
306
+ return { ...base, state: "STOPPED", healthy: false, managed: false, detail: "The customer-owned Gateway is not running." };
307
+ }
308
+ export async function stopManagedCustomerGateway(options = {}) {
309
+ const repository = resolve(options.repository ?? process.cwd());
310
+ const directory = resolve(repository, options.dir ?? ".witnora/gateway");
311
+ const runtime = await readRuntime(directory);
312
+ const before = await statusManagedCustomerGateway(options);
313
+ if (!runtime)
314
+ return { ...before, stopped: false };
315
+ if (before.state === "CONFLICT")
316
+ throw new Error(before.detail);
317
+ if (pidRunning(runtime.pid)) {
318
+ try {
319
+ process.kill(runtime.pid, "SIGTERM");
320
+ }
321
+ catch (error) {
322
+ if (!isMissingProcess(error))
323
+ throw error;
324
+ }
325
+ const sleep = options.sleep ?? wait;
326
+ const deadline = Date.now() + (options.timeoutMs ?? 5_000);
327
+ while (Date.now() < deadline && pidRunning(runtime.pid))
328
+ await sleep(100);
329
+ if (pidRunning(runtime.pid)) {
330
+ try {
331
+ process.kill(runtime.pid, "SIGKILL");
332
+ }
333
+ catch (error) {
334
+ if (!isMissingProcess(error))
335
+ throw error;
336
+ }
337
+ }
338
+ }
339
+ await rm(runtimePath(directory), { force: true });
340
+ const after = await statusManagedCustomerGateway(options);
341
+ return { ...after, stopped: !after.healthy };
342
+ }
343
+ export async function restartManagedCustomerGateway(options = {}) {
344
+ const status = await statusManagedCustomerGateway(options);
345
+ if (status.state === "RUNNING_EXTERNAL")
346
+ throw new Error("The Gateway is running outside Witnora process management. Stop that foreground process before requesting a managed restart.");
347
+ await stopManagedCustomerGateway(options);
348
+ return startManagedCustomerGateway(options);
349
+ }
350
+ export async function readManagedCustomerGatewayLogs(options = {}) {
351
+ const repository = resolve(options.repository ?? process.cwd());
352
+ const directory = resolve(repository, options.dir ?? ".witnora/gateway");
353
+ const path = join(directory, "runtime", "gateway.log");
354
+ const content = await readFile(path, "utf8");
355
+ return content.split(/\r?\n/).slice(-Math.max(1, options.lines ?? 100)).join("\n").trim();
356
+ }
357
+ export function renderManagedGatewayStatus(result) {
358
+ return [
359
+ `Customer-owned Gateway: ${result.state}`,
360
+ `Health: ${result.healthy ? "HEALTHY" : "UNAVAILABLE"}`,
361
+ `Endpoint: ${result.baseUrl}`,
362
+ ...(result.pid ? [`Process: ${result.pid}${result.managed ? " (managed)" : ""}`] : []),
363
+ ...(result.logPath ? [`Log: ${result.logPath}`] : []),
364
+ result.detail,
365
+ "",
366
+ ].join("\n");
367
+ }
184
368
  export function renderGatewayDoctor(result) {
185
369
  return [
186
370
  `Customer-owned Gateway: ${result.overall}`,
@@ -218,7 +402,7 @@ function gatewayPort(value, fallback) {
218
402
  return parsed;
219
403
  }
220
404
  function gatewayReadme(config) {
221
- return `# Witnora customer-owned Gateway\n\nThis directory configures a metadata-only Gateway for project \`${config.projectId}\`.\n\n## Start the Gateway\n\n\`\`\`bash\nnpx witnora@latest gateway doctor\nnpx witnora@latest gateway run\n\`\`\`\n\nKeep \`gateway run\` open. In the Agent repository, import the generated client at one meaningful sandbox workflow boundary:\n\n\`\`\`js\nimport { randomUUID } from "node:crypto";\nimport { witnoraGateway } from "./.witnora/gateway/client.mjs";\n\nconst runId = randomUUID();\nawait witnoraGateway.start(runId, { workflow: "sandbox-workflow" });\ntry {\n // Run the existing customer workflow here. Do not add raw inputs or outputs.\n await witnoraGateway.event(runId, "workflow.step.completed", { step: "meaningful-boundary" });\n await witnoraGateway.complete(runId, { status: "completed" });\n} catch (error) {\n await witnoraGateway.event(runId, "workflow.failed", { errorType: error?.name ?? "Error" });\n await witnoraGateway.complete(runId, { status: "failed" });\n throw error;\n}\n\`\`\`\n\nThe generated client reads only the local ignored Gateway token and sends metadata to \`http://${config.host}:${config.port}\`. The Hosted API key and source-signing key stay in the Gateway process. Do not commit \`secrets.json\` or \`data/\`.\n\nThis reference process creates source-signed, durable **RECORDED** evidence. It does not claim complete mediation. **ENFORCED** requires the target write credential to be removed from the Agent and placed behind a controlled execution adapter. **OUTCOME VERIFIED** requires a separate read-only credential and independent probe.\n`;
405
+ return `# Witnora customer-owned Gateway\n\nThis directory configures a metadata-only Gateway for project \`${config.projectId}\`. The Setup Autopilot starts it in the background after browser authorization.\n\n## Operations\n\n\`\`\`bash\nnpx witnora@latest gateway status\nnpx witnora@latest gateway logs\nnpx witnora@latest gateway restart\nnpx witnora@latest gateway stop\n\`\`\`\n\n\`gateway run\` remains available as a foreground debugging command. In the Agent repository, import the generated client at one meaningful sandbox workflow boundary:\n\n\`\`\`js\nimport { randomUUID } from "node:crypto";\nimport { witnoraGateway } from "./.witnora/gateway/client.mjs";\n\nconst runId = randomUUID();\nawait witnoraGateway.start(runId, { workflow: "sandbox-workflow" });\ntry {\n // Run the existing customer workflow here. Do not add raw inputs or outputs.\n await witnoraGateway.event(runId, "workflow.step.completed", { step: "meaningful-boundary" });\n await witnoraGateway.complete(runId, { status: "completed" });\n} catch (error) {\n await witnoraGateway.event(runId, "workflow.failed", { errorType: error?.name ?? "Error" });\n await witnoraGateway.complete(runId, { status: "failed" });\n throw error;\n}\n\`\`\`\n\nThe generated client reads only the local ignored Gateway token and sends metadata to \`http://${config.host}:${config.port}\`. The Hosted API key and source-signing key stay in the Gateway process. Do not commit \`secrets.json\`, \`data/\`, or \`runtime/\`.\n\nThis reference process creates source-signed, durable **RECORDED** evidence. It does not claim complete mediation. **ENFORCED** requires the target write credential to be removed from the Agent and placed behind a controlled execution adapter. **OUTCOME VERIFIED** requires a separate read-only credential and independent probe.\n`;
222
406
  }
223
407
  function gatewayClient(config) {
224
408
  return `import { readFile } from "node:fs/promises";\n\nconst baseUrl = "http://${config.host}:${config.port}";\nlet gatewayToken;\n\nasync function token() {\n if (gatewayToken) return gatewayToken;\n const secrets = JSON.parse(await readFile(new URL("./secrets.json", import.meta.url), "utf8"));\n if (typeof secrets.gatewayToken !== "string" || secrets.gatewayToken.length < 32) {\n throw new Error("Witnora local Gateway token is missing or invalid.");\n }\n gatewayToken = secrets.gatewayToken;\n return gatewayToken;\n}\n\nasync function post(runId, operation, body) {\n if (!/^[A-Za-z0-9._:-]+$/.test(runId)) throw new Error("Witnora runId contains unsupported characters.");\n const response = await fetch(\`\${baseUrl}/v1/runs/\${encodeURIComponent(runId)}/\${operation}\`, {\n method: "POST",\n headers: { authorization: \`Bearer \${await token()}\`, "content-type": "application/json" },\n body: JSON.stringify(body),\n });\n const result = await response.json().catch(() => ({}));\n if (!response.ok) throw new Error(result.error ?? \`Witnora Gateway returned HTTP \${response.status}.\`);\n return result;\n}\n\nexport const witnoraGateway = {\n start(runId, metadata = {}) {\n return post(runId, "start", { payload: metadata, idempotencyKey: "run-start" });\n },\n event(runId, type, metadata = {}, idempotencyKey = \`\${type}-\${crypto.randomUUID()}\`) {\n return post(runId, "events", { type, payload: metadata, idempotencyKey });\n },\n complete(runId, metadata = {}) {\n return post(runId, "complete", {\n payload: metadata,\n evidenceStrength: {\n schemaVersion: "agentcert.evidence_strength.v0.1",\n level: "recorded",\n claims: [],\n limitations: ["No write-credential mediation or independent outcome probe is configured."],\n },\n idempotencyKey: "run-complete",\n });\n },\n};\n`;
@@ -248,6 +432,51 @@ function gatewayPaths(directory) {
248
432
  join(directory, "README.md"),
249
433
  ];
250
434
  }
435
+ async function gatewayHealth(baseUrl, requestFetch) {
436
+ try {
437
+ const response = await requestFetch(`${baseUrl}/healthz`, { signal: AbortSignal.timeout(800) });
438
+ if (!response.ok)
439
+ return undefined;
440
+ const value = await response.json();
441
+ return typeof value.collectorId === "string" && value.collectorId ? { collectorId: value.collectorId } : undefined;
442
+ }
443
+ catch {
444
+ return undefined;
445
+ }
446
+ }
447
+ async function readRuntime(directory) {
448
+ try {
449
+ const value = JSON.parse(await readFile(runtimePath(directory), "utf8"));
450
+ if (value.schemaVersion !== RUNTIME_SCHEMA || !Number.isSafeInteger(value.pid) || Number(value.pid) < 1 || !value.projectId || !value.collectorId || !value.host || !Number.isSafeInteger(value.port) || !value.cliEntry || !value.logPath)
451
+ return undefined;
452
+ return value;
453
+ }
454
+ catch {
455
+ return undefined;
456
+ }
457
+ }
458
+ function runtimeMatches(runtime, config) {
459
+ return runtime.projectId === config.projectId && runtime.server === config.server && runtime.collectorId === config.collectorId
460
+ && runtime.host === config.host && runtime.port === config.port;
461
+ }
462
+ function runtimePath(directory) {
463
+ return join(directory, "runtime", "gateway-runtime.json");
464
+ }
465
+ function pidRunning(pid) {
466
+ try {
467
+ process.kill(pid, 0);
468
+ return true;
469
+ }
470
+ catch (error) {
471
+ return !isMissingProcess(error);
472
+ }
473
+ }
474
+ function isMissingProcess(error) {
475
+ return Boolean(error && typeof error === "object" && "code" in error && error.code === "ESRCH");
476
+ }
477
+ function wait(milliseconds) {
478
+ return new Promise((resolveWait) => setTimeout(resolveWait, milliseconds));
479
+ }
251
480
  function safeSlug(value) {
252
481
  return (value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "agent-repository").slice(0, 48);
253
482
  }
package/dist/onboard.js CHANGED
@@ -7,7 +7,7 @@ import { verifyControlPlaneConnection } from "./control-plane.js";
7
7
  import { inferPrivateCapabilities, runPrivateCapabilityDiscovery } from "./private-discovery.js";
8
8
  import { parseAgentTemplate, starterAdapter, starterProfile, starterTripwireConfig } from "./onboarding-templates.js";
9
9
  import { writeTryEvidence } from "./try.js";
10
- import { doctorCustomerGateway, initializeCustomerGateway, inspectCustomerGatewayFiles } from "./gateway.js";
10
+ import { doctorCustomerGateway, initializeCustomerGateway, inspectCustomerGatewayFiles, startManagedCustomerGateway, stopManagedCustomerGateway, } from "./gateway.js";
11
11
  export async function runOnboard(options) {
12
12
  const requestFetch = options.fetch ?? fetch;
13
13
  const output = options.output ?? ((message) => process.stdout.write(message));
@@ -38,7 +38,12 @@ export async function runOnboard(options) {
38
38
  }
39
39
  const attemptId = `install-${Date.now()}-${randomSuffix()}`;
40
40
  const generatedFiles = [];
41
+ const gatewayLifecycle = options.gatewayLifecycle ?? {
42
+ start: startManagedCustomerGateway,
43
+ stop: stopManagedCustomerGateway,
44
+ };
41
45
  let gatewayMigration;
46
+ let managedGateway;
42
47
  await reportInstall(requestFetch, server, token.projectId, token.apiKey, setupPlan.id, { status: "installing", attemptId });
43
48
  try {
44
49
  generatedFiles.push(...await generateRepositoryConfig(repositoryPath, repository.template, repository.name));
@@ -103,6 +108,14 @@ export async function runOnboard(options) {
103
108
  }
104
109
  }
105
110
  generatedFiles.push(...await generateAutopilotFiles(repositoryPath, repository.name));
111
+ managedGateway = await gatewayLifecycle.start({
112
+ repository: repositoryPath,
113
+ configHome: options.configHome,
114
+ fetch: requestFetch,
115
+ output,
116
+ });
117
+ if (!managedGateway.healthy)
118
+ throw new Error(managedGateway.detail);
106
119
  const doctor = await doctorCustomerGateway({ repository: repositoryPath, configHome: options.configHome, fetch: requestFetch });
107
120
  if (doctor.overall !== "READY_TO_RECORD")
108
121
  throw new Error(doctor.checks.filter((check) => check.status === "FAIL").map((check) => check.message).join(" "));
@@ -114,13 +127,18 @@ export async function runOnboard(options) {
114
127
  output(`Credentials: ${credentialsPath}\nSelf-test receipt: ${receiptPath}\n`);
115
128
  output(`Private discovery: ${discovery.capabilityCount} capability group(s); ${discovery.unknownCapabilityCount} pending confirmation.\n`);
116
129
  output("Installed: customer-owned Gateway, default-deny policy, independent-probe contract, review contract, and PR/release/nightly CI.\n");
117
- output("The self-test remains isolated. Start the generated Gateway and run the agent normally; the first source-signed, server-reconciled Gateway run completes onboarding.\n");
130
+ output(`Gateway: ${managedGateway.state} at ${managedGateway.baseUrl}${managedGateway.pid ? ` (process ${managedGateway.pid})` : ""}.\n`);
131
+ output("The self-test remains isolated. The Gateway is ready in the background; run the agent normally. The first source-signed, server-reconciled Gateway run completes onboarding.\n");
118
132
  return { projectId: token.projectId, connectionName: token.connectionName, credentialsPath, template: repository.template,
119
133
  repositoryKind: repository.kind, generatedFiles, receiptPath, discovery, setupPlanId: setupPlan.id,
120
- gatewayDirectory: join(repositoryPath, ".witnora", "gateway"), gatewayArchiveDirectory: gatewayMigration?.archiveDirectory };
134
+ gatewayDirectory: join(repositoryPath, ".witnora", "gateway"), gatewayArchiveDirectory: gatewayMigration?.archiveDirectory,
135
+ gateway: managedGateway };
121
136
  }
122
137
  catch (error) {
123
138
  const diagnosis = error instanceof Error ? error.message : String(error);
139
+ if (managedGateway?.started) {
140
+ await gatewayLifecycle.stop({ repository: repositoryPath, configHome: options.configHome, fetch: requestFetch }).catch(() => undefined);
141
+ }
124
142
  await rollbackGeneratedFiles(generatedFiles);
125
143
  if (gatewayMigration)
126
144
  await restoreGateway(gatewayMigration);
@@ -0,0 +1,309 @@
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 child = spawn(process.platform === "win32" ? "npm.cmd" : "npm", ["run", input.script], {
134
+ cwd: input.repository,
135
+ detached: process.platform !== "win32",
136
+ shell: false,
137
+ windowsHide: true,
138
+ stdio: ["ignore", "pipe", "pipe"],
139
+ });
140
+ let stdout = "";
141
+ let stderr = "";
142
+ let timedOut = false;
143
+ let settled = false;
144
+ let forceTimer;
145
+ const finish = (result) => {
146
+ if (settled)
147
+ return;
148
+ settled = true;
149
+ clearTimeout(timer);
150
+ if (forceTimer)
151
+ clearTimeout(forceTimer);
152
+ resolveRun(result);
153
+ };
154
+ const timer = setTimeout(() => {
155
+ timedOut = true;
156
+ terminateProcessTree(child.pid);
157
+ forceTimer = setTimeout(() => {
158
+ terminateProcessTree(child.pid, true);
159
+ finish({ exitCode: 1, stdout, stderr, timedOut: true });
160
+ }, 5_000);
161
+ }, input.timeoutMs);
162
+ child.stdout.on("data", (chunk) => { stdout = appendBounded(stdout, chunk); });
163
+ child.stderr.on("data", (chunk) => { stderr = appendBounded(stderr, chunk); });
164
+ child.once("error", (error) => {
165
+ if (settled)
166
+ return;
167
+ settled = true;
168
+ clearTimeout(timer);
169
+ if (forceTimer)
170
+ clearTimeout(forceTimer);
171
+ rejectRun(error);
172
+ });
173
+ child.once("close", (code) => {
174
+ finish({ exitCode: code ?? 1, stdout, stderr, timedOut });
175
+ });
176
+ });
177
+ }
178
+ function terminateProcessTree(pid, force = false) {
179
+ if (!pid)
180
+ return;
181
+ if (process.platform === "win32") {
182
+ const taskkill = spawn("taskkill", ["/pid", String(pid), "/t", "/f"], {
183
+ shell: false,
184
+ windowsHide: true,
185
+ stdio: "ignore",
186
+ });
187
+ taskkill.on("error", () => undefined);
188
+ return;
189
+ }
190
+ try {
191
+ process.kill(-pid, force ? "SIGKILL" : "SIGTERM");
192
+ }
193
+ catch {
194
+ // The process may have exited between the timeout and termination attempt.
195
+ }
196
+ }
197
+ function appendBounded(current, chunk) {
198
+ if (Buffer.byteLength(current) >= MAX_OUTPUT_BYTES)
199
+ return current;
200
+ const remaining = MAX_OUTPUT_BYTES - Buffer.byteLength(current);
201
+ return current + chunk.subarray(0, remaining).toString("utf8");
202
+ }
203
+ function normalizeTimeout(value) {
204
+ const timeout = value ?? DEFAULT_TIMEOUT_MS;
205
+ if (!Number.isSafeInteger(timeout) || timeout < 1_000 || timeout > 30 * 60 * 1000) {
206
+ throw new Error("Release evaluation timeout must be between 1 second and 30 minutes.");
207
+ }
208
+ return timeout;
209
+ }
210
+ function renderEvaluationLog(script, result) {
211
+ return [
212
+ `command=npm run ${script}`,
213
+ `exitCode=${result.exitCode}`,
214
+ `timedOut=${result.timedOut}`,
215
+ "",
216
+ "[stdout]",
217
+ result.stdout,
218
+ "",
219
+ "[stderr]",
220
+ result.stderr,
221
+ "",
222
+ ].join("\n");
223
+ }
224
+ function renderEvaluationArtifact(script, result) {
225
+ const stdout = redactEvaluationOutput(result.stdout);
226
+ const stderr = redactEvaluationOutput(result.stderr);
227
+ return {
228
+ schemaVersion: "witnora.release_evaluation_output.v0.1",
229
+ command: { executable: "npm", arguments: ["run", script] },
230
+ exitCode: result.exitCode,
231
+ timedOut: result.timedOut,
232
+ stdout: outputDescriptor(result.stdout, stdout.value),
233
+ stderr: outputDescriptor(result.stderr, stderr.value),
234
+ redaction: {
235
+ policyVersion: "witnora.release_output_redaction.v0.1",
236
+ replacements: stdout.replacements + stderr.replacements,
237
+ rawOutputRetainedLocally: true,
238
+ },
239
+ };
240
+ }
241
+ function outputDescriptor(raw, redacted) {
242
+ return {
243
+ sha256: createHash("sha256").update(raw).digest("hex"),
244
+ byteLength: Buffer.byteLength(raw),
245
+ excerpt: redacted,
246
+ };
247
+ }
248
+ function redactEvaluationOutput(value) {
249
+ const patterns = [
250
+ /\bsk-(?:proj-|live_|test_)?[A-Za-z0-9_-]{16,}\b/g,
251
+ /\brk_(?:live|test)_[A-Za-z0-9]{12,}\b/g,
252
+ /\bnpm_[A-Za-z0-9]{16,}\b/g,
253
+ /\bac_(?:live|test)_[A-Za-z0-9_-]{12,}\b/g,
254
+ /\bBearer\s+[A-Za-z0-9._~+/=-]{12,}\b/gi,
255
+ ];
256
+ let redacted = value;
257
+ let replacements = 0;
258
+ for (const pattern of patterns) {
259
+ redacted = redacted.replace(pattern, () => {
260
+ replacements += 1;
261
+ return "[REDACTED_SECRET]";
262
+ });
263
+ }
264
+ return { value: redacted, replacements };
265
+ }
266
+ function releaseEvaluationResult(input) {
267
+ const passed = input.execution.exitCode === 0 && !input.execution.timedOut;
268
+ return {
269
+ schemaVersion: "1",
270
+ product: "agentcert-cli",
271
+ runId: input.runId,
272
+ timestamp: input.timestamp,
273
+ phase: "pre-release",
274
+ score: passed ? 100 : 0,
275
+ passed,
276
+ summary: passed
277
+ ? `The repository's existing ${input.script} evaluation completed successfully.`
278
+ : `The repository's existing ${input.script} evaluation failed${input.execution.timedOut ? " after timing out" : ""}.`,
279
+ evidenceStrength: {
280
+ schemaVersion: "agentcert.evidence_strength.v0.1",
281
+ level: "reported",
282
+ claims: ["A customer-owned repository evaluation produced a captured process result."],
283
+ limitations: ["This local command result is not independent outcome verification or an independent Witnora review."],
284
+ trustVector: {
285
+ schemaVersion: "witnora.evidence_trust_vector.v0.1",
286
+ capture: "self_reported",
287
+ mediation: "none",
288
+ outcome: "unverified",
289
+ completeness: "partial",
290
+ attestation: "unsigned",
291
+ review: "none",
292
+ limitations: ["The repository controls the evaluated script and its reported outcome."],
293
+ },
294
+ },
295
+ artifacts: { evaluationOutput: input.artifactPath },
296
+ evidence: [{
297
+ id: `release-evaluation-${input.runId}`,
298
+ kind: "release_evaluation",
299
+ severity: passed ? "info" : "high",
300
+ message: passed
301
+ ? `npm run ${input.script} completed with exit code 0.`
302
+ : `npm run ${input.script} completed with exit code ${input.execution.exitCode}${input.execution.timedOut ? " after timing out" : ""}.`,
303
+ source: "witnora-cli",
304
+ artifactPath: input.artifactPath,
305
+ suggestedFix: passed ? undefined : "Inspect the complete local evaluation log and the uploaded redacted summary, correct the failure, and rerun this release evaluation.",
306
+ metadata: { assuranceCaseId: input.assuranceCase.id, script: input.script, timedOut: input.execution.timedOut },
307
+ }],
308
+ };
309
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "witnora",
3
- "version": "0.12.1",
3
+ "version": "0.13.1",
4
4
  "description": "Independent assurance for covered agent action paths across models and frameworks.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",