witnora 0.8.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/README.md +275 -12
  2. package/dist/artifact-manifest.js +37 -0
  3. package/dist/artifact-validation.js +102 -0
  4. package/dist/badge.js +31 -0
  5. package/dist/browser-adapter.js +156 -0
  6. package/dist/bundle.js +100 -0
  7. package/dist/cli.js +971 -0
  8. package/dist/command-help.js +71 -0
  9. package/dist/companion-artifacts.js +170 -0
  10. package/dist/conformance.js +115 -0
  11. package/dist/control-plane.js +409 -0
  12. package/dist/corpus-governance.js +126 -0
  13. package/dist/corpus-store.js +239 -0
  14. package/dist/corpus.js +438 -0
  15. package/dist/credentials.js +127 -0
  16. package/dist/evidence-signing.js +85 -0
  17. package/dist/failure-review.js +188 -0
  18. package/dist/guided-setup.js +101 -0
  19. package/dist/index.js +16 -0
  20. package/dist/lab.js +323 -0
  21. package/dist/local-server.js +491 -0
  22. package/dist/monitor.js +100 -0
  23. package/dist/normalizers.js +265 -0
  24. package/dist/onboarding-templates.js +158 -0
  25. package/dist/release-gate.js +457 -0
  26. package/dist/report.js +165 -0
  27. package/dist/runner.js +349 -0
  28. package/dist/sandbox.js +424 -0
  29. package/dist/schema-validator.js +503 -0
  30. package/dist/try.js +135 -0
  31. package/dist/types.js +3 -0
  32. package/dist/vendor/onegent-runtime/browser-adapter-kit.d.ts +107 -0
  33. package/dist/vendor/onegent-runtime/browser-adapter-kit.d.ts.map +1 -0
  34. package/dist/vendor/onegent-runtime/browser-adapter-kit.js +187 -0
  35. package/dist/vendor/onegent-runtime/browser-enforcement-runtime.d.ts +183 -0
  36. package/dist/vendor/onegent-runtime/browser-enforcement-runtime.d.ts.map +1 -0
  37. package/dist/vendor/onegent-runtime/browser-enforcement-runtime.js +344 -0
  38. package/dist/vendor/onegent-runtime/controlled-adapter.d.ts +6 -0
  39. package/dist/vendor/onegent-runtime/controlled-adapter.d.ts.map +1 -0
  40. package/dist/vendor/onegent-runtime/controlled-adapter.js +40 -0
  41. package/dist/vendor/onegent-runtime/mock-procurement.d.ts +6 -0
  42. package/dist/vendor/onegent-runtime/mock-procurement.d.ts.map +1 -0
  43. package/dist/vendor/onegent-runtime/mock-procurement.js +33 -0
  44. package/dist/vendor/onegent-runtime/policies.d.ts +6 -0
  45. package/dist/vendor/onegent-runtime/policies.d.ts.map +1 -0
  46. package/dist/vendor/onegent-runtime/policies.js +109 -0
  47. package/dist/vendor/onegent-runtime/risk.d.ts +3 -0
  48. package/dist/vendor/onegent-runtime/risk.d.ts.map +1 -0
  49. package/dist/vendor/onegent-runtime/risk.js +54 -0
  50. package/dist/vendor/onegent-runtime/sandbox-adapter-kit.d.ts +45 -0
  51. package/dist/vendor/onegent-runtime/sandbox-adapter-kit.d.ts.map +1 -0
  52. package/dist/vendor/onegent-runtime/sandbox-adapter-kit.js +148 -0
  53. package/dist/vendor/onegent-runtime/sandbox-harness.d.ts +167 -0
  54. package/dist/vendor/onegent-runtime/sandbox-harness.d.ts.map +1 -0
  55. package/dist/vendor/onegent-runtime/sandbox-harness.js +729 -0
  56. package/dist/vendor/onegent-runtime/sandbox-hosted.d.ts +77 -0
  57. package/dist/vendor/onegent-runtime/sandbox-hosted.d.ts.map +1 -0
  58. package/dist/vendor/onegent-runtime/sandbox-hosted.js +152 -0
  59. package/dist/vendor/onegent-runtime/sdk.d.ts +29 -0
  60. package/dist/vendor/onegent-runtime/sdk.d.ts.map +1 -0
  61. package/dist/vendor/onegent-runtime/sdk.js +140 -0
  62. package/dist/vendor/onegent-runtime/service.d.ts +27 -0
  63. package/dist/vendor/onegent-runtime/service.d.ts.map +1 -0
  64. package/dist/vendor/onegent-runtime/service.js +423 -0
  65. package/dist/vendor/onegent-runtime/store.d.ts +16 -0
  66. package/dist/vendor/onegent-runtime/store.d.ts.map +1 -0
  67. package/dist/vendor/onegent-runtime/store.js +29 -0
  68. package/dist/vendor/onegent-runtime/stripe-test-readonly.d.ts +79 -0
  69. package/dist/vendor/onegent-runtime/stripe-test-readonly.d.ts.map +1 -0
  70. package/dist/vendor/onegent-runtime/stripe-test-readonly.js +163 -0
  71. package/dist/vendor/onegent-runtime/trust-crypto.d.ts +11 -0
  72. package/dist/vendor/onegent-runtime/trust-crypto.d.ts.map +1 -0
  73. package/dist/vendor/onegent-runtime/trust-crypto.js +70 -0
  74. package/dist/vendor/onegent-runtime/trust-types.d.ts +163 -0
  75. package/dist/vendor/onegent-runtime/trust-types.d.ts.map +1 -0
  76. package/dist/vendor/onegent-runtime/trust-types.js +7 -0
  77. package/dist/vendor/onegent-runtime/trusted-recorder.d.ts +49 -0
  78. package/dist/vendor/onegent-runtime/trusted-recorder.d.ts.map +1 -0
  79. package/dist/vendor/onegent-runtime/trusted-recorder.js +347 -0
  80. package/dist/vendor/onegent-runtime/types.d.ts +287 -0
  81. package/dist/vendor/onegent-runtime/types.d.ts.map +1 -0
  82. package/dist/vendor/onegent-runtime/types.js +1 -0
  83. package/dist/vendor/onegent-runtime/vendor-sandbox-egress.d.ts +49 -0
  84. package/dist/vendor/onegent-runtime/vendor-sandbox-egress.d.ts.map +1 -0
  85. package/dist/vendor/onegent-runtime/vendor-sandbox-egress.js +152 -0
  86. package/package.json +33 -13
  87. package/bin/witnora.js +0 -3
@@ -0,0 +1,127 @@
1
+ import { chmod, mkdir, readFile, rename, writeFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join } from "node:path";
4
+ import { randomUUID } from "node:crypto";
5
+ export const DEFAULT_WITNORA_SERVER = "https://witnora.com";
6
+ /** @deprecated Use DEFAULT_WITNORA_SERVER. */
7
+ export const DEFAULT_AGENTCERT_SERVER = DEFAULT_WITNORA_SERVER;
8
+ export async function saveConnection(name, input, options = {}) {
9
+ const connectionName = validateConnectionName(name);
10
+ const connection = validateConnection(input);
11
+ const path = credentialsPath(options);
12
+ const current = await readCredentialFile(path);
13
+ const next = {
14
+ schemaVersion: "agentcert.credentials.v1",
15
+ defaultConnection: connectionName,
16
+ connections: { ...current?.connections, [connectionName]: connection },
17
+ };
18
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
19
+ const temporaryPath = `${path}.${randomUUID()}.tmp`;
20
+ await writeFile(temporaryPath, `${JSON.stringify(next, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
21
+ await rename(temporaryPath, path);
22
+ await chmod(path, 0o600).catch(() => undefined);
23
+ return path;
24
+ }
25
+ export async function loadConnection(name, options = {}) {
26
+ const file = await readCredentialFile(credentialsPath(options))
27
+ ?? (options.configHome ? undefined : await readCredentialFile(legacyCredentialsPath()));
28
+ if (!file)
29
+ return undefined;
30
+ const connectionName = name ? validateConnectionName(name) : file.defaultConnection;
31
+ const connection = file.connections[connectionName];
32
+ return connection ? validateConnection(connection) : undefined;
33
+ }
34
+ export async function resolveConnection(options = {}) {
35
+ const env = options.env ?? process.env;
36
+ const stored = options.server && options.projectId && options.apiKey
37
+ ? undefined
38
+ : await loadConnection(options.name, options);
39
+ if (options.name && stored && !options.server && !options.projectId && !options.apiKey) {
40
+ return stored;
41
+ }
42
+ const candidate = {
43
+ server: options.server ?? env.WITNORA_BASE_URL ?? env.AGENTCERT_BASE_URL ?? stored?.server,
44
+ projectId: options.projectId ?? env.WITNORA_PROJECT_ID ?? env.AGENTCERT_PROJECT_ID ?? stored?.projectId,
45
+ apiKey: options.apiKey ?? env.WITNORA_API_KEY ?? env.AGENTCERT_API_KEY ?? stored?.apiKey,
46
+ };
47
+ const missing = Object.entries(candidate).filter(([, value]) => !value).map(([key]) => key);
48
+ if (missing.length > 0) {
49
+ throw new Error(`Hosted connection is incomplete (${missing.join(", ")} missing). Run \`witnora connect\` or set WITNORA_BASE_URL, WITNORA_PROJECT_ID, and WITNORA_API_KEY. Legacy AGENTCERT_* variables remain supported.`);
50
+ }
51
+ return validateConnection(candidate);
52
+ }
53
+ export function credentialsPath(options = {}) {
54
+ const configHome = options.configHome
55
+ ?? process.env.WITNORA_CONFIG_HOME
56
+ ?? process.env.AGENTCERT_CONFIG_HOME
57
+ ?? join(homedir(), ".witnora");
58
+ return join(configHome, "credentials.json");
59
+ }
60
+ function legacyCredentialsPath() {
61
+ return join(homedir(), ".agentcert", "credentials.json");
62
+ }
63
+ function validateConnection(input) {
64
+ const projectId = input.projectId.trim();
65
+ const apiKey = input.apiKey.trim();
66
+ if (!projectId)
67
+ throw new Error("Witnora project ID is required.");
68
+ if (!apiKey.startsWith("ac_live_"))
69
+ throw new Error("Witnora API key must start with ac_live_.");
70
+ return { server: normalizeServer(input.server), projectId, apiKey };
71
+ }
72
+ function normalizeServer(value) {
73
+ let url;
74
+ try {
75
+ url = new URL(value.trim());
76
+ }
77
+ catch {
78
+ throw new Error("Witnora server must be a valid HTTP or HTTPS URL.");
79
+ }
80
+ const loopback = new Set(["localhost", "127.0.0.1", "[::1]"]).has(url.hostname);
81
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) {
82
+ throw new Error("Witnora server must use HTTPS. Plain HTTP is allowed only for localhost development.");
83
+ }
84
+ if (url.username || url.password || url.search || url.hash) {
85
+ throw new Error("Witnora server URL cannot contain credentials, query parameters, or a fragment.");
86
+ }
87
+ return url.toString().replace(/\/$/, "");
88
+ }
89
+ function validateConnectionName(value) {
90
+ const name = value.trim();
91
+ if (!/^[A-Za-z0-9._-]{1,64}$/.test(name)) {
92
+ throw new Error("Connection name must contain 1-64 letters, numbers, dots, underscores, or hyphens.");
93
+ }
94
+ return name;
95
+ }
96
+ async function readCredentialFile(path) {
97
+ let raw;
98
+ try {
99
+ raw = await readFile(path, "utf8");
100
+ }
101
+ catch (error) {
102
+ if (error.code === "ENOENT")
103
+ return undefined;
104
+ throw error;
105
+ }
106
+ let value;
107
+ try {
108
+ value = JSON.parse(raw);
109
+ }
110
+ catch {
111
+ throw new Error(`Witnora credentials file is not valid JSON: ${path}`);
112
+ }
113
+ if (!isCredentialFile(value)) {
114
+ throw new Error(`Witnora credentials file has an unsupported format: ${path}`);
115
+ }
116
+ return value;
117
+ }
118
+ function isCredentialFile(value) {
119
+ if (!value || typeof value !== "object" || Array.isArray(value))
120
+ return false;
121
+ const file = value;
122
+ return file.schemaVersion === "agentcert.credentials.v1"
123
+ && typeof file.defaultConnection === "string"
124
+ && Boolean(file.connections)
125
+ && typeof file.connections === "object"
126
+ && !Array.isArray(file.connections);
127
+ }
@@ -0,0 +1,85 @@
1
+ import { createHash, createPrivateKey, createPublicKey, generateKeyPairSync, sign, verify } from "node:crypto";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { dirname, resolve } from "node:path";
4
+ export const AGENTCERT_SIGNATURE_SCHEMA_VERSION = "agentcert.evidence_signature.v0.1";
5
+ export async function generateEvidenceSigningKeyPair(privateKeyPath, publicKeyPath) {
6
+ const { privateKey, publicKey } = generateKeyPairSync("ed25519", {
7
+ privateKeyEncoding: { type: "pkcs8", format: "pem" },
8
+ publicKeyEncoding: { type: "spki", format: "pem" },
9
+ });
10
+ await Promise.all([
11
+ writePrivateFile(privateKeyPath, privateKey),
12
+ writePublicFile(publicKeyPath, publicKey),
13
+ ]);
14
+ }
15
+ export async function signEvidenceFile(artifactPath, privateKeyPath, signaturePath = `${artifactPath}.sig.json`) {
16
+ const [artifact, privatePem] = await Promise.all([readFile(resolve(artifactPath)), readFile(resolve(privateKeyPath), "utf8")]);
17
+ const privateKey = createPrivateKey(privatePem);
18
+ const publicKey = createPublicKey(privateKey);
19
+ const envelope = {
20
+ schemaVersion: AGENTCERT_SIGNATURE_SCHEMA_VERSION,
21
+ kind: "agentcert.evidence_signature",
22
+ algorithm: "Ed25519",
23
+ keyId: keyId(publicKey),
24
+ signedAt: new Date().toISOString(),
25
+ artifactPath: artifactPath.replace(/\\/g, "/"),
26
+ artifactSha256: sha256(artifact),
27
+ signature: sign(null, artifact, privateKey).toString("base64"),
28
+ };
29
+ const resolvedSignaturePath = resolve(signaturePath);
30
+ await mkdir(dirname(resolvedSignaturePath), { recursive: true });
31
+ await writeFile(resolvedSignaturePath, `${JSON.stringify(envelope, null, 2)}\n`);
32
+ return envelope;
33
+ }
34
+ export async function verifyEvidenceFile(artifactPath, signaturePath, publicKeyPath) {
35
+ const [artifact, signatureRaw, publicPem] = await Promise.all([
36
+ readFile(resolve(artifactPath)),
37
+ readFile(resolve(signaturePath), "utf8"),
38
+ readFile(resolve(publicKeyPath), "utf8"),
39
+ ]);
40
+ const envelope = JSON.parse(signatureRaw);
41
+ const errors = validateEvidenceSignature(envelope);
42
+ if (errors.length > 0)
43
+ throw new Error(`Invalid evidence signature: ${errors.join(" ")}`);
44
+ const publicKey = createPublicKey(publicPem);
45
+ const artifactSha256 = sha256(artifact);
46
+ const actualKeyId = keyId(publicKey);
47
+ const digestMatches = envelope.artifactSha256 === artifactSha256;
48
+ const keyIdMatches = envelope.keyId === actualKeyId;
49
+ const signatureMatches = verify(null, artifact, publicKey, Buffer.from(envelope.signature, "base64"));
50
+ return { valid: digestMatches && signatureMatches && keyIdMatches, digestMatches, signatureMatches, keyIdMatches, artifactSha256, keyId: actualKeyId };
51
+ }
52
+ export function validateEvidenceSignature(input) {
53
+ if (!input || typeof input !== "object" || Array.isArray(input))
54
+ return ["$ must be an object."];
55
+ const value = input;
56
+ const errors = [];
57
+ if (value.schemaVersion !== AGENTCERT_SIGNATURE_SCHEMA_VERSION)
58
+ errors.push(`schemaVersion must be ${JSON.stringify(AGENTCERT_SIGNATURE_SCHEMA_VERSION)}.`);
59
+ if (value.kind !== "agentcert.evidence_signature")
60
+ errors.push('kind must be "agentcert.evidence_signature".');
61
+ if (value.algorithm !== "Ed25519")
62
+ errors.push('algorithm must be "Ed25519".');
63
+ for (const field of ["keyId", "signedAt", "artifactPath", "artifactSha256", "signature"]) {
64
+ if (typeof value[field] !== "string" || value[field].length === 0)
65
+ errors.push(`${field} must be a non-empty string.`);
66
+ }
67
+ return errors;
68
+ }
69
+ function keyId(publicKey) {
70
+ const der = publicKey.export({ type: "spki", format: "der" });
71
+ return `sha256:${sha256(der).slice(0, 32)}`;
72
+ }
73
+ function sha256(input) {
74
+ return createHash("sha256").update(input).digest("hex");
75
+ }
76
+ async function writePrivateFile(path, content) {
77
+ const resolved = resolve(path);
78
+ await mkdir(dirname(resolved), { recursive: true });
79
+ await writeFile(resolved, content, { mode: 0o600, flag: "wx" });
80
+ }
81
+ async function writePublicFile(path, content) {
82
+ const resolved = resolve(path);
83
+ await mkdir(dirname(resolved), { recursive: true });
84
+ await writeFile(resolved, content, { flag: "wx" });
85
+ }
@@ -0,0 +1,188 @@
1
+ import { createHash } from "node:crypto";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { dirname, resolve } from "node:path";
4
+ export const FAILURE_TYPES = [
5
+ "prompt_injection",
6
+ "wrong_click",
7
+ "timeout",
8
+ "verification_gap",
9
+ "silent_partial_success",
10
+ "network_failure",
11
+ "ui_drift",
12
+ "policy_or_approval",
13
+ "agent_connection",
14
+ "console_error",
15
+ "assertion_failure",
16
+ "unknown_failure",
17
+ ];
18
+ export function createFailureReview(input) {
19
+ const reviewedAt = input.reviewedAt ?? new Date().toISOString();
20
+ const reviewer = input.reviewer ?? "human-reviewer";
21
+ const confidence = normalizeReviewConfidence(input.confidence);
22
+ const id = stableReviewId({
23
+ reviewedAt,
24
+ reviewer,
25
+ target: input.target,
26
+ type: input.type,
27
+ status: input.status,
28
+ });
29
+ return {
30
+ schemaVersion: "1",
31
+ kind: "agentcert.failure_review",
32
+ id,
33
+ reviewedAt,
34
+ reviewer,
35
+ status: input.status,
36
+ target: input.target,
37
+ suggestedType: input.suggestedType,
38
+ type: input.type,
39
+ note: input.note,
40
+ confidence,
41
+ evidenceContext: input.evidenceContext,
42
+ taxonomyRationale: input.taxonomyRationale,
43
+ };
44
+ }
45
+ export function parseFailureType(input) {
46
+ if (input && FAILURE_TYPES.includes(input)) {
47
+ return input;
48
+ }
49
+ throw new Error(`Unknown failure type "${input ?? ""}". Use one of: ${FAILURE_TYPES.join(", ")}.`);
50
+ }
51
+ export function parseFailureReviewStatus(input, type, suggestedType) {
52
+ if (input === "confirmed" || input === "corrected") {
53
+ return input;
54
+ }
55
+ return suggestedType && type === suggestedType ? "confirmed" : "corrected";
56
+ }
57
+ export function parseReviewConfidence(input) {
58
+ if (input === undefined)
59
+ return undefined;
60
+ const value = typeof input === "number" ? input : Number(input);
61
+ return normalizeReviewConfidence(value);
62
+ }
63
+ export async function readFailureReviews(path) {
64
+ if (!path)
65
+ return [];
66
+ try {
67
+ const raw = await readFile(resolve(path), "utf8");
68
+ return raw
69
+ .split(/\r?\n/)
70
+ .filter((line) => line.trim().length > 0)
71
+ .map((line) => JSON.parse(line))
72
+ .filter((review) => review.kind === "agentcert.failure_review");
73
+ }
74
+ catch (error) {
75
+ if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
76
+ return [];
77
+ }
78
+ throw error;
79
+ }
80
+ }
81
+ export async function appendFailureReview(path, review) {
82
+ const outPath = resolve(path);
83
+ await mkdir(dirname(outPath), { recursive: true });
84
+ const existing = await readFile(outPath, "utf8").catch((error) => {
85
+ if (error && typeof error === "object" && "code" in error && error.code === "ENOENT")
86
+ return "";
87
+ throw error;
88
+ });
89
+ const prefix = existing.trimEnd();
90
+ const separator = prefix.length > 0 ? "\n" : "";
91
+ await writeFile(outPath, `${prefix}${separator}${JSON.stringify(review)}\n`);
92
+ }
93
+ export function applyFailureReviews(records, reviews) {
94
+ if (reviews.length === 0) {
95
+ return records.map(normalizeRecordReviewState);
96
+ }
97
+ const sortedReviews = [...reviews].sort((left, right) => left.reviewedAt.localeCompare(right.reviewedAt));
98
+ return records.map((record) => {
99
+ const failurePatterns = record.failurePatterns.map((pattern) => applyReviewToPattern(record, pattern, sortedReviews));
100
+ return {
101
+ ...record,
102
+ failurePatterns,
103
+ metadata: {
104
+ ...record.metadata,
105
+ taxonomyReview: {
106
+ reviewedFailurePatterns: failurePatterns.filter((pattern) => pattern.reviewStatus === "confirmed" || pattern.reviewStatus === "corrected").length,
107
+ correctedFailurePatterns: failurePatterns.filter((pattern) => pattern.reviewStatus === "corrected").length,
108
+ unreviewedFailurePatterns: failurePatterns.filter((pattern) => (pattern.reviewStatus ?? "unreviewed") === "unreviewed").length,
109
+ },
110
+ },
111
+ };
112
+ });
113
+ }
114
+ export function findFailurePattern(records, target) {
115
+ for (const record of records) {
116
+ if (!recordMatchesTarget(record, target))
117
+ continue;
118
+ const pattern = record.failurePatterns.find((item) => item.key === target.patternKey);
119
+ if (pattern)
120
+ return { record, pattern };
121
+ }
122
+ return undefined;
123
+ }
124
+ function normalizeRecordReviewState(record) {
125
+ return {
126
+ ...record,
127
+ failurePatterns: record.failurePatterns.map((pattern) => ({
128
+ ...pattern,
129
+ suggestedType: pattern.suggestedType ?? pattern.type,
130
+ reviewStatus: pattern.reviewStatus ?? "unreviewed",
131
+ })),
132
+ };
133
+ }
134
+ function applyReviewToPattern(record, pattern, reviews) {
135
+ const suggestedType = pattern.suggestedType ?? pattern.type;
136
+ let next = {
137
+ ...pattern,
138
+ suggestedType,
139
+ reviewStatus: pattern.reviewStatus ?? "unreviewed",
140
+ };
141
+ for (const review of reviews) {
142
+ if (!reviewApplies(record, pattern, review))
143
+ continue;
144
+ next = {
145
+ ...next,
146
+ type: review.type,
147
+ suggestedType,
148
+ reviewStatus: review.status,
149
+ reviewId: review.id,
150
+ reviewedAt: review.reviewedAt,
151
+ reviewer: review.reviewer,
152
+ reviewNote: review.note,
153
+ reviewConfidence: review.confidence,
154
+ reviewEvidenceContext: review.evidenceContext,
155
+ taxonomyRationale: review.taxonomyRationale,
156
+ };
157
+ }
158
+ return next;
159
+ }
160
+ function reviewApplies(record, pattern, review) {
161
+ if (review.target.patternKey !== pattern.key)
162
+ return false;
163
+ return recordMatchesTarget(record, review.target);
164
+ }
165
+ function recordMatchesTarget(record, target) {
166
+ if (target.recordId && target.recordId !== record.id)
167
+ return false;
168
+ if (target.runId && target.runId !== record.runId)
169
+ return false;
170
+ if (target.product && target.product !== record.product)
171
+ return false;
172
+ if (target.scenarioName && target.scenarioName !== record.scenarioName)
173
+ return false;
174
+ if (target.faultName && target.faultName !== record.faultName)
175
+ return false;
176
+ return true;
177
+ }
178
+ function stableReviewId(input) {
179
+ return createHash("sha256").update(JSON.stringify(input)).digest("hex").slice(0, 16);
180
+ }
181
+ function normalizeReviewConfidence(input) {
182
+ if (input === undefined)
183
+ return undefined;
184
+ if (!Number.isFinite(input) || input < 0 || input > 1) {
185
+ throw new Error("Review confidence must be a number from 0 to 1.");
186
+ }
187
+ return input;
188
+ }
@@ -0,0 +1,101 @@
1
+ import { access, mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { dirname, relative, resolve } from "node:path";
3
+ import { spawnSync } from "node:child_process";
4
+ import { starterAdapter, starterGitHubActionWorkflow, starterProfile, starterTripwireConfig } from "./onboarding-templates.js";
5
+ export async function applyGuidedSetup(options) {
6
+ const repository = resolve(options.repository);
7
+ const plan = parseGuidedSetupPlan(JSON.parse(await readFile(resolve(options.planPath), "utf8")));
8
+ const subject = slug(plan.intent.agentName);
9
+ const files = generatedFiles(plan.intent.boundary, subject);
10
+ for (const [relativePath, content] of files) {
11
+ const target = resolve(repository, relativePath);
12
+ if (!isInside(repository, target))
13
+ throw new Error(`Generated path escapes the repository: ${relativePath}`);
14
+ if (!options.force && await exists(target))
15
+ throw new Error(`${relativePath} already exists. Re-run with --force only after reviewing the existing file.`);
16
+ await mkdir(dirname(target), { recursive: true });
17
+ await writeFile(target, content, "utf8");
18
+ }
19
+ const redactedPlan = { schemaVersion: plan.schemaVersion, id: plan.id, intent: plan.intent };
20
+ const planTarget = resolve(repository, ".witnora/setup-plan.json");
21
+ await mkdir(dirname(planTarget), { recursive: true });
22
+ await writeFile(planTarget, `${JSON.stringify(redactedPlan, null, 2)}\n`, "utf8");
23
+ const written = [...files.keys(), ".witnora/setup-plan.json"];
24
+ let pullRequestUrl;
25
+ if (options.configureCi && !options.githubPullRequest)
26
+ throw new Error("--configure-ci requires --github-pr so credential changes are tied to a reviewable integration.");
27
+ if (options.githubPullRequest)
28
+ pullRequestUrl = createSetupPullRequest(repository, plan, written, options.execute ?? defaultExecute, options.configureCi ?? false);
29
+ return { planId: plan.id, template: plan.intent.boundary, files: written, pullRequestUrl, ciConfigured: options.configureCi ?? false };
30
+ }
31
+ export function parseGuidedSetupPlan(value) {
32
+ if (!value || typeof value !== "object" || Array.isArray(value))
33
+ throw new Error("Setup plan must be a JSON object.");
34
+ const plan = value;
35
+ if (plan.schemaVersion !== "agentcert.setup_plan.v0.1")
36
+ throw new Error("Setup plan schemaVersion must be agentcert.setup_plan.v0.1.");
37
+ if (typeof plan.id !== "string" || !/^[0-9a-f-]{20,}$/i.test(plan.id))
38
+ throw new Error("Setup plan id is invalid.");
39
+ const intent = plan.intent && typeof plan.intent === "object" && !Array.isArray(plan.intent) ? plan.intent : {};
40
+ const boundary = intent.boundary;
41
+ if (boundary !== "browser" && boundary !== "coding" && boundary !== "mcp" && boundary !== "workflow" && boundary !== "data")
42
+ throw new Error("Setup plan boundary is not supported.");
43
+ if (typeof intent.agentName !== "string" || !intent.agentName.trim())
44
+ throw new Error("Setup plan agentName is required.");
45
+ return { schemaVersion: "agentcert.setup_plan.v0.1", id: plan.id, intent: { agentName: intent.agentName.trim().slice(0, 120), boundary } };
46
+ }
47
+ function generatedFiles(template, subject) {
48
+ const files = new Map();
49
+ files.set("witnora.config.json", `${JSON.stringify(starterProfile(template, subject), null, 2)}\n`);
50
+ if (template === "browser") {
51
+ files.set("tripwire.yml", starterTripwireConfig(subject));
52
+ files.set(".github/workflows/witnora-tripwire.yml", starterGitHubActionWorkflow(subject));
53
+ }
54
+ else if (template === "coding" || template === "workflow" || template === "data") {
55
+ files.set("witnora.adapter.mjs", starterAdapter(template, subject));
56
+ files.set(".github/workflows/witnora-observation.yml", adapterWorkflow());
57
+ }
58
+ else {
59
+ files.set(".github/workflows/witnora-assurance.yml", mcpWorkflow(subject));
60
+ }
61
+ return files;
62
+ }
63
+ function createSetupPullRequest(repository, plan, files, execute, configureCi) {
64
+ const branch = `witnora/setup-${plan.id.slice(0, 8)}`;
65
+ run(execute, "git", ["switch", "-c", branch], repository);
66
+ run(execute, "git", ["add", "--", ...files], repository);
67
+ run(execute, "git", ["commit", "-m", "chore(witnora): add assurance integration"], repository);
68
+ run(execute, "git", ["push", "-u", "origin", branch], repository);
69
+ if (configureCi) {
70
+ const projectId = requiredBrandedEnvironment("PROJECT_ID");
71
+ const apiKey = requiredBrandedEnvironment("API_KEY");
72
+ run(execute, "gh", ["variable", "set", "WITNORA_PROJECT_ID", "--body", projectId], repository);
73
+ run(execute, "gh", ["secret", "set", "WITNORA_API_KEY"], repository, apiKey);
74
+ }
75
+ const result = run(execute, "gh", ["pr", "create", "--title", "chore: add Witnora assurance integration", "--body", `Generated from bounded setup plan ${plan.id}. No credentials are committed. Review the workflow and configure WITNORA_API_KEY as a repository secret before merging.`], repository);
76
+ return result.stdout?.trim().split(/\r?\n/).find((line) => /^https:\/\/github\.com\//.test(line));
77
+ }
78
+ function run(execute, command, args, cwd, input) {
79
+ const result = execute(command, args, cwd, input);
80
+ if (result.status !== 0)
81
+ throw new Error(`${command} ${args[0] ?? ""} failed: ${result.stderr?.trim() || "unknown error"}`);
82
+ return result;
83
+ }
84
+ function defaultExecute(command, args, cwd, input) { const result = spawnSync(command, args, { cwd, encoding: "utf8", shell: false, input }); return { status: result.status, stdout: result.stdout, stderr: result.stderr }; }
85
+ function adapterWorkflow() {
86
+ return `name: Witnora Observation\n\non:\n pull_request:\n workflow_dispatch:\n\npermissions:\n contents: read\n\njobs:\n observe:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v7\n - uses: actions/setup-node@v6\n with:\n node-version: "20"\n - name: Record one bounded agent event\n run: node witnora.adapter.mjs\n env:\n WITNORA_BASE_URL: https://witnora.com\n WITNORA_PROJECT_ID: \${{ vars.WITNORA_PROJECT_ID }}\n WITNORA_API_KEY: \${{ secrets.WITNORA_API_KEY }}\n\n# This workflow proves event ingestion only. It does not establish CURRENT assurance.\n# Add a deterministic task assertion and evidence generation before using it as a release gate.\n`;
87
+ }
88
+ function mcpWorkflow(subject) {
89
+ return `name: Witnora MCP Assurance\n\non:\n workflow_dispatch:\n schedule:\n - cron: "17 3 * * *"\n\npermissions:\n contents: read\n\njobs:\n assurance:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v7\n - uses: actions/setup-node@v6\n with:\n node-version: "20"\n - name: Require an MCPBench result\n shell: bash\n run: test -f .mcpbench/latest/results.json || { echo "Run MCPBench before Witnora; expected .mcpbench/latest/results.json"; exit 2; }\n - run: npx --package agentcert@latest witnora run --mcpbench .mcpbench/latest/results.json --subject ${JSON.stringify(subject)} --push\n env:\n WITNORA_BASE_URL: https://witnora.com\n WITNORA_PROJECT_ID: \${{ vars.WITNORA_PROJECT_ID }}\n WITNORA_API_KEY: \${{ secrets.WITNORA_API_KEY }}\n`;
90
+ }
91
+ async function exists(path) { try {
92
+ await access(path);
93
+ return true;
94
+ }
95
+ catch {
96
+ return false;
97
+ } }
98
+ function requiredBrandedEnvironment(suffix) { const name = `WITNORA_${suffix}`; const value = (process.env[name] ?? process.env[`AGENTCERT_${suffix}`])?.trim(); if (!value)
99
+ throw new Error(`${name} is required with --configure-ci. Legacy AGENTCERT_* names remain supported.`); return value; }
100
+ function isInside(root, candidate) { const path = relative(root, candidate); return path === "" || (!path.startsWith("..") && !/^([/\\]|[A-Za-z]:)/.test(path)); }
101
+ function slug(value) { return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 60) || "my-agent"; }
package/dist/index.js ADDED
@@ -0,0 +1,16 @@
1
+ export * from "./bundle.js";
2
+ export * from "./corpus.js";
3
+ export * from "./corpus-store.js";
4
+ export * from "./control-plane.js";
5
+ export * from "./sandbox.js";
6
+ export * from "./conformance.js";
7
+ export * from "./credentials.js";
8
+ export * from "./failure-review.js";
9
+ export * from "./evidence-signing.js";
10
+ export * from "./local-server.js";
11
+ export * from "./monitor.js";
12
+ export * from "./normalizers.js";
13
+ export * from "./report.js";
14
+ export * from "./release-gate.js";
15
+ export * from "./schema-validator.js";
16
+ export * from "./types.js";