witnora 0.11.0 → 0.12.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 CHANGED
@@ -13,18 +13,21 @@ 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, open a terminal in the agent repository, and run the
17
- one command shown by Witnora:
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:
18
19
 
19
20
  ```bash
20
21
  npx witnora@latest onboard --project your-project-id
21
22
  ```
22
23
 
23
- The browser approval creates a restricted project credential without exposing
24
- it in Hosted, then the CLI saves it outside the repository, detects the agent
25
- boundary, writes only missing configuration, and verifies the local evidence
26
- path. `.witnora/onboarding/receipt.json` is synthetic: it cannot create a run,
27
- evidence object, release decision, or `CURRENT` assurance.
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.
28
31
 
29
32
  Onboarding also performs the default private capability discovery locally. It
30
33
  does not read source-file contents or upload source code, prompt text,
@@ -42,12 +45,10 @@ explicit GitHub authorization and is never enabled implicitly.
42
45
 
43
46
  ### Customer-owned Gateway
44
47
 
45
- For a durable recorder that runs beside the Agent in the customer environment,
46
- choose **Customer-owned Gateway** in Hosted and run:
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:
47
50
 
48
51
  ```bash
49
- npx witnora@latest gateway init --project your-project-id
50
- npx witnora@latest gateway doctor
51
52
  npx witnora@latest gateway run
52
53
  ```
53
54
 
@@ -56,20 +57,28 @@ Initialization writes a reusable `.witnora/gateway/client.mjs`. Import its
56
57
  workflow boundary. The generated README contains the exact code and privacy
57
58
  boundary; no Hosted key or source-signing key is written into the repository.
58
59
 
59
- The browser approval issues only `runs:read`, `events:write`, and
60
- `collector:manage`. The Hosted key is stored in the user's Witnora credential
61
- directory; the repository contains only metadata-only config plus ignored local
62
- secrets and queue data. This reference path establishes **RECORDED** evidence.
60
+ The browser approval issues one bounded credential for setup, evidence, the
61
+ customer-owned recorder, and covered runtime actions. It has no team,
62
+ retention, administrative, target-system, or customer-data permissions. The
63
+ Hosted key is stored in the user's Witnora credential directory; the repository
64
+ contains only metadata-only config plus ignored local secrets and queue data.
65
+ This reference path establishes **RECORDED** evidence.
63
66
  It does not claim **ENFORCED** until target write credentials are placed behind a
64
67
  controlled execution adapter, or **OUTCOME VERIFIED** until an independent
65
68
  read-only probe checks the target state.
66
69
 
70
+ Enterprises that require self-hosted or manual component control can use the
71
+ 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.
74
+
67
75
  After the connection self-test, Hosted shows one template-specific next step.
68
- Place the generated boundary in one meaningful sandbox workflow and run it
69
- normally. The generated local adapter reuses the saved restricted connection;
70
- CI and production should use the same project-scoped values from a secret
71
- manager. The first non-synthetic run completes onboarding, but remains
72
- `reported` evidence and does not establish a release decision or `CURRENT`.
76
+ Place the generated Gateway client at one meaningful sandbox boundary and run
77
+ it normally. The Gateway reuses the saved restricted connection; CI and
78
+ production should use the same project-scoped values from a secret manager.
79
+ Only a source-signed Gateway run whose journal is reconciled by Hosted completes
80
+ onboarding. A direct run is still visible, but cannot verify the Gateway or
81
+ establish a release decision or `CURRENT` assurance.
73
82
 
74
83
  Without an agent repository, run `npx witnora@latest try --template workflow`
75
84
  for an offline-only sample.
package/dist/gateway.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { randomBytes } from "node:crypto";
2
- import { access, chmod, mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { access, chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
3
3
  import { basename, dirname, join, resolve } from "node:path";
4
4
  import { loadConnection } from "./credentials.js";
5
5
  import { authorizeProjectConnection } from "./device-authorization.js";
@@ -11,30 +11,27 @@ export async function initializeCustomerGateway(options) {
11
11
  const output = options.output ?? ((message) => process.stdout.write(message));
12
12
  const slug = safeSlug(basename(repository));
13
13
  const connectionName = options.connectionName ?? `${slug}-gateway`;
14
- const configPath = join(outDir, "gateway.json");
15
- const secretsPath = join(outDir, "secrets.json");
16
- const gitignorePath = join(outDir, ".gitignore");
17
- const readmePath = join(outDir, "README.md");
18
- const clientPath = join(outDir, "client.mjs");
14
+ const [configPath, secretsPath, gitignorePath, clientPath, readmePath] = gatewayPaths(outDir);
19
15
  if (!(options.force ?? false)) {
20
- const existing = (await Promise.all([configPath, secretsPath, gitignorePath, readmePath, clientPath].map(async (path) => await exists(path) ? path : undefined)))
21
- .filter((path) => path !== undefined);
16
+ const { existing } = await inspectCustomerGatewayFiles({ repository, outDir });
22
17
  if (existing.length > 0) {
23
18
  throw new Error(`Gateway setup already exists at ${existing.join(", ")}. Use --force only after reviewing the existing local setup.`);
24
19
  }
25
20
  }
26
- const authorization = await authorizeProjectConnection({
27
- projectId: options.projectId,
28
- connectionName,
29
- credentialProfile: "customer_gateway",
30
- server: options.server,
31
- openBrowser: options.openBrowser,
32
- fetch: options.fetch,
33
- sleep: options.sleep,
34
- timeoutMs: options.timeoutMs,
35
- output,
36
- configHome: options.configHome,
37
- });
21
+ const authorization = options.authorization
22
+ ? options.authorization
23
+ : await authorizeProjectConnection({
24
+ projectId: options.projectId,
25
+ connectionName,
26
+ credentialProfile: "customer_gateway",
27
+ server: options.server,
28
+ openBrowser: options.openBrowser,
29
+ fetch: options.fetch,
30
+ sleep: options.sleep,
31
+ timeoutMs: options.timeoutMs,
32
+ output,
33
+ configHome: options.configHome,
34
+ });
38
35
  const expectedScopes = ["runs:read", "events:write", "collector:manage"];
39
36
  const missingScopes = expectedScopes.filter((scope) => !authorization.scopes.includes(scope));
40
37
  if (missingScopes.length > 0)
@@ -45,8 +42,8 @@ export async function initializeCustomerGateway(options) {
45
42
  server: authorization.server,
46
43
  connectionName: authorization.connectionName,
47
44
  collectorId: `${slug}-collector`,
48
- host: "127.0.0.1",
49
- port: 8787,
45
+ host: process.env.WITNORA_GATEWAY_HOST?.trim() || "127.0.0.1",
46
+ port: gatewayPort(process.env.WITNORA_GATEWAY_PORT, 8787),
50
47
  storageDirectory: "data",
51
48
  privacyMode: "metadata_only",
52
49
  coverage: { recorded: "configured", enforced: "not_configured", outcomeVerified: "not_configured" },
@@ -56,11 +53,28 @@ export async function initializeCustomerGateway(options) {
56
53
  gatewayToken: randomBytes(32).toString("base64url"),
57
54
  };
58
55
  await mkdir(outDir, { recursive: true });
59
- await writeExclusive(configPath, `${JSON.stringify(config, null, 2)}\n`, options.force ?? false, 0o644);
60
- await writeExclusive(secretsPath, `${JSON.stringify(secrets, null, 2)}\n`, options.force ?? false, 0o600);
61
- await writeExclusive(gitignorePath, "secrets.json\ndata/\n", options.force ?? false, 0o644);
62
- await writeExclusive(clientPath, gatewayClient(config), options.force ?? false, 0o644);
63
- await writeExclusive(readmePath, gatewayReadme(config), options.force ?? false, 0o644);
56
+ const generatedFiles = [];
57
+ const force = options.force ?? false;
58
+ try {
59
+ for (const [path, content, mode] of [
60
+ [configPath, `${JSON.stringify(config, null, 2)}\n`, 0o644],
61
+ [secretsPath, `${JSON.stringify(secrets, null, 2)}\n`, 0o600],
62
+ [gitignorePath, "secrets.json\ndata/\n", 0o644],
63
+ [clientPath, gatewayClient(config), 0o644],
64
+ [readmePath, gatewayReadme(config), 0o644],
65
+ ]) {
66
+ await writeExclusive(path, content, force, mode);
67
+ if (!force)
68
+ generatedFiles.push(path);
69
+ }
70
+ }
71
+ catch (error) {
72
+ if (!force) {
73
+ for (const path of [...generatedFiles].reverse())
74
+ await rm(path, { force: true }).catch(() => undefined);
75
+ }
76
+ throw error;
77
+ }
64
78
  output("\nCustomer-owned Gateway initialized.\n");
65
79
  output(`Configuration: ${configPath}\nLocal secret: ${secretsPath} (never commit or upload)\n`);
66
80
  output("Next:\n");
@@ -69,7 +83,22 @@ export async function initializeCustomerGateway(options) {
69
83
  output(" 3. Import .witnora/gateway/client.mjs at one sandbox workflow boundary.\n");
70
84
  output(" 4. Run that workflow normally; start, event, and complete are recorded locally.\n");
71
85
  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");
72
- return { configPath, secretsPath, config };
86
+ return { configPath, secretsPath, config, generatedFiles };
87
+ }
88
+ export async function inspectCustomerGatewayFiles(options) {
89
+ const repository = resolve(options.repository ?? process.cwd());
90
+ const directory = resolve(repository, options.outDir ?? ".witnora/gateway");
91
+ const paths = gatewayPaths(directory);
92
+ const states = await Promise.all(paths.map(async (path) => ({ path, exists: await exists(path) })));
93
+ const existing = states.filter((item) => item.exists).map((item) => item.path);
94
+ const missing = states.filter((item) => !item.exists).map((item) => item.path);
95
+ return {
96
+ status: existing.length === 0 ? "absent" : missing.length === 0 ? "complete" : "partial",
97
+ directory,
98
+ paths,
99
+ existing,
100
+ missing,
101
+ };
73
102
  }
74
103
  export async function doctorCustomerGateway(options) {
75
104
  const directory = resolve(options.repository ?? process.cwd(), options.dir ?? ".witnora/gateway");
@@ -142,8 +171,8 @@ export async function runCustomerGateway(options) {
142
171
  keyRing,
143
172
  gatewayToken: secrets.gatewayToken,
144
173
  storageDirectory: dataDirectory,
145
- host: config.host,
146
- port: config.port,
174
+ host: process.env.WITNORA_GATEWAY_HOST?.trim() || config.host,
175
+ port: gatewayPort(process.env.WITNORA_GATEWAY_PORT, config.port),
147
176
  environment: "customer-owned",
148
177
  });
149
178
  process.stdout.write(`Witnora customer-owned Gateway listening on ${gateway.baseUrl}\n`);
@@ -179,6 +208,15 @@ function parseSecrets(raw) {
179
208
  }
180
209
  return value;
181
210
  }
211
+ function gatewayPort(value, fallback) {
212
+ if (!value?.trim())
213
+ return fallback;
214
+ const parsed = Number(value);
215
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65_535) {
216
+ throw new Error("WITNORA_GATEWAY_PORT must be an integer between 1 and 65535.");
217
+ }
218
+ return parsed;
219
+ }
182
220
  function gatewayReadme(config) {
183
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`;
184
222
  }
@@ -201,6 +239,15 @@ async function exists(path) {
201
239
  return false;
202
240
  }
203
241
  }
242
+ function gatewayPaths(directory) {
243
+ return [
244
+ join(directory, "gateway.json"),
245
+ join(directory, "secrets.json"),
246
+ join(directory, ".gitignore"),
247
+ join(directory, "client.mjs"),
248
+ join(directory, "README.md"),
249
+ ];
250
+ }
204
251
  function safeSlug(value) {
205
252
  return (value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "agent-repository").slice(0, 48);
206
253
  }
package/dist/onboard.js CHANGED
@@ -1,12 +1,13 @@
1
1
  import { createHash } from "node:crypto";
2
- import { access, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
2
+ import { access, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
3
3
  import { basename, dirname, join, resolve } from "node:path";
4
4
  import { DEFAULT_WITNORA_SERVER } from "./credentials.js";
5
5
  import { authorizeProjectConnection } from "./device-authorization.js";
6
6
  import { verifyControlPlaneConnection } from "./control-plane.js";
7
7
  import { inferPrivateCapabilities, runPrivateCapabilityDiscovery } from "./private-discovery.js";
8
- import { parseAgentTemplate, starterAdapter, starterInstructions, starterProfile, starterTripwireConfig } from "./onboarding-templates.js";
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
11
  export async function runOnboard(options) {
11
12
  const requestFetch = options.fetch ?? fetch;
12
13
  const output = options.output ?? ((message) => process.stdout.write(message));
@@ -17,7 +18,7 @@ export async function runOnboard(options) {
17
18
  const token = await authorizeProjectConnection({
18
19
  projectId: options.projectId,
19
20
  connectionName,
20
- credentialProfile: "repository",
21
+ credentialProfile: "autopilot",
21
22
  server,
22
23
  openBrowser: options.openBrowser,
23
24
  fetch: requestFetch,
@@ -28,40 +29,124 @@ export async function runOnboard(options) {
28
29
  });
29
30
  const credentialsPath = token.credentialsPath;
30
31
  await verifyControlPlaneConnection({ baseUrl: server, projectId: token.projectId, apiKey: token.apiKey, fetch: requestFetch });
31
- const generatedFiles = await generateRepositoryConfig(repositoryPath, repository.template, repository.name);
32
- const selfTest = await writeTryEvidence({
33
- template: repository.template,
34
- subject: `witnora-onboarding-${repository.slug}`,
35
- outDir: join(repositoryPath, ".witnora", "onboarding", "self-test"),
32
+ const planSnapshot = await jsonRequest(requestFetch, `${server}/v1/projects/${encodeURIComponent(token.projectId)}/setup-plans`, {
33
+ method: "GET", headers: { authorization: `Bearer ${token.apiKey}` },
36
34
  });
37
- const bundleBytes = Buffer.from(`${JSON.stringify(selfTest.bundle, null, 2)}\n`);
38
- const receipt = await jsonRequest(requestFetch, `${server}/v1/projects/${encodeURIComponent(token.projectId)}/onboarding/self-test`, {
39
- method: "POST", headers: { authorization: `Bearer ${token.apiKey}`, "content-type": "application/json" },
40
- body: JSON.stringify({
35
+ const setupPlan = planSnapshot.plan;
36
+ if (!setupPlan || !setupPlan.authorizedOperations.some((item) => item.kind === "authorize_install")) {
37
+ throw new Error("Confirm the Setup Plan and authorize installation in the Witnora Workspace before running onboard.");
38
+ }
39
+ const attemptId = `install-${Date.now()}-${randomSuffix()}`;
40
+ const generatedFiles = [];
41
+ await reportInstall(requestFetch, server, token.projectId, token.apiKey, setupPlan.id, { status: "installing", attemptId });
42
+ try {
43
+ generatedFiles.push(...await generateRepositoryConfig(repositoryPath, repository.template, repository.name));
44
+ const selfTestDir = join(repositoryPath, ".witnora", "onboarding", "self-test", attemptId);
45
+ const selfTest = await writeTryEvidence({
41
46
  template: repository.template,
42
- repository: { kind: repository.kind, name: repository.name, fingerprintSha256: repository.fingerprintSha256 },
43
- bundleSha256: createHash("sha256").update(bundleBytes).digest("hex"),
44
- }),
45
- });
46
- const receiptPath = join(repositoryPath, ".witnora", "onboarding", "receipt.json");
47
- await mkdir(dirname(receiptPath), { recursive: true });
48
- await writeFile(receiptPath, `${JSON.stringify(receipt, null, 2)}\n`);
49
- const discovery = await runPrivateCapabilityDiscovery({
50
- connection: { server, projectId: token.projectId, apiKey: token.apiKey },
51
- repository,
52
- connectionName: token.connectionName,
53
- configHome: options.configHome,
54
- fetch: requestFetch,
47
+ subject: `witnora-onboarding-${repository.slug}`,
48
+ outDir: selfTestDir,
49
+ });
50
+ generatedFiles.push(selfTestDir);
51
+ const bundleBytes = Buffer.from(`${JSON.stringify(selfTest.bundle, null, 2)}\n`);
52
+ const receipt = await jsonRequest(requestFetch, `${server}/v1/projects/${encodeURIComponent(token.projectId)}/onboarding/self-test`, {
53
+ method: "POST", headers: { authorization: `Bearer ${token.apiKey}`, "content-type": "application/json" },
54
+ body: JSON.stringify({
55
+ template: repository.template,
56
+ repository: { kind: repository.kind, name: repository.name, fingerprintSha256: repository.fingerprintSha256 },
57
+ bundleSha256: createHash("sha256").update(bundleBytes).digest("hex"),
58
+ }),
59
+ });
60
+ const receiptPath = join(repositoryPath, ".witnora", "onboarding", "receipts", `${attemptId}.json`);
61
+ await mkdir(dirname(receiptPath), { recursive: true });
62
+ await writeFile(receiptPath, `${JSON.stringify(receipt, null, 2)}\n`);
63
+ generatedFiles.push(receiptPath);
64
+ const legacyReceiptPath = join(repositoryPath, ".witnora", "onboarding", "receipt.json");
65
+ if (!await exists(legacyReceiptPath)) {
66
+ await writeFile(legacyReceiptPath, `${JSON.stringify(receipt, null, 2)}\n`);
67
+ generatedFiles.push(legacyReceiptPath);
68
+ }
69
+ const discovery = await runPrivateCapabilityDiscovery({
70
+ connection: { server, projectId: token.projectId, apiKey: token.apiKey },
71
+ repository,
72
+ connectionName: token.connectionName,
73
+ configHome: options.configHome,
74
+ fetch: requestFetch,
75
+ });
76
+ const gatewayState = await inspectCustomerGatewayFiles({ repository: repositoryPath });
77
+ if (gatewayState.status === "partial") {
78
+ throw new Error(`Existing Gateway setup is incomplete. Preserve the existing files and repair or remove the partial setup in Advanced mode. Missing: ${gatewayState.missing.map((path) => basename(path)).join(", ")}.`);
79
+ }
80
+ if (gatewayState.status === "absent") {
81
+ const gateway = await initializeCustomerGateway({
82
+ projectId: token.projectId, server, repository: repositoryPath, authorization: token,
83
+ fetch: requestFetch, configHome: options.configHome, output,
84
+ });
85
+ generatedFiles.push(...gateway.generatedFiles);
86
+ }
87
+ else {
88
+ output("\nExisting complete customer-owned Gateway found; verifying and reusing it.\n");
89
+ }
90
+ generatedFiles.push(...await generateAutopilotFiles(repositoryPath, repository.name));
91
+ const doctor = await doctorCustomerGateway({ repository: repositoryPath, configHome: options.configHome, fetch: requestFetch });
92
+ if (doctor.overall !== "READY_TO_RECORD")
93
+ throw new Error(doctor.checks.filter((check) => check.status === "FAIL").map((check) => check.message).join(" "));
94
+ await reportInstall(requestFetch, server, token.projectId, token.apiKey, setupPlan.id, {
95
+ status: "verified", attemptId, generatedFiles: relativeGeneratedFiles(repositoryPath, generatedFiles),
96
+ });
97
+ output(`\nWitnora Setup Autopilot completed for ${repository.name}.\n`);
98
+ output(`Project: ${token.projectId}\nTemplate: ${repository.template} (${repository.kind})\n`);
99
+ output(`Credentials: ${credentialsPath}\nSelf-test receipt: ${receiptPath}\n`);
100
+ output(`Private discovery: ${discovery.capabilityCount} capability group(s); ${discovery.unknownCapabilityCount} pending confirmation.\n`);
101
+ output("Installed: customer-owned Gateway, default-deny policy, independent-probe contract, review contract, and PR/release/nightly CI.\n");
102
+ 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");
103
+ return { projectId: token.projectId, connectionName: token.connectionName, credentialsPath, template: repository.template,
104
+ repositoryKind: repository.kind, generatedFiles, receiptPath, discovery, setupPlanId: setupPlan.id,
105
+ gatewayDirectory: join(repositoryPath, ".witnora", "gateway") };
106
+ }
107
+ catch (error) {
108
+ const diagnosis = error instanceof Error ? error.message : String(error);
109
+ await rollbackGeneratedFiles(generatedFiles);
110
+ await reportInstall(requestFetch, server, token.projectId, token.apiKey, setupPlan.id, {
111
+ status: "failed", attemptId, generatedFiles: relativeGeneratedFiles(repositoryPath, generatedFiles), diagnosis, rolledBack: true,
112
+ }).catch(() => undefined);
113
+ throw new Error(`Witnora Setup Autopilot rolled back this install attempt: ${diagnosis}`);
114
+ }
115
+ }
116
+ async function generateAutopilotFiles(repositoryPath, subject) {
117
+ const files = new Map([
118
+ [".witnora/setup/policy.json", `${JSON.stringify({ schemaVersion: "witnora.setup_policy.v0.1", subject, unknownCapabilities: "pending_confirmation", defaultDecision: "deny", environment: "sandbox" }, null, 2)}\n`],
119
+ [".witnora/setup/outcome-probe.json", `${JSON.stringify({ schemaVersion: "witnora.outcome_probe_contract.v0.1", mode: "independent_read_only", credentialEnv: "WITNORA_OUTCOME_PROBE_CREDENTIAL", status: "awaiting_customer_secret", uploadsCredential: false }, null, 2)}\n`],
120
+ [".witnora/setup/review.json", `${JSON.stringify({ schemaVersion: "witnora.review_contract.v0.1", independentReviewRequired: true, syntheticEvidenceEligible: false }, null, 2)}\n`],
121
+ [".github/workflows/witnora-assurance.yml", continuousAssuranceWorkflow()],
122
+ ]);
123
+ const written = [];
124
+ for (const [relativePath, content] of files) {
125
+ const target = join(repositoryPath, relativePath);
126
+ if (await exists(target))
127
+ continue;
128
+ await mkdir(dirname(target), { recursive: true });
129
+ await writeFile(target, content);
130
+ written.push(target);
131
+ }
132
+ return written;
133
+ }
134
+ function continuousAssuranceWorkflow() {
135
+ return `name: Witnora assurance\non:\n pull_request:\n push:\n tags: [\"v*\"]\n schedule:\n - cron: \"17 3 * * *\"\n workflow_dispatch:\npermissions:\n contents: read\njobs:\n assurance:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - uses: actions/setup-node@v4\n with:\n node-version: 22\n - run: npx witnora@latest release-gate --config witnora.config.json --strict\n`;
136
+ }
137
+ async function reportInstall(requestFetch, server, projectId, apiKey, setupPlanId, body) {
138
+ await jsonRequest(requestFetch, `${server}/v1/projects/${encodeURIComponent(projectId)}/setup-plans/${encodeURIComponent(setupPlanId)}/install-status`, {
139
+ method: "POST", headers: { authorization: `Bearer ${apiKey}`, "content-type": "application/json" }, body: JSON.stringify(body),
55
140
  });
56
- output(`\nConnected ${repository.name} to Witnora.\n`);
57
- output(`Project: ${token.projectId}\nTemplate: ${repository.template} (${repository.kind})\n`);
58
- output(`Credentials: ${credentialsPath}\nSelf-test receipt: ${receiptPath}\n`);
59
- output(`Private discovery: ${discovery.capabilityCount} capability group(s); source code, prompts, credentials, and payloads stayed local.\n`);
60
- output("The synthetic self-test verified the local evidence path. It did not create a run, evidence object, release decision, or CURRENT assurance.\n");
61
- output(starterInstructions(repository.template, repository.name));
62
- return { projectId: token.projectId, connectionName: token.connectionName, credentialsPath, template: repository.template,
63
- repositoryKind: repository.kind, generatedFiles, receiptPath, discovery };
64
141
  }
142
+ async function rollbackGeneratedFiles(files) {
143
+ for (const path of [...files].reverse())
144
+ await rm(path, { recursive: true, force: true }).catch(() => undefined);
145
+ }
146
+ function relativeGeneratedFiles(repositoryPath, files) {
147
+ return files.map((file) => file.startsWith(repositoryPath) ? file.slice(repositoryPath.length + 1).replaceAll("\\", "/") : file).filter(Boolean);
148
+ }
149
+ function randomSuffix() { return Math.random().toString(36).slice(2, 10); }
65
150
  export async function inspectRepository(repositoryPath, explicitTemplate) {
66
151
  const entries = await readdir(repositoryPath, { withFileTypes: true });
67
152
  const names = entries.map((entry) => entry.name).sort();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "witnora",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "Independent assurance for covered agent action paths across models and frameworks.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",