witnora 0.10.0 → 0.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -9
- package/dist/assurance-contract.js +312 -0
- package/dist/assurance-loop-demo.js +245 -0
- package/dist/authorization-state-machine.js +35 -0
- package/dist/authorization-v02.js +60 -0
- package/dist/canonical-v02.js +197 -0
- package/dist/canonical.js +31 -0
- package/dist/cli.js +118 -0
- package/dist/command-help.js +76 -0
- package/dist/control-plane.js +3 -3
- package/dist/credentials.js +1 -1
- package/dist/design-partner-pilot.js +402 -0
- package/dist/design-partner-v02.js +449 -0
- package/dist/evidence-v02.js +118 -0
- package/dist/generic-eval.js +160 -0
- package/dist/github-design-partner-v02.js +399 -0
- package/dist/github-live-v02.js +45 -0
- package/dist/guided-setup.js +1 -1
- package/dist/index.js +1 -0
- package/dist/offline-verifier.js +375 -0
- package/dist/onboard.js +132 -0
- package/dist/onboarding-templates.js +3 -3
- package/dist/outcome-evaluator.js +51 -0
- package/dist/privacy-manifest.js +273 -0
- package/dist/probe-protocol-v02.js +101 -0
- package/dist/sandbox.js +1 -1
- package/dist/trust-v02.js +111 -0
- package/dist/try.js +1 -1
- package/dist/vendor/design-partner/failure-exercises.mjs +353 -0
- package/dist/vendor/onegent-runtime/browser-enforcement-runtime.d.ts +8 -0
- package/dist/vendor/onegent-runtime/browser-enforcement-runtime.d.ts.map +1 -1
- package/dist/vendor/onegent-runtime/browser-enforcement-runtime.js +43 -1
- package/dist/vendor/witnora-probe/canonical.d.ts +3 -0
- package/dist/vendor/witnora-probe/canonical.js +39 -0
- package/dist/vendor/witnora-probe/cli.d.ts +2 -0
- package/dist/vendor/witnora-probe/cli.js +36 -0
- package/dist/vendor/witnora-probe/config.d.ts +2 -0
- package/dist/vendor/witnora-probe/config.js +63 -0
- package/dist/vendor/witnora-probe/credential-resolver.d.ts +6 -0
- package/dist/vendor/witnora-probe/credential-resolver.js +17 -0
- package/dist/vendor/witnora-probe/crypto.d.ts +2 -0
- package/dist/vendor/witnora-probe/crypto.js +14 -0
- package/dist/vendor/witnora-probe/emulator.d.ts +51 -0
- package/dist/vendor/witnora-probe/emulator.js +164 -0
- package/dist/vendor/witnora-probe/index.d.ts +11 -0
- package/dist/vendor/witnora-probe/index.js +11 -0
- package/dist/vendor/witnora-probe/logger.d.ts +3 -0
- package/dist/vendor/witnora-probe/logger.js +26 -0
- package/dist/vendor/witnora-probe/metrics.d.ts +12 -0
- package/dist/vendor/witnora-probe/metrics.js +31 -0
- package/dist/vendor/witnora-probe/probe.d.ts +22 -0
- package/dist/vendor/witnora-probe/probe.js +208 -0
- package/dist/vendor/witnora-probe/request-verifier.d.ts +9 -0
- package/dist/vendor/witnora-probe/request-verifier.js +84 -0
- package/dist/vendor/witnora-probe/server.d.ts +9 -0
- package/dist/vendor/witnora-probe/server.js +85 -0
- package/dist/vendor/witnora-probe/storage.d.ts +16 -0
- package/dist/vendor/witnora-probe/storage.js +114 -0
- package/dist/vendor/witnora-probe/types.d.ts +126 -0
- package/dist/vendor/witnora-probe/types.js +2 -0
- package/dist/vendor/witnora-verifier-python/README.md +43 -0
- package/dist/vendor/witnora-verifier-python/pyproject.toml +23 -0
- package/dist/vendor/witnora-verifier-python/src/witnora_verifier/__init__.py +33 -0
- package/dist/vendor/witnora-verifier-python/src/witnora_verifier/canonical.py +218 -0
- package/dist/vendor/witnora-verifier-python/src/witnora_verifier/cli.py +75 -0
- package/dist/vendor/witnora-verifier-python/src/witnora_verifier/conformance.py +124 -0
- package/dist/vendor/witnora-verifier-python/src/witnora_verifier/crypto.py +139 -0
- package/dist/vendor/witnora-verifier-python/src/witnora_verifier/errors.py +15 -0
- package/dist/vendor/witnora-verifier-python/src/witnora_verifier/event_chain.py +80 -0
- package/dist/vendor/witnora-verifier-python/src/witnora_verifier/evidence.py +336 -0
- package/dist/vendor/witnora-verifier-python/src/witnora_verifier/probe.py +188 -0
- package/dist/vendor/witnora-verifier-python/src/witnora_verifier/producer.py +127 -0
- package/dist/vendor/witnora-verifier-python/src/witnora_verifier/trust.py +337 -0
- package/package.json +4 -4
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export function decideCoveredDispatch(grant, request) {
|
|
2
|
+
if (!grant)
|
|
3
|
+
return denied("GRANT_MISSING");
|
|
4
|
+
if (grant.status === "REVOKED")
|
|
5
|
+
return denied("GRANT_REVOKED");
|
|
6
|
+
if (grant.status === "CONSUMED")
|
|
7
|
+
return denied("GRANT_ALREADY_CONSUMED");
|
|
8
|
+
if (request.incidentSuspended)
|
|
9
|
+
return denied("INCIDENT_SUSPENDED");
|
|
10
|
+
const now = Date.parse(request.now);
|
|
11
|
+
const notBefore = Date.parse(grant.notBefore);
|
|
12
|
+
const expiresAt = Date.parse(grant.expiresAt);
|
|
13
|
+
if (![now, notBefore, expiresAt].every(Number.isFinite) || notBefore >= expiresAt)
|
|
14
|
+
return denied("INVALID_TIME");
|
|
15
|
+
if (now < notBefore)
|
|
16
|
+
return denied("GRANT_NOT_YET_VALID");
|
|
17
|
+
if (now >= expiresAt)
|
|
18
|
+
return denied("GRANT_EXPIRED");
|
|
19
|
+
if (request.tenantRef !== grant.tenantRef)
|
|
20
|
+
return denied("TENANT_MISMATCH");
|
|
21
|
+
if (request.agentRef !== grant.agentRef)
|
|
22
|
+
return denied("AGENT_MISMATCH");
|
|
23
|
+
if (request.environment !== grant.environment)
|
|
24
|
+
return denied("ENVIRONMENT_MISMATCH");
|
|
25
|
+
if (request.policyRef !== grant.policyRef)
|
|
26
|
+
return denied("POLICY_VERSION_MISMATCH");
|
|
27
|
+
if (request.contractDigest !== grant.contractDigest)
|
|
28
|
+
return denied("CONTRACT_VERSION_MISMATCH");
|
|
29
|
+
if (request.actionDigest !== grant.actionDigest)
|
|
30
|
+
return denied("ACTION_DIGEST_MISMATCH");
|
|
31
|
+
return { allowed: true };
|
|
32
|
+
}
|
|
33
|
+
function denied(code) {
|
|
34
|
+
return { allowed: false, code };
|
|
35
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { createPrivateKey, createPublicKey, randomBytes, randomUUID, sign, verify } from "node:crypto";
|
|
2
|
+
import { canonicalBytesV02, sha256CanonicalV02 } from "./canonical-v02.js";
|
|
3
|
+
import { assertNotRevoked, verifyKeyCertificate } from "./trust-v02.js";
|
|
4
|
+
export function signApprovalV02(input, privateKeyPem) {
|
|
5
|
+
const unsigned = { ...input, approvalId: input.approvalId ?? randomUUID(), protocolVersion: "witnora.signed_action_approval.v0.2" };
|
|
6
|
+
return { ...unsigned, signature: sign(null, canonicalBytesV02(unsigned), createPrivateKey(privateKeyPem)).toString("base64") };
|
|
7
|
+
}
|
|
8
|
+
export function signExecutionGrantV02(input, privateKeyPem) {
|
|
9
|
+
const unsigned = { ...input, grantId: input.grantId ?? randomUUID(), nonce: input.nonce ?? randomBytes(24).toString("hex"), protocolVersion: "witnora.signed_execution_grant.v0.2" };
|
|
10
|
+
return { ...unsigned, signature: sign(null, canonicalBytesV02(unsigned), createPrivateKey(privateKeyPem)).toString("base64") };
|
|
11
|
+
}
|
|
12
|
+
export function approvalDigestV02(approval) { return sha256CanonicalV02(approval); }
|
|
13
|
+
export function verifyAuthorizationV02(input) {
|
|
14
|
+
const { approval, grant, trust, expected } = input;
|
|
15
|
+
const at = input.at ?? new Date();
|
|
16
|
+
if (approval.protocolVersion !== "witnora.signed_action_approval.v0.2" || grant.protocolVersion !== "witnora.signed_execution_grant.v0.2")
|
|
17
|
+
throw new Error("Unsupported signed authorization protocol.");
|
|
18
|
+
verifyKeyCertificate(trust.approvalCertificate, trust.root, new Date(approval.approvedAt), { role: "reviewer", tenantRef: expected.tenantRef, projectRef: expected.projectRef, environment: expected.environment });
|
|
19
|
+
verifyKeyCertificate(trust.grantCertificate, trust.root, new Date(grant.issuedAt), { role: "resource_scope_evaluator", tenantRef: expected.tenantRef, projectRef: expected.projectRef, environment: expected.environment });
|
|
20
|
+
assertNotRevoked(trust.approvalCertificate, trust.revocationJournal, new Date(approval.approvedAt));
|
|
21
|
+
assertNotRevoked(trust.grantCertificate, trust.revocationJournal, new Date(grant.issuedAt));
|
|
22
|
+
verifySignedApproval(approval, trust.approvalCertificate);
|
|
23
|
+
verifySignedGrant(grant, trust.grantCertificate);
|
|
24
|
+
for (const value of [approval, grant]) {
|
|
25
|
+
if (value.tenantRef !== expected.tenantRef || value.projectRef !== expected.projectRef || value.environment !== expected.environment || value.actionDigest !== expected.actionDigest)
|
|
26
|
+
throw new Error("Signed authorization scope or exact-action binding mismatch.");
|
|
27
|
+
}
|
|
28
|
+
if (grant.approvalDigest !== approvalDigestV02(approval))
|
|
29
|
+
throw new Error("Execution Grant is not bound to the signed approval.");
|
|
30
|
+
if (grant.maxUses !== 1)
|
|
31
|
+
throw new Error("Execution Grant must be single-use.");
|
|
32
|
+
const now = at.getTime();
|
|
33
|
+
const notBefore = parseInstant(grant.notBefore, "grant notBefore");
|
|
34
|
+
const grantExpiry = parseInstant(grant.expiresAt, "grant expiresAt");
|
|
35
|
+
const approvalExpiry = parseInstant(approval.expiresAt, "approval expiresAt");
|
|
36
|
+
if (now < notBefore || now > grantExpiry || now > approvalExpiry)
|
|
37
|
+
throw new Error("Signed authorization is not currently valid.");
|
|
38
|
+
}
|
|
39
|
+
function verifySignedApproval(value, certificate) {
|
|
40
|
+
if (value.signerKeyId !== certificate.subjectKeyId)
|
|
41
|
+
throw new Error("Signed approval key ID does not match its certificate.");
|
|
42
|
+
const { signature, ...unsigned } = value;
|
|
43
|
+
if (!verify(null, canonicalBytesV02(unsigned), createPublicKey(certificate.publicKeyPem), Buffer.from(signature, "base64")))
|
|
44
|
+
throw new Error("Signed approval signature is invalid.");
|
|
45
|
+
}
|
|
46
|
+
function verifySignedGrant(value, certificate) {
|
|
47
|
+
if (value.issuerKeyId !== certificate.subjectKeyId)
|
|
48
|
+
throw new Error("Signed execution Grant key ID does not match its certificate.");
|
|
49
|
+
const { signature, ...unsigned } = value;
|
|
50
|
+
if (!verify(null, canonicalBytesV02(unsigned), createPublicKey(certificate.publicKeyPem), Buffer.from(signature, "base64")))
|
|
51
|
+
throw new Error("Signed execution Grant signature is invalid.");
|
|
52
|
+
}
|
|
53
|
+
function parseInstant(value, field) {
|
|
54
|
+
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/.test(value))
|
|
55
|
+
throw new Error(`${field} must be an RFC 3339 UTC timestamp with at most millisecond precision.`);
|
|
56
|
+
const parsed = Date.parse(value);
|
|
57
|
+
if (!Number.isFinite(parsed))
|
|
58
|
+
throw new Error(`${field} is invalid.`);
|
|
59
|
+
return parsed;
|
|
60
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
export const WITNORA_CANONICALIZATION_V02 = "witnora.canonical_json.v0.2";
|
|
3
|
+
const MAX_DEPTH = 64;
|
|
4
|
+
/** Strict v0.2 profile: JSON only, safe integers only, no duplicate keys or negative zero. */
|
|
5
|
+
export function canonicalJsonV02(value) {
|
|
6
|
+
return JSON.stringify(canonicalValueV02(value, "$", 0));
|
|
7
|
+
}
|
|
8
|
+
export function canonicalBytesV02(value) {
|
|
9
|
+
return Buffer.from(canonicalJsonV02(value), "utf8");
|
|
10
|
+
}
|
|
11
|
+
export function sha256CanonicalV02(value) {
|
|
12
|
+
return createHash("sha256").update(canonicalBytesV02(value)).digest("hex");
|
|
13
|
+
}
|
|
14
|
+
export function parseStrictJsonV02(input) {
|
|
15
|
+
const parser = new StrictJsonParser(input);
|
|
16
|
+
const value = parser.parseValue(0);
|
|
17
|
+
parser.skipWhitespace();
|
|
18
|
+
if (!parser.done())
|
|
19
|
+
throw new Error(`Unexpected token at offset ${parser.offset()}.`);
|
|
20
|
+
return canonicalValueV02(value, "$", 0);
|
|
21
|
+
}
|
|
22
|
+
function canonicalValueV02(value, path, depth) {
|
|
23
|
+
if (depth > MAX_DEPTH)
|
|
24
|
+
throw new Error(`${path} exceeds the v0.2 nesting limit of ${MAX_DEPTH}.`);
|
|
25
|
+
if (value === null || typeof value === "boolean")
|
|
26
|
+
return value;
|
|
27
|
+
if (typeof value === "string") {
|
|
28
|
+
assertValidUnicode(value, path);
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
if (typeof value === "number") {
|
|
32
|
+
if (!Number.isFinite(value))
|
|
33
|
+
throw new Error(`${path} contains a non-finite number.`);
|
|
34
|
+
if (Object.is(value, -0))
|
|
35
|
+
throw new Error(`${path} contains negative zero.`);
|
|
36
|
+
if (!Number.isSafeInteger(value))
|
|
37
|
+
throw new Error(`${path} must be a safe integer; encode decimals as normalized strings.`);
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
if (Array.isArray(value))
|
|
41
|
+
return value.map((entry, index) => canonicalValueV02(entry, `${path}[${index}]`, depth + 1));
|
|
42
|
+
if (value && typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype) {
|
|
43
|
+
const source = value;
|
|
44
|
+
const output = {};
|
|
45
|
+
for (const key of Object.keys(source).sort()) {
|
|
46
|
+
assertValidUnicode(key, `${path} key`);
|
|
47
|
+
if (source[key] === undefined)
|
|
48
|
+
throw new Error(`${path}.${key} is undefined.`);
|
|
49
|
+
output[key] = canonicalValueV02(source[key], `${path}.${key}`, depth + 1);
|
|
50
|
+
}
|
|
51
|
+
return output;
|
|
52
|
+
}
|
|
53
|
+
throw new Error(`${path} contains unsupported ${typeof value}.`);
|
|
54
|
+
}
|
|
55
|
+
class StrictJsonParser {
|
|
56
|
+
source;
|
|
57
|
+
index = 0;
|
|
58
|
+
constructor(source) {
|
|
59
|
+
this.source = source;
|
|
60
|
+
}
|
|
61
|
+
offset() { return this.index; }
|
|
62
|
+
done() { return this.index >= this.source.length; }
|
|
63
|
+
skipWhitespace() { while (/\s/u.test(this.source[this.index] ?? ""))
|
|
64
|
+
this.index += 1; }
|
|
65
|
+
parseValue(depth) {
|
|
66
|
+
if (depth > MAX_DEPTH)
|
|
67
|
+
throw new Error(`JSON exceeds the v0.2 nesting limit of ${MAX_DEPTH}.`);
|
|
68
|
+
this.skipWhitespace();
|
|
69
|
+
const token = this.source[this.index];
|
|
70
|
+
if (token === "{")
|
|
71
|
+
return this.parseObject(depth + 1);
|
|
72
|
+
if (token === "[")
|
|
73
|
+
return this.parseArray(depth + 1);
|
|
74
|
+
if (token === '"')
|
|
75
|
+
return this.parseString();
|
|
76
|
+
if (token === "t" && this.consume("true"))
|
|
77
|
+
return true;
|
|
78
|
+
if (token === "f" && this.consume("false"))
|
|
79
|
+
return false;
|
|
80
|
+
if (token === "n" && this.consume("null"))
|
|
81
|
+
return null;
|
|
82
|
+
if (token === "-" || /[0-9]/u.test(token ?? ""))
|
|
83
|
+
return this.parseNumber();
|
|
84
|
+
throw new Error(`Invalid JSON value at offset ${this.index}.`);
|
|
85
|
+
}
|
|
86
|
+
parseObject(depth) {
|
|
87
|
+
this.expect("{");
|
|
88
|
+
this.skipWhitespace();
|
|
89
|
+
const output = {};
|
|
90
|
+
const seen = new Set();
|
|
91
|
+
if (this.peek("}")) {
|
|
92
|
+
this.index += 1;
|
|
93
|
+
return output;
|
|
94
|
+
}
|
|
95
|
+
while (true) {
|
|
96
|
+
this.skipWhitespace();
|
|
97
|
+
const key = this.parseString();
|
|
98
|
+
if (seen.has(key))
|
|
99
|
+
throw new Error(`Duplicate JSON object key ${JSON.stringify(key)}.`);
|
|
100
|
+
seen.add(key);
|
|
101
|
+
this.skipWhitespace();
|
|
102
|
+
this.expect(":");
|
|
103
|
+
output[key] = this.parseValue(depth);
|
|
104
|
+
this.skipWhitespace();
|
|
105
|
+
if (this.peek("}")) {
|
|
106
|
+
this.index += 1;
|
|
107
|
+
return output;
|
|
108
|
+
}
|
|
109
|
+
this.expect(",");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
parseArray(depth) {
|
|
113
|
+
this.expect("[");
|
|
114
|
+
this.skipWhitespace();
|
|
115
|
+
const output = [];
|
|
116
|
+
if (this.peek("]")) {
|
|
117
|
+
this.index += 1;
|
|
118
|
+
return output;
|
|
119
|
+
}
|
|
120
|
+
while (true) {
|
|
121
|
+
output.push(this.parseValue(depth));
|
|
122
|
+
this.skipWhitespace();
|
|
123
|
+
if (this.peek("]")) {
|
|
124
|
+
this.index += 1;
|
|
125
|
+
return output;
|
|
126
|
+
}
|
|
127
|
+
this.expect(",");
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
parseString() {
|
|
131
|
+
const start = this.index;
|
|
132
|
+
this.expect('"');
|
|
133
|
+
let escaped = false;
|
|
134
|
+
while (!this.done()) {
|
|
135
|
+
const char = this.source[this.index++];
|
|
136
|
+
if (!escaped && char === '"') {
|
|
137
|
+
const token = this.source.slice(start, this.index);
|
|
138
|
+
try {
|
|
139
|
+
const value = JSON.parse(token);
|
|
140
|
+
assertValidUnicode(value, `JSON string at offset ${start}`);
|
|
141
|
+
return value;
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
throw new Error(`Invalid JSON string at offset ${start}.`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (!escaped && char === "\\")
|
|
148
|
+
escaped = true;
|
|
149
|
+
else
|
|
150
|
+
escaped = false;
|
|
151
|
+
if ((char?.charCodeAt(0) ?? 32) < 32)
|
|
152
|
+
throw new Error(`Unescaped control character at offset ${this.index - 1}.`);
|
|
153
|
+
}
|
|
154
|
+
throw new Error(`Unterminated JSON string at offset ${start}.`);
|
|
155
|
+
}
|
|
156
|
+
parseNumber() {
|
|
157
|
+
const rest = this.source.slice(this.index);
|
|
158
|
+
const match = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/u.exec(rest);
|
|
159
|
+
if (!match)
|
|
160
|
+
throw new Error(`Invalid JSON number at offset ${this.index}.`);
|
|
161
|
+
this.index += match[0].length;
|
|
162
|
+
const value = Number(match[0]);
|
|
163
|
+
if (!Number.isFinite(value))
|
|
164
|
+
throw new Error("Non-finite JSON number is not supported.");
|
|
165
|
+
if (Object.is(value, -0))
|
|
166
|
+
throw new Error("Negative zero is not supported.");
|
|
167
|
+
if (!Number.isSafeInteger(value))
|
|
168
|
+
throw new Error("Only safe integer JSON numbers are supported; use normalized decimal strings.");
|
|
169
|
+
return value;
|
|
170
|
+
}
|
|
171
|
+
consume(value) {
|
|
172
|
+
if (!this.source.startsWith(value, this.index))
|
|
173
|
+
return false;
|
|
174
|
+
this.index += value.length;
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
peek(value) { return this.source[this.index] === value; }
|
|
178
|
+
expect(value) {
|
|
179
|
+
if (!this.peek(value))
|
|
180
|
+
throw new Error(`Expected ${JSON.stringify(value)} at offset ${this.index}.`);
|
|
181
|
+
this.index += 1;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function assertValidUnicode(value, path) {
|
|
185
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
186
|
+
const code = value.charCodeAt(index);
|
|
187
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
188
|
+
const next = value.charCodeAt(index + 1);
|
|
189
|
+
if (!(next >= 0xdc00 && next <= 0xdfff))
|
|
190
|
+
throw new Error(`${path} contains an unpaired Unicode surrogate.`);
|
|
191
|
+
index += 1;
|
|
192
|
+
}
|
|
193
|
+
else if (code >= 0xdc00 && code <= 0xdfff) {
|
|
194
|
+
throw new Error(`${path} contains an unpaired Unicode surrogate.`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
export const WITNORA_CANONICALIZATION_VERSION = "witnora.canonical_json.v0.1";
|
|
3
|
+
/** Stable JSON used by v0.1 digests. Arrays remain ordered; undefined is rejected. */
|
|
4
|
+
export function canonicalJson(value) {
|
|
5
|
+
return JSON.stringify(canonicalValue(value, "$"));
|
|
6
|
+
}
|
|
7
|
+
export function sha256Canonical(value) {
|
|
8
|
+
return createHash("sha256").update(canonicalJson(value), "utf8").digest("hex");
|
|
9
|
+
}
|
|
10
|
+
function canonicalValue(value, path) {
|
|
11
|
+
if (value === null || typeof value === "string" || typeof value === "boolean")
|
|
12
|
+
return value;
|
|
13
|
+
if (typeof value === "number") {
|
|
14
|
+
if (!Number.isFinite(value))
|
|
15
|
+
throw new Error(`${path} contains a non-finite number.`);
|
|
16
|
+
return Object.is(value, -0) ? 0 : value;
|
|
17
|
+
}
|
|
18
|
+
if (Array.isArray(value))
|
|
19
|
+
return value.map((item, index) => canonicalValue(item, `${path}[${index}]`));
|
|
20
|
+
if (value && typeof value === "object") {
|
|
21
|
+
const source = value;
|
|
22
|
+
const output = {};
|
|
23
|
+
for (const key of Object.keys(source).sort()) {
|
|
24
|
+
if (source[key] === undefined)
|
|
25
|
+
throw new Error(`${path}.${key} is undefined; canonical input must be explicit.`);
|
|
26
|
+
output[key] = canonicalValue(source[key], `${path}.${key}`);
|
|
27
|
+
}
|
|
28
|
+
return output;
|
|
29
|
+
}
|
|
30
|
+
throw new Error(`${path} contains unsupported ${typeof value}.`);
|
|
31
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -29,6 +29,14 @@ import { parseAgentTemplate, starterAdapter, starterGitHubActionWorkflow, starte
|
|
|
29
29
|
import { renderTrySummary, writeTryEvidence } from "./try.js";
|
|
30
30
|
import { applyGuidedSetup } from "./guided-setup.js";
|
|
31
31
|
import { resolveRuntimeContext } from "./runtime-context.js";
|
|
32
|
+
import { compileAssuranceContract, diffAssuranceContracts, exampleAssuranceContract, readAssuranceContract, renderContractExplanation, } from "./assurance-contract.js";
|
|
33
|
+
import { examplePrivacyManifest, inspectPrivacy } from "./privacy-manifest.js";
|
|
34
|
+
import { renderOfflineVerification, verifyEvidencePacket } from "./offline-verifier.js";
|
|
35
|
+
import { runAssuranceLoopDemo } from "./assurance-loop-demo.js";
|
|
36
|
+
import { importGenericEval, renderGenericEvalReport } from "./generic-eval.js";
|
|
37
|
+
import { runDesignPartnerCommand } from "./design-partner-v02.js";
|
|
38
|
+
import { runOnboard } from "./onboard.js";
|
|
39
|
+
import { verifyEvidencePacketV02 } from "./evidence-v02.js";
|
|
32
40
|
process.on("uncaughtException", reportFatalError);
|
|
33
41
|
process.on("unhandledRejection", reportFatalError);
|
|
34
42
|
const command = process.argv[2] ?? "help";
|
|
@@ -38,6 +46,115 @@ const commandHelp = process.argv.some((argument) => argument === "--help" || arg
|
|
|
38
46
|
if (commandHelp) {
|
|
39
47
|
process.stdout.write(commandHelp);
|
|
40
48
|
}
|
|
49
|
+
else if (command === "contract") {
|
|
50
|
+
const action = process.argv[3] ?? "help";
|
|
51
|
+
if (action === "init") {
|
|
52
|
+
const out = resolve(readFlag("--out") ?? "witnora.assurance-contract.json");
|
|
53
|
+
await writeStarterFile(out, `${JSON.stringify(exampleAssuranceContract(), null, 2)}\n`, readBoolFlag("--force"));
|
|
54
|
+
process.stdout.write(`Wrote ${out}\n`);
|
|
55
|
+
}
|
|
56
|
+
else if (action === "validate" || action === "explain") {
|
|
57
|
+
const path = readFlag("--contract") ?? process.argv[4];
|
|
58
|
+
if (!path)
|
|
59
|
+
throw new Error("A contract path is required.");
|
|
60
|
+
process.stdout.write(renderContractExplanation(await readAssuranceContract(path)));
|
|
61
|
+
}
|
|
62
|
+
else if (action === "compile") {
|
|
63
|
+
const path = readFlag("--contract") ?? process.argv[4];
|
|
64
|
+
if (!path)
|
|
65
|
+
throw new Error("A contract path is required.");
|
|
66
|
+
const out = resolve(readFlag("--out") ?? ".witnora/contract/compiled-contract.json");
|
|
67
|
+
await mkdir(dirname(out), { recursive: true });
|
|
68
|
+
await writeFile(out, `${JSON.stringify(compileAssuranceContract(await readAssuranceContract(path)), null, 2)}\n`);
|
|
69
|
+
process.stdout.write(`Wrote deterministic compiler output ${out}\n`);
|
|
70
|
+
}
|
|
71
|
+
else if (action === "diff") {
|
|
72
|
+
const previous = readFlag("--from");
|
|
73
|
+
const next = readFlag("--to");
|
|
74
|
+
if (!previous || !next)
|
|
75
|
+
throw new Error("Contract diff requires --from <contract> and --to <contract>.");
|
|
76
|
+
process.stdout.write(`${JSON.stringify(diffAssuranceContracts(await readAssuranceContract(previous), await readAssuranceContract(next)), null, 2)}\n`);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
throw new Error("Use witnora contract init|validate|compile|diff|explain.");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
else if (command === "privacy") {
|
|
83
|
+
const action = process.argv[3] ?? "help";
|
|
84
|
+
if (action === "init") {
|
|
85
|
+
const out = resolve(readFlag("--out") ?? "witnora.privacy-manifest.json");
|
|
86
|
+
await writeStarterFile(out, `${JSON.stringify(examplePrivacyManifest(), null, 2)}\n`, readBoolFlag("--force"));
|
|
87
|
+
process.stdout.write(`Wrote ${out}\n`);
|
|
88
|
+
}
|
|
89
|
+
else if (action === "inspect") {
|
|
90
|
+
const path = readFlag("--manifest") ?? process.argv[4];
|
|
91
|
+
if (!path)
|
|
92
|
+
throw new Error("A Privacy Manifest path is required.");
|
|
93
|
+
process.stdout.write(inspectPrivacy(await readJson(path)));
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
throw new Error("Use witnora privacy init|inspect.");
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
else if (command === "design-partner") {
|
|
100
|
+
await runDesignPartnerCommand(process.argv.slice(3));
|
|
101
|
+
}
|
|
102
|
+
else if (command === "verify") {
|
|
103
|
+
const path = readFlag("--offline") ?? readFlag("--packet") ?? process.argv[3];
|
|
104
|
+
if (!path || path.startsWith("--"))
|
|
105
|
+
throw new Error("Use witnora verify --offline <packet.json>.");
|
|
106
|
+
const trustPath = readFlag("--trust");
|
|
107
|
+
const packet = await readJson(path);
|
|
108
|
+
if (packet.protocolVersion === "witnora.verifiable_evidence_packet.v0.2") {
|
|
109
|
+
const revocationPath = readFlag("--revocation-journal");
|
|
110
|
+
if (!trustPath || !revocationPath)
|
|
111
|
+
throw new Error("Evidence v0.2 requires --trust <pinned-root.json> and --revocation-journal <latest.json>.");
|
|
112
|
+
const report = verifyEvidencePacketV02(packet, await readJson(trustPath), new Date(), await readJson(revocationPath));
|
|
113
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
114
|
+
if (report.overall !== "PASS")
|
|
115
|
+
process.exitCode = 1;
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
const report = verifyEvidencePacket(packet, new Date(), trustPath ? await readJson(trustPath) : undefined);
|
|
119
|
+
process.stdout.write(readBoolFlag("--json") ? `${JSON.stringify(report, null, 2)}\n` : renderOfflineVerification(report));
|
|
120
|
+
if (report.overall !== "PASS")
|
|
121
|
+
process.exitCode = 1;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
else if (command === "demo") {
|
|
125
|
+
if (process.argv[3] !== "assurance-loop")
|
|
126
|
+
throw new Error("Use witnora demo assurance-loop.");
|
|
127
|
+
const attack = (readFlag("--attack") ?? "none");
|
|
128
|
+
if (!["none", "parameter-substitution", "replay", "false-success", "event-tamper"].includes(attack))
|
|
129
|
+
throw new Error("Unsupported --attack value.");
|
|
130
|
+
const outcome = await runAssuranceLoopDemo({ outDir: readFlag("--out") ?? ".witnora/assurance-loop", attack });
|
|
131
|
+
process.stdout.write(renderOfflineVerification(outcome.report));
|
|
132
|
+
process.stdout.write(`Evidence packet: ${outcome.packetPath}\nTrust bundle: ${outcome.trustBundlePath}\nVerification report: ${outcome.reportPath}\nRevalidation diff: ${outcome.revalidationPath}\n`);
|
|
133
|
+
if (outcome.report.overall !== "PASS")
|
|
134
|
+
process.exitCode = 1;
|
|
135
|
+
}
|
|
136
|
+
else if (command === "import") {
|
|
137
|
+
if (readFlag("--format") !== "generic-eval")
|
|
138
|
+
throw new Error("Use witnora import --format generic-eval <result.json>.");
|
|
139
|
+
const path = process.argv.find((argument, index) => index >= 3 && !argument.startsWith("--") && process.argv[index - 1] !== "--format" && process.argv[index - 1] !== "--out");
|
|
140
|
+
if (!path)
|
|
141
|
+
throw new Error("A generic eval result path is required.");
|
|
142
|
+
const report = await importGenericEval(path, readBoolFlag("--dry-run") ? undefined : readFlag("--out") ?? ".witnora/import/generic-eval-report.json");
|
|
143
|
+
process.stdout.write(readBoolFlag("--json") ? `${JSON.stringify(report, null, 2)}\n` : renderGenericEvalReport(report));
|
|
144
|
+
}
|
|
145
|
+
else if (command === "onboard") {
|
|
146
|
+
const projectId = readFlag("--project") ?? brandedEnvironment("PROJECT_ID");
|
|
147
|
+
if (!projectId)
|
|
148
|
+
throw new Error("--project <project-id> is required.");
|
|
149
|
+
await runOnboard({
|
|
150
|
+
projectId,
|
|
151
|
+
server: readFlag("--server") ?? brandedEnvironment("BASE_URL") ?? DEFAULT_WITNORA_SERVER,
|
|
152
|
+
name: readFlag("--name"),
|
|
153
|
+
repository: readFlag("--repo") ?? process.cwd(),
|
|
154
|
+
template: readFlag("--template") ? parseAgentTemplate(readFlag("--template")) : undefined,
|
|
155
|
+
openBrowser: readBoolFlag("--no-browser") ? async () => { } : undefined,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
41
158
|
else if (command === "try") {
|
|
42
159
|
const template = parseAgentTemplate(readFlag("--template") ?? "workflow");
|
|
43
160
|
const subject = readFlag("--subject") ?? `witnora-sample-${template}`;
|
|
@@ -599,6 +716,7 @@ else if (command === "conformance") {
|
|
|
599
716
|
}
|
|
600
717
|
else {
|
|
601
718
|
process.stdout.write(`Usage:
|
|
719
|
+
witnora onboard --project <project-id>
|
|
602
720
|
witnora try --template workflow [--push]
|
|
603
721
|
witnora init --subject my-browser-agent
|
|
604
722
|
witnora connect --server https://witnora.com --project <project-id>
|
package/dist/command-help.js
CHANGED
|
@@ -1,6 +1,82 @@
|
|
|
1
1
|
export function renderCommandHelp(command) {
|
|
2
2
|
if (command === "sandbox" || command === "browser-adapter")
|
|
3
3
|
return undefined;
|
|
4
|
+
if (command === "onboard")
|
|
5
|
+
return `Usage:
|
|
6
|
+
witnora onboard --project <project-id>
|
|
7
|
+
witnora onboard --project <project-id> --template <browser|coding|mcp|workflow|data>
|
|
8
|
+
|
|
9
|
+
Opens one browser authorization, saves a restricted project credential, detects the repository,
|
|
10
|
+
writes missing starter files, and records an isolated synthetic self-test receipt. The self-test
|
|
11
|
+
does not create assurance evidence or establish CURRENT status.
|
|
12
|
+
|
|
13
|
+
Options:
|
|
14
|
+
--server <url> Hosted server (default: https://witnora.com)
|
|
15
|
+
--project <id> Project to authorize (required)
|
|
16
|
+
--name <name> Saved connection name (default: repository name)
|
|
17
|
+
--repo <directory> Repository to configure (default: current directory)
|
|
18
|
+
--template <type> Override automatic repository detection
|
|
19
|
+
--no-browser Print the approval URL without opening it
|
|
20
|
+
`;
|
|
21
|
+
if (command === "design-partner")
|
|
22
|
+
return `Usage:
|
|
23
|
+
witnora design-partner init github-pr-merge [--out .witnora/design-partner]
|
|
24
|
+
witnora design-partner doctor [--dir .witnora/design-partner]
|
|
25
|
+
witnora design-partner privacy inspect
|
|
26
|
+
witnora design-partner trust inspect
|
|
27
|
+
witnora design-partner trust revoke-probe [--effective-at <timestamp>] [--retroactive]
|
|
28
|
+
witnora design-partner trust rotate-probe
|
|
29
|
+
witnora design-partner dry-run
|
|
30
|
+
witnora design-partner run-acceptance
|
|
31
|
+
witnora design-partner prepare-live-github --repository owner/sandbox-repo --pull-request 1
|
|
32
|
+
witnora design-partner verify-offline --packet <packet.json> --root <pinned-root.json> [--revocation-journal <latest.json>]
|
|
33
|
+
|
|
34
|
+
The v0.2 acceptance path is emulator-only. The optional live GitHub command is a
|
|
35
|
+
read-only preflight and cannot mutate or produce acceptance evidence for a repository.
|
|
36
|
+
`;
|
|
37
|
+
if (command === "contract")
|
|
38
|
+
return `Usage:
|
|
39
|
+
witnora contract init [--out witnora.assurance-contract.json]
|
|
40
|
+
witnora contract validate <contract.json>
|
|
41
|
+
witnora contract compile <contract.json> [--out compiled-contract.json]
|
|
42
|
+
witnora contract diff --from previous.json --to next.json
|
|
43
|
+
witnora contract explain <contract.json>
|
|
44
|
+
|
|
45
|
+
Contracts are strict JSON (also valid YAML 1.2). Unknown fields are rejected.
|
|
46
|
+
`;
|
|
47
|
+
if (command === "privacy")
|
|
48
|
+
return `Usage:
|
|
49
|
+
witnora privacy init [--out witnora.privacy-manifest.json]
|
|
50
|
+
witnora privacy inspect <manifest.json>
|
|
51
|
+
|
|
52
|
+
The manifest defaults to deny and classifies fields as local_only, disclose, or commit_only.
|
|
53
|
+
`;
|
|
54
|
+
if (command === "verify")
|
|
55
|
+
return `Usage:
|
|
56
|
+
witnora verify --offline <witnora-evidence-packet.json> --trust <trust-bundle.json>
|
|
57
|
+
witnora verify --offline <packet.json> --trust <trust-bundle.json> --json
|
|
58
|
+
|
|
59
|
+
Performs deterministic local verification without network access. Keys embedded in a packet
|
|
60
|
+
are never trust anchors; missing caller-supplied trust returns UNVERIFIABLE.
|
|
61
|
+
`;
|
|
62
|
+
if (command === "demo")
|
|
63
|
+
return `Usage:
|
|
64
|
+
witnora demo assurance-loop [--out .witnora/assurance-loop]
|
|
65
|
+
witnora demo assurance-loop --attack <parameter-substitution|replay|false-success|event-tamper>
|
|
66
|
+
|
|
67
|
+
Runs a generic local high-risk action through contract, policy, approval, single-use grant,
|
|
68
|
+
controlled execution, independent observation, signed evidence, and offline verification.
|
|
69
|
+
No external system or credential is used.
|
|
70
|
+
`;
|
|
71
|
+
if (command === "import")
|
|
72
|
+
return `Usage:
|
|
73
|
+
witnora import --format generic-eval <result.json> [--out .witnora/import/generic-eval-report.json]
|
|
74
|
+
witnora import --format generic-eval <result.json> --dry-run
|
|
75
|
+
|
|
76
|
+
Normalizes framework-independent evaluation results and reports apparent success,
|
|
77
|
+
independently verified success, Assurance Gap, explicit denominators, and exclusions.
|
|
78
|
+
Imported results are unreviewed and never establish CURRENT assurance.
|
|
79
|
+
`;
|
|
4
80
|
if (command === "setup")
|
|
5
81
|
return `Usage:
|
|
6
82
|
witnora setup apply --plan witnora-setup-plan.json --repo .
|
package/dist/control-plane.js
CHANGED
|
@@ -30,13 +30,13 @@ export async function verifyControlPlaneConnection(options) {
|
|
|
30
30
|
}
|
|
31
31
|
catch (error) {
|
|
32
32
|
if (error instanceof ControlPlaneRequestError && error.status === 401) {
|
|
33
|
-
throw new Error("
|
|
33
|
+
throw new Error("Witnora API key was rejected. Run `witnora onboard --project <project-id>` again or replace the revoked key.");
|
|
34
34
|
}
|
|
35
35
|
if (error instanceof ControlPlaneRequestError && error.status === 403) {
|
|
36
|
-
throw new Error(`
|
|
36
|
+
throw new Error(`Witnora API key cannot access project ${options.projectId}. Check the project ID and key scope.`);
|
|
37
37
|
}
|
|
38
38
|
if (error instanceof ControlPlaneRequestError && error.status === 404) {
|
|
39
|
-
throw new Error(`
|
|
39
|
+
throw new Error(`Witnora project ${options.projectId} was not found.`);
|
|
40
40
|
}
|
|
41
41
|
throw error;
|
|
42
42
|
}
|
package/dist/credentials.js
CHANGED
|
@@ -46,7 +46,7 @@ export async function resolveConnection(options = {}) {
|
|
|
46
46
|
};
|
|
47
47
|
const missing = Object.entries(candidate).filter(([, value]) => !value).map(([key]) => key);
|
|
48
48
|
if (missing.length > 0) {
|
|
49
|
-
throw new Error(`Hosted connection is incomplete (${missing.join(", ")} missing). Run \`witnora
|
|
49
|
+
throw new Error(`Hosted connection is incomplete (${missing.join(", ")} missing). Run \`witnora onboard --project <project-id>\` or set WITNORA_BASE_URL, WITNORA_PROJECT_ID, and WITNORA_API_KEY. Legacy AGENTCERT_* variables remain supported.`);
|
|
50
50
|
}
|
|
51
51
|
return validateConnection(candidate);
|
|
52
52
|
}
|