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.
Files changed (74) hide show
  1. package/README.md +54 -9
  2. package/dist/assurance-contract.js +312 -0
  3. package/dist/assurance-loop-demo.js +245 -0
  4. package/dist/authorization-state-machine.js +35 -0
  5. package/dist/authorization-v02.js +60 -0
  6. package/dist/canonical-v02.js +197 -0
  7. package/dist/canonical.js +31 -0
  8. package/dist/cli.js +118 -0
  9. package/dist/command-help.js +76 -0
  10. package/dist/control-plane.js +3 -3
  11. package/dist/credentials.js +1 -1
  12. package/dist/design-partner-pilot.js +402 -0
  13. package/dist/design-partner-v02.js +449 -0
  14. package/dist/evidence-v02.js +118 -0
  15. package/dist/generic-eval.js +160 -0
  16. package/dist/github-design-partner-v02.js +399 -0
  17. package/dist/github-live-v02.js +45 -0
  18. package/dist/guided-setup.js +1 -1
  19. package/dist/index.js +1 -0
  20. package/dist/offline-verifier.js +375 -0
  21. package/dist/onboard.js +132 -0
  22. package/dist/onboarding-templates.js +3 -3
  23. package/dist/outcome-evaluator.js +51 -0
  24. package/dist/privacy-manifest.js +273 -0
  25. package/dist/probe-protocol-v02.js +101 -0
  26. package/dist/sandbox.js +1 -1
  27. package/dist/trust-v02.js +111 -0
  28. package/dist/try.js +1 -1
  29. package/dist/vendor/design-partner/failure-exercises.mjs +353 -0
  30. package/dist/vendor/onegent-runtime/browser-enforcement-runtime.d.ts +8 -0
  31. package/dist/vendor/onegent-runtime/browser-enforcement-runtime.d.ts.map +1 -1
  32. package/dist/vendor/onegent-runtime/browser-enforcement-runtime.js +43 -1
  33. package/dist/vendor/witnora-probe/canonical.d.ts +3 -0
  34. package/dist/vendor/witnora-probe/canonical.js +39 -0
  35. package/dist/vendor/witnora-probe/cli.d.ts +2 -0
  36. package/dist/vendor/witnora-probe/cli.js +36 -0
  37. package/dist/vendor/witnora-probe/config.d.ts +2 -0
  38. package/dist/vendor/witnora-probe/config.js +63 -0
  39. package/dist/vendor/witnora-probe/credential-resolver.d.ts +6 -0
  40. package/dist/vendor/witnora-probe/credential-resolver.js +17 -0
  41. package/dist/vendor/witnora-probe/crypto.d.ts +2 -0
  42. package/dist/vendor/witnora-probe/crypto.js +14 -0
  43. package/dist/vendor/witnora-probe/emulator.d.ts +51 -0
  44. package/dist/vendor/witnora-probe/emulator.js +164 -0
  45. package/dist/vendor/witnora-probe/index.d.ts +11 -0
  46. package/dist/vendor/witnora-probe/index.js +11 -0
  47. package/dist/vendor/witnora-probe/logger.d.ts +3 -0
  48. package/dist/vendor/witnora-probe/logger.js +26 -0
  49. package/dist/vendor/witnora-probe/metrics.d.ts +12 -0
  50. package/dist/vendor/witnora-probe/metrics.js +31 -0
  51. package/dist/vendor/witnora-probe/probe.d.ts +22 -0
  52. package/dist/vendor/witnora-probe/probe.js +208 -0
  53. package/dist/vendor/witnora-probe/request-verifier.d.ts +9 -0
  54. package/dist/vendor/witnora-probe/request-verifier.js +84 -0
  55. package/dist/vendor/witnora-probe/server.d.ts +9 -0
  56. package/dist/vendor/witnora-probe/server.js +85 -0
  57. package/dist/vendor/witnora-probe/storage.d.ts +16 -0
  58. package/dist/vendor/witnora-probe/storage.js +114 -0
  59. package/dist/vendor/witnora-probe/types.d.ts +126 -0
  60. package/dist/vendor/witnora-probe/types.js +2 -0
  61. package/dist/vendor/witnora-verifier-python/README.md +43 -0
  62. package/dist/vendor/witnora-verifier-python/pyproject.toml +23 -0
  63. package/dist/vendor/witnora-verifier-python/src/witnora_verifier/__init__.py +33 -0
  64. package/dist/vendor/witnora-verifier-python/src/witnora_verifier/canonical.py +218 -0
  65. package/dist/vendor/witnora-verifier-python/src/witnora_verifier/cli.py +75 -0
  66. package/dist/vendor/witnora-verifier-python/src/witnora_verifier/conformance.py +124 -0
  67. package/dist/vendor/witnora-verifier-python/src/witnora_verifier/crypto.py +139 -0
  68. package/dist/vendor/witnora-verifier-python/src/witnora_verifier/errors.py +15 -0
  69. package/dist/vendor/witnora-verifier-python/src/witnora_verifier/event_chain.py +80 -0
  70. package/dist/vendor/witnora-verifier-python/src/witnora_verifier/evidence.py +336 -0
  71. package/dist/vendor/witnora-verifier-python/src/witnora_verifier/probe.py +188 -0
  72. package/dist/vendor/witnora-verifier-python/src/witnora_verifier/producer.py +127 -0
  73. package/dist/vendor/witnora-verifier-python/src/witnora_verifier/trust.py +337 -0
  74. package/package.json +4 -4
package/README.md CHANGED
@@ -9,17 +9,51 @@ outcome. It writes portable reports and accumulates a local failure corpus.
9
9
  [Public evidence](https://witnora.com/evidence) |
10
10
  [Get started](https://witnora.com/docs) |
11
11
  [Private workspace](https://witnora.com/app) |
12
- [GitHub source](https://github.com/Kakarottoooo/agentcert)
12
+ [GitHub source](https://github.com/Kakarottoooo/witnora)
13
13
 
14
- ## 5-minute local path
14
+ ## 5-minute hosted path
15
15
 
16
- No agent repository or API key is required for the first run:
16
+ Create a Hosted project, open a terminal in the agent repository, and run the
17
+ one command shown by Witnora:
17
18
 
18
19
  ```bash
19
- npx witnora@latest try --template workflow
20
+ npx witnora@latest onboard --project your-project-id
20
21
  ```
21
22
 
22
- Inspect `.witnora/try/agentcert-report.html` and
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.
28
+
29
+ Without an agent repository, run `npx witnora@latest try --template workflow`
30
+ for an offline-only sample.
31
+
32
+ To exercise the privacy-preserving high-risk action loop locally, including
33
+ contract compilation, exact-action approval, a single-use grant, controlled
34
+ execution, independent outcome observation, signed evidence, and offline
35
+ verification:
36
+
37
+ ```bash
38
+ npx witnora@latest contract init
39
+ npx witnora@latest contract compile witnora.assurance-contract.json
40
+ npx witnora@latest privacy init
41
+ npx witnora@latest demo assurance-loop
42
+ npx witnora@latest verify --offline .witnora/assurance-loop/witnora-evidence-packet.json \
43
+ --trust .witnora/assurance-loop/witnora-trust-bundle.json
44
+ ```
45
+
46
+ This reference path is deterministic and local. It uses no real integration or
47
+ credential. See the full [privacy-preserving action loop](https://github.com/Kakarottoooo/witnora/blob/main/docs/how-it-works/privacy-preserving-action-loop.md),
48
+ including attack demonstrations and explicit limitations.
49
+
50
+ Implementation references:
51
+
52
+ - [Assurance Contract v0.1](https://github.com/Kakarottoooo/witnora/blob/main/docs/specifications/witnora-assurance-contract-v0.1.md)
53
+ - [Two-hour design-partner quickstart](https://github.com/Kakarottoooo/witnora/blob/main/docs/guides/design-partner-two-hour-quickstart.md)
54
+ - [Threat model and trust boundaries](https://github.com/Kakarottoooo/witnora/blob/main/docs/security/privacy-verifiable-assurance-v1-threat-model.md)
55
+
56
+ If you use the offline sample, inspect `.witnora/try/agentcert-report.html` and
23
57
  `.witnora/try/agentcert-evidence.json`. The output is explicitly synthetic and
24
58
  reported-strength: it proves the evidence tooling works, not that a real agent
25
59
  is reliable.
@@ -92,11 +126,11 @@ Default outputs:
92
126
  - `.witnora/latest/reviewed-failure-dataset.jsonl`
93
127
  - `.witnora/latest/monitor.json`
94
128
 
95
- Push the validated evidence bundle into a hosted Witnora project:
129
+ Onboard once, then push the validated evidence bundle into the same project:
96
130
 
97
131
  ```bash
98
- npx witnora connect --server https://witnora.com --project your-project-id
99
- npx witnora push --evidence .witnora/latest/agentcert-evidence.json
132
+ npx witnora@latest onboard --project your-project-id
133
+ npx witnora@latest push --evidence .witnora/latest/agentcert-evidence.json
100
134
  ```
101
135
 
102
136
  ### Automatic agent and environment context
@@ -194,7 +228,7 @@ the bundled deterministic adapter contract and writes
194
228
  `.witnora/sandbox/sandbox-adapter-conformance.json`. No `@agentcert` scoped
195
229
  package is required.
196
230
 
197
- After `agentcert connect`, certify and upload in one command:
231
+ After `witnora onboard`, certify and upload in one command:
198
232
 
199
233
  ```bash
200
234
  npx witnora sandbox push --adapter ./agentcert.sandbox.mjs
@@ -321,3 +355,14 @@ npm --prefix packages/witnora-cli ci
321
355
  npm --prefix packages/witnora-cli run build
322
356
  node packages/witnora-cli/dist/cli.js run --profile public-demo
323
357
  ```
358
+
359
+ ## Design-partner independent assurance v0.2
360
+
361
+ The sandbox-only reference path separates a write-capable Gateway from a
362
+ customer-operated read-only Probe, binds approval and a single-use grant to one
363
+ declared GitHub pull-request action, reconciles target state after dispatch, and
364
+ produces a packet that TypeScript and Python can verify offline.
365
+
366
+ Start with the [two-hour design-partner quickstart](../../docs/guides/design-partner-two-hour-quickstart.md).
367
+ The included GitHub target is an emulator. It does not claim live GitHub or
368
+ production acceptance, organizational independence, or external review.
@@ -0,0 +1,312 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { canonicalJson, sha256Canonical, WITNORA_CANONICALIZATION_VERSION } from "./canonical.js";
3
+ export const ASSURANCE_CONTRACT_VERSION = "witnora.assurance_contract.v0.1";
4
+ export const COMPILED_CONTRACT_VERSION = "witnora.compiled_assurance_contract.v0.1";
5
+ export const ASSURANCE_COMPILER_VERSION = "witnora.contract_compiler.v0.1";
6
+ export async function readAssuranceContract(path) {
7
+ const raw = await readFile(path, "utf8");
8
+ let parsed;
9
+ try {
10
+ parsed = JSON.parse(raw);
11
+ }
12
+ catch {
13
+ throw new Error("Contract must be JSON (JSON is valid YAML 1.2). Generated contracts use a .json file to avoid parser ambiguity.");
14
+ }
15
+ return validateAssuranceContract(parsed);
16
+ }
17
+ export function validateAssuranceContract(input) {
18
+ const errors = [];
19
+ const root = object(input, "$", errors);
20
+ exactKeys(root, ["kind", "metadata", "subject", "capability", "authority", "preconditions", "effect", "execution", "observation", "privacy", "evidence", "revalidation"], "$", errors);
21
+ if (root.kind !== ASSURANCE_CONTRACT_VERSION)
22
+ errors.push(`$.kind must equal ${ASSURANCE_CONTRACT_VERSION}.`);
23
+ const metadata = object(root.metadata, "$.metadata", errors);
24
+ exactKeys(metadata, ["id", "version", "description"], "$.metadata", errors);
25
+ const subject = object(root.subject, "$.subject", errors);
26
+ exactKeys(subject, ["agentId", "environment"], "$.subject", errors);
27
+ const capability = object(root.capability, "$.capability", errors);
28
+ exactKeys(capability, ["id", "action", "sideEffect", "risk"], "$.capability", errors);
29
+ const authority = object(root.authority, "$.authority", errors);
30
+ exactKeys(authority, ["mandateRef", "policyRef", "approval", "resourceScope"], "$.authority", errors);
31
+ const approval = object(authority.approval, "$.authority.approval", errors);
32
+ exactKeys(approval, ["required"], "$.authority.approval", errors);
33
+ const resourceScope = object(authority.resourceScope, "$.authority.resourceScope", errors);
34
+ exactKeys(resourceScope, ["type", "pattern"], "$.authority.resourceScope", errors);
35
+ const effect = object(root.effect, "$.effect", errors);
36
+ exactKeys(effect, ["eventuallyWithin", "stableFor", "must", "mustNot"], "$.effect", errors, ["stableFor"]);
37
+ const execution = object(root.execution, "$.execution", errors);
38
+ exactKeys(execution, ["atMostOnce", "idempotencyRequired", "retryPolicy"], "$.execution", errors);
39
+ const retry = object(execution.retryPolicy, "$.execution.retryPolicy", errors);
40
+ exactKeys(retry, ["maxAttempts"], "$.execution.retryPolicy", errors);
41
+ const observation = object(root.observation, "$.observation", errors);
42
+ exactKeys(observation, ["mode", "probeRef", "reconcileWithTargetAuditLog"], "$.observation", errors);
43
+ const privacy = object(root.privacy, "$.privacy", errors);
44
+ exactKeys(privacy, ["localOnly", "disclose", "commitOnly"], "$.privacy", errors);
45
+ const evidence = object(root.evidence, "$.evidence", errors);
46
+ exactKeys(evidence, ["require"], "$.evidence", errors);
47
+ const revalidation = object(root.revalidation, "$.revalidation", errors);
48
+ exactKeys(revalidation, ["triggers"], "$.revalidation", errors);
49
+ requiredString(metadata.id, "$.metadata.id", errors);
50
+ positiveInteger(metadata.version, "$.metadata.version", errors);
51
+ requiredString(metadata.description, "$.metadata.description", errors);
52
+ requiredString(subject.agentId, "$.subject.agentId", errors);
53
+ requiredString(subject.environment, "$.subject.environment", errors);
54
+ requiredString(capability.id, "$.capability.id", errors);
55
+ oneOf(capability.action, ["SUBMIT", "PAY", "SEND", "UPDATE"], "$.capability.action", errors);
56
+ bool(capability.sideEffect, "$.capability.sideEffect", errors);
57
+ oneOf(capability.risk, ["low", "medium", "high", "critical"], "$.capability.risk", errors);
58
+ requiredString(authority.mandateRef, "$.authority.mandateRef", errors);
59
+ requiredString(authority.policyRef, "$.authority.policyRef", errors);
60
+ bool(approval.required, "$.authority.approval.required", errors);
61
+ requiredString(resourceScope.type, "$.authority.resourceScope.type", errors);
62
+ requiredString(resourceScope.pattern, "$.authority.resourceScope.pattern", errors);
63
+ const preconditions = predicates(root.preconditions, "$.preconditions", errors);
64
+ requiredString(effect.eventuallyWithin, "$.effect.eventuallyWithin", errors);
65
+ if (typeof effect.eventuallyWithin === "string" && parseContractDurationMs(effect.eventuallyWithin) === undefined)
66
+ errors.push("$.effect.eventuallyWithin must be a positive duration such as 30s, 5m, or 1h.");
67
+ if (effect.stableFor !== undefined) {
68
+ requiredString(effect.stableFor, "$.effect.stableFor", errors);
69
+ if (typeof effect.stableFor === "string" && parseContractDurationMs(effect.stableFor) === undefined)
70
+ errors.push("$.effect.stableFor must be a positive duration such as 5s, 1m, or 1h.");
71
+ }
72
+ const must = predicates(effect.must, "$.effect.must", errors);
73
+ const mustNot = predicates(effect.mustNot, "$.effect.mustNot", errors);
74
+ bool(execution.atMostOnce, "$.execution.atMostOnce", errors);
75
+ bool(execution.idempotencyRequired, "$.execution.idempotencyRequired", errors);
76
+ positiveInteger(retry.maxAttempts, "$.execution.retryPolicy.maxAttempts", errors);
77
+ if (observation.mode !== "independent_read_path")
78
+ errors.push("$.observation.mode must equal independent_read_path.");
79
+ requiredString(observation.probeRef, "$.observation.probeRef", errors);
80
+ bool(observation.reconcileWithTargetAuditLog, "$.observation.reconcileWithTargetAuditLog", errors);
81
+ const localOnly = strings(privacy.localOnly, "$.privacy.localOnly", errors);
82
+ const disclose = strings(privacy.disclose, "$.privacy.disclose", errors);
83
+ const commitOnly = strings(privacy.commitOnly, "$.privacy.commitOnly", errors);
84
+ const overlap = [...localOnly, ...disclose, ...commitOnly].filter((value, index, all) => all.indexOf(value) !== index);
85
+ if (overlap.length)
86
+ errors.push(`$.privacy categories overlap: ${[...new Set(overlap)].join(", ")}.`);
87
+ const requireEvidence = strings(evidence.require, "$.evidence.require", errors);
88
+ const triggers = strings(revalidation.triggers, "$.revalidation.triggers", errors);
89
+ if (capability.sideEffect === true && ["high", "critical"].includes(String(capability.risk)) && approval.required !== true) {
90
+ errors.push("$.authority.approval.required must be true for a high-risk side effect.");
91
+ }
92
+ if (execution.atMostOnce !== true || execution.idempotencyRequired !== true)
93
+ errors.push("High-risk v0.1 contracts require at-most-once grant use and target idempotency.");
94
+ if (!must.length)
95
+ errors.push("$.effect.must must contain at least one independently observable predicate.");
96
+ if (errors.length)
97
+ throw new Error(`Invalid Assurance Contract:\n${errors.map((error) => `- ${error}`).join("\n")}`);
98
+ return structuredClone(input);
99
+ }
100
+ export function compileAssuranceContract(input) {
101
+ const contract = validateAssuranceContract(input);
102
+ const sourceContractDigest = sha256Canonical(contract);
103
+ const artifacts = {
104
+ capabilityRegistration: contract.capability,
105
+ runtimePolicyInput: { ...contract.authority, contractDigest: sourceContractDigest },
106
+ approvalPresentation: { capability: contract.capability.id, action: contract.capability.action, resourceType: contract.authority.resourceScope.type, exactActionDigestRequired: true },
107
+ executionGrantBindingTemplate: {
108
+ contractDigest: sourceContractDigest,
109
+ policyRef: contract.authority.policyRef,
110
+ mandateRef: contract.authority.mandateRef,
111
+ agentId: contract.subject.agentId,
112
+ environment: contract.subject.environment,
113
+ maxUses: 1,
114
+ },
115
+ outcomeProbeSpecification: { ...contract.observation, ...contract.effect },
116
+ evidenceObligations: [...contract.evidence.require].sort(),
117
+ privacyObligations: contract.privacy,
118
+ revalidationFingerprint: sha256Canonical({ triggers: contract.revalidation.triggers, values: revalidationValues(contract) }),
119
+ };
120
+ return {
121
+ schemaVersion: COMPILED_CONTRACT_VERSION,
122
+ compilerVersion: ASSURANCE_COMPILER_VERSION,
123
+ canonicalizationVersion: WITNORA_CANONICALIZATION_VERSION,
124
+ sourceContractDigest,
125
+ ...artifacts,
126
+ artifactDigests: Object.fromEntries(Object.entries(artifacts).map(([key, value]) => [key, sha256Canonical(value)])),
127
+ };
128
+ }
129
+ export function parseContractDurationMs(value) {
130
+ const match = /^(\d+(?:\.\d+)?)(ms|s|m|h|d)$/.exec(value);
131
+ if (!match)
132
+ return undefined;
133
+ const amount = Number(match[1]);
134
+ const multiplier = { ms: 1, s: 1_000, m: 60_000, h: 3_600_000, d: 86_400_000 }[match[2]];
135
+ if (multiplier === undefined)
136
+ return undefined;
137
+ const result = amount * multiplier;
138
+ return Number.isFinite(result) && result > 0 ? result : undefined;
139
+ }
140
+ export function diffAssuranceContracts(previousInput, nextInput) {
141
+ const previous = validateAssuranceContract(previousInput);
142
+ const next = validateAssuranceContract(nextInput);
143
+ const changedPaths = changed(previous, next);
144
+ const reasons = new Set();
145
+ if (changedPaths.some((path) => path.startsWith("capability")))
146
+ reasons.add("CAPABILITY_CHANGED");
147
+ if (changedPaths.some((path) => path.startsWith("authority.resourceScope")))
148
+ reasons.add("RESOURCE_SCOPE_CHANGED");
149
+ if (changedPaths.some((path) => path.startsWith("authority.approval")))
150
+ reasons.add("APPROVAL_REQUIREMENT_CHANGED");
151
+ if (changedPaths.some((path) => path.startsWith("authority.policyRef")))
152
+ reasons.add("POLICY_VERSION_CHANGED");
153
+ if (changedPaths.some((path) => path.startsWith("effect") || path.startsWith("observation")))
154
+ reasons.add("OUTCOME_SEMANTICS_CHANGED");
155
+ if (changedPaths.some((path) => path.startsWith("privacy")))
156
+ reasons.add("PRIVACY_DISCLOSURE_CHANGED");
157
+ if (changedPaths.some((path) => path.startsWith("subject")))
158
+ reasons.add("SUBJECT_SCOPE_CHANGED");
159
+ const authorityExpanded = scopeExpanded(previous, next);
160
+ if (authorityExpanded)
161
+ reasons.add("AUTHORITY_EXPANDED");
162
+ return {
163
+ schemaVersion: "witnora.assurance_contract_diff.v0.1",
164
+ revalidationRequired: reasons.size > 0,
165
+ authorityExpanded,
166
+ reasons: [...reasons].sort(),
167
+ changedPaths,
168
+ };
169
+ }
170
+ export function exampleAssuranceContract() {
171
+ return {
172
+ kind: ASSURANCE_CONTRACT_VERSION,
173
+ metadata: { id: "record-submit", version: 1, description: "Submit one record through a controlled action path." },
174
+ subject: { agentId: "example-workflow-agent", environment: "sandbox" },
175
+ capability: { id: "record.submit", action: "SUBMIT", sideEffect: true, risk: "high" },
176
+ authority: {
177
+ mandateRef: "mandate:record-submit:v1",
178
+ policyRef: "policy:high-risk-submit:v1",
179
+ approval: { required: true },
180
+ resourceScope: { type: "record", pattern: "sandbox/*" },
181
+ },
182
+ preconditions: [{ path: "status", operator: "equals", value: "DRAFT" }],
183
+ effect: {
184
+ eventuallyWithin: "30s",
185
+ must: [{ path: "status", operator: "equals", value: "SUBMITTED" }],
186
+ mustNot: [{ path: "prohibitedSideEffect", operator: "equals", value: true }],
187
+ },
188
+ execution: { atMostOnce: true, idempotencyRequired: true, retryPolicy: { maxAttempts: 1 } },
189
+ observation: { mode: "independent_read_path", probeRef: "probe:record-status:v1", reconcileWithTargetAuditLog: true },
190
+ privacy: {
191
+ localOnly: ["prompt", "agentSource", "credentials", "rawArguments", "rawResult"],
192
+ disclose: ["actionType", "policyDecision", "outcomeStatus"],
193
+ commitOnly: ["resourceId", "canonicalAction"],
194
+ },
195
+ evidence: { require: ["sourceSignature", "serverReceipt", "grantBinding", "outcomeObservation", "sequenceContinuity"] },
196
+ revalidation: { triggers: ["agentVersion", "modelVersion", "promptDigest", "toolSchemaDigest", "policyVersion", "probeVersion"] },
197
+ };
198
+ }
199
+ export function resourceMatchesScope(scope, resourceId) {
200
+ if (!scope.type || !resourceId)
201
+ return false;
202
+ const wildcard = scope.pattern.indexOf("*");
203
+ if (wildcard < 0)
204
+ return resourceId === scope.pattern;
205
+ if (wildcard !== scope.pattern.length - 1 || scope.pattern.indexOf("*", wildcard + 1) >= 0)
206
+ return false;
207
+ return resourceId.startsWith(scope.pattern.slice(0, -1));
208
+ }
209
+ export function renderContractExplanation(contract) {
210
+ const compiled = compileAssuranceContract(contract);
211
+ return [
212
+ `Contract valid: ${contract.metadata.id} v${contract.metadata.version}`,
213
+ `Contract digest: ${compiled.sourceContractDigest}`,
214
+ `Capability: ${contract.capability.id} (${contract.capability.action}, ${contract.capability.risk} risk)`,
215
+ `Approval: ${contract.authority.approval.required ? "required and bound to exact action digest" : "not required"}`,
216
+ `Outcome probe: ${contract.observation.probeRef} via ${contract.observation.mode}`,
217
+ `Cloud disclosure: ${contract.privacy.disclose.join(", ") || "none"}`,
218
+ `Commitment only: ${contract.privacy.commitOnly.join(", ") || "none"}`,
219
+ `Local only: ${contract.privacy.localOnly.join(", ") || "none"}`,
220
+ `Revalidation triggers: ${contract.revalidation.triggers.join(", ")}`,
221
+ "Enforcement caveat: this contract covers only dispatches mediated by a credential-isolated Gateway.",
222
+ ].join("\n") + "\n";
223
+ }
224
+ function revalidationValues(contract) {
225
+ return {
226
+ agentId: contract.subject.agentId,
227
+ environment: contract.subject.environment,
228
+ capability: contract.capability,
229
+ authority: contract.authority,
230
+ effect: contract.effect,
231
+ observation: contract.observation,
232
+ privacy: contract.privacy,
233
+ };
234
+ }
235
+ function scopeExpanded(previous, next) {
236
+ if (previous.authority.resourceScope.pattern !== next.authority.resourceScope.pattern && next.authority.resourceScope.pattern === "*")
237
+ return true;
238
+ if (previous.authority.approval.required && !next.authority.approval.required)
239
+ return true;
240
+ if (!previous.capability.sideEffect && next.capability.sideEffect)
241
+ return true;
242
+ const rank = { low: 0, medium: 1, high: 2, critical: 3 };
243
+ return rank[next.capability.risk] > rank[previous.capability.risk];
244
+ }
245
+ function changed(previous, next, path = "") {
246
+ if (canonicalJson(previous) === canonicalJson(next))
247
+ return [];
248
+ if (Array.isArray(previous) || Array.isArray(next) || !previous || !next || typeof previous !== "object" || typeof next !== "object")
249
+ return [path || "$"];
250
+ const left = previous;
251
+ const right = next;
252
+ return [...new Set([...Object.keys(left), ...Object.keys(right)])].sort().flatMap((key) => changed(left[key], right[key], path ? `${path}.${key}` : key));
253
+ }
254
+ function predicates(value, path, errors) {
255
+ if (!Array.isArray(value)) {
256
+ errors.push(`${path} must be an array.`);
257
+ return [];
258
+ }
259
+ return value.map((item, index) => {
260
+ const itemPath = `${path}[${index}]`;
261
+ const result = object(item, itemPath, errors);
262
+ exactKeys(result, ["path", "operator", "value"], itemPath, errors, ["value"]);
263
+ requiredString(result.path, `${itemPath}.path`, errors);
264
+ oneOf(result.operator, ["equals", "not_equals", "exists", "not_exists", "contains", "count"], `${itemPath}.operator`, errors);
265
+ if (!["exists", "not_exists"].includes(String(result.operator)) && result.value === undefined)
266
+ errors.push(`${itemPath}.value is required for ${String(result.operator)}.`);
267
+ return result;
268
+ });
269
+ }
270
+ function object(value, path, errors) {
271
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
272
+ errors.push(`${path} must be an object.`);
273
+ return {};
274
+ }
275
+ return value;
276
+ }
277
+ function exactKeys(value, allowed, path, errors, optional = []) {
278
+ for (const key of Object.keys(value))
279
+ if (!allowed.includes(key))
280
+ errors.push(`${path}.${key} is not allowed.`);
281
+ for (const key of allowed)
282
+ if (!optional.includes(key) && !(key in value))
283
+ errors.push(`${path}.${key} is required.`);
284
+ }
285
+ function requiredString(value, path, errors) {
286
+ if (typeof value !== "string" || !value.trim())
287
+ errors.push(`${path} must be a non-empty string.`);
288
+ }
289
+ function strings(value, path, errors) {
290
+ if (!Array.isArray(value)) {
291
+ errors.push(`${path} must be an array.`);
292
+ return [];
293
+ }
294
+ const output = [];
295
+ value.forEach((item, index) => { requiredString(item, `${path}[${index}]`, errors); if (typeof item === "string")
296
+ output.push(item); });
297
+ if (new Set(output).size !== output.length)
298
+ errors.push(`${path} must not contain duplicates.`);
299
+ return output;
300
+ }
301
+ function positiveInteger(value, path, errors) {
302
+ if (!Number.isSafeInteger(value) || Number(value) <= 0)
303
+ errors.push(`${path} must be a positive safe integer.`);
304
+ }
305
+ function bool(value, path, errors) {
306
+ if (typeof value !== "boolean")
307
+ errors.push(`${path} must be Boolean.`);
308
+ }
309
+ function oneOf(value, allowed, path, errors) {
310
+ if (typeof value !== "string" || !allowed.includes(value))
311
+ errors.push(`${path} must be one of ${allowed.join(", ")}.`);
312
+ }
@@ -0,0 +1,245 @@
1
+ import { createHash, generateKeyPairSync, randomBytes, randomUUID, sign } from "node:crypto";
2
+ import { mkdir, writeFile } from "node:fs/promises";
3
+ import { resolve } from "node:path";
4
+ import { compileAssuranceContract, diffAssuranceContracts, exampleAssuranceContract, resourceMatchesScope } from "./assurance-contract.js";
5
+ import { canonicalJson, sha256Canonical } from "./canonical.js";
6
+ import { serverAttestedValue, sourceSignedValue, outcomeProbeSignedValue, preconditionProbeSignedValue, resourceScopeProofSignedValue, verifyEvidencePacket, } from "./offline-verifier.js";
7
+ import { examplePrivacyManifest, transformForEgress } from "./privacy-manifest.js";
8
+ import { evaluateOutcome } from "./outcome-evaluator.js";
9
+ export async function runAssuranceLoopDemo(options) {
10
+ const now = options.now ?? new Date();
11
+ const attack = options.attack ?? "none";
12
+ const contract = exampleAssuranceContract();
13
+ const compiledContract = compileAssuranceContract(contract);
14
+ const privacyManifest = examplePrivacyManifest();
15
+ const privacyInput = {
16
+ actionType: "SUBMIT",
17
+ outcomeStatus: "SATISFIED",
18
+ policyDecision: "REQUIRE_APPROVAL",
19
+ resourceId: "sandbox/record-001",
20
+ actionParameters: { expectedRevision: 1, status: "SUBMITTED" },
21
+ prompt: "Confidential local task context",
22
+ credentials: "local-demo-write-credential",
23
+ rawResult: { id: "record-001", status: "SUBMITTED", revision: 2 },
24
+ };
25
+ const privacy = transformForEgress(privacyInput, privacyManifest, randomBytes(32));
26
+ const resourceScopeAllowed = resourceMatchesScope(contract.authority.resourceScope, privacyInput.resourceId);
27
+ const canonicalAction = {
28
+ tenantRef: "tenant:local-demo",
29
+ projectRef: "project:assurance-loop",
30
+ agentRef: "agent:example-workflow-agent",
31
+ agentVersion: "1.0.0",
32
+ environment: "sandbox",
33
+ capability: contract.capability.id,
34
+ action: contract.capability.action,
35
+ resourceCommitment: privacy.commitments.resourceId.digest,
36
+ parameterCommitment: privacy.commitments.actionParameters.digest,
37
+ outcomePredicateDigest: sha256Canonical(contract.effect),
38
+ contractDigest: compiledContract.sourceContractDigest,
39
+ policyRef: contract.authority.policyRef,
40
+ mandateRef: contract.authority.mandateRef,
41
+ };
42
+ const actionDigest = sha256Canonical(canonicalAction);
43
+ const target = new LocalReferenceTarget();
44
+ const before = target.read("record-001");
45
+ if (before.status !== "DRAFT")
46
+ throw new Error("Reference target precondition failed.");
47
+ const preconditionObservedAt = new Date(now.getTime() + 10).toISOString();
48
+ const policy = { decision: "REQUIRE_APPROVAL", actionDigest, policyRef: contract.authority.policyRef };
49
+ const approval = {
50
+ approved: true,
51
+ actionDigest,
52
+ tenantRef: String(canonicalAction.tenantRef),
53
+ agentRef: String(canonicalAction.agentRef),
54
+ environment: String(canonicalAction.environment),
55
+ contractDigest: compiledContract.sourceContractDigest,
56
+ policyRef: contract.authority.policyRef,
57
+ resourceCommitment: String(canonicalAction.resourceCommitment),
58
+ expiresAt: new Date(now.getTime() + 2 * 60_000).toISOString(),
59
+ approverRef: "reviewer:local-demo",
60
+ };
61
+ const grant = {
62
+ id: `grant_${randomUUID()}`,
63
+ tenantRef: String(canonicalAction.tenantRef),
64
+ agentRef: String(canonicalAction.agentRef),
65
+ environment: String(canonicalAction.environment),
66
+ actionDigest,
67
+ contractDigest: compiledContract.sourceContractDigest,
68
+ policyRef: contract.authority.policyRef,
69
+ mandateRef: contract.authority.mandateRef,
70
+ resourceCommitment: String(canonicalAction.resourceCommitment),
71
+ maxUses: 1,
72
+ consumedCount: 0,
73
+ issuedAt: now.toISOString(),
74
+ expiresAt: new Date(now.getTime() + 5 * 60_000).toISOString(),
75
+ };
76
+ const grants = new AtomicLocalGrantStore();
77
+ grants.issue(grant.id);
78
+ grants.consume(grant.id);
79
+ grant.consumedCount = 1;
80
+ // The only write credential exists inside this controlled adapter boundary.
81
+ target.controlledSubmit("record-001", { status: "SUBMITTED", expectedRevision: 1 }, actionDigest);
82
+ const observed = target.read("record-001");
83
+ const audit = target.auditSince(0);
84
+ const targetAuditReconciled = audit.length === 1 && audit[0].actionDigest === actionDigest && audit[0].revision === observed.revision;
85
+ const normalizedObservation = { status: observed.status, prohibitedSideEffect: false };
86
+ const outcomeEvaluation = evaluateOutcome(contract.effect, normalizedObservation, { targetAuditReconciled });
87
+ const outcomeSatisfied = outcomeEvaluation.status === "VERIFIED";
88
+ const eventPayloads = [
89
+ ["witnora.run.started", { collector: "local-reference-collector", actionDigest }],
90
+ ["witnora.precondition.observed", { status: "SATISFIED", rawObservationCommitment: sha256Canonical(before) }],
91
+ ["witnora.policy.evaluated", policy],
92
+ ["witnora.approval.recorded", approval],
93
+ ["witnora.grant.consumed", { grantId: grant.id, actionDigest }],
94
+ ["witnora.action.executed", { actionDigest, adapter: "local-reference-target" }],
95
+ ["witnora.outcome.observed", { status: outcomeEvaluation.status, rawObservationCommitment: sha256Canonical(observed) }],
96
+ ["witnora.run.completed", { droppedEvents: 0, targetAuditReconciled }],
97
+ ];
98
+ const events = eventPayloads.reduce((result, [type, payload], index) => {
99
+ const previousHash = result.at(-1)?.eventHash ?? null;
100
+ const base = { sequence: index + 1, type, occurredAt: new Date(now.getTime() + index * 10).toISOString(), previousHash, payloadDigest: sha256Canonical(payload) };
101
+ result.push({ ...base, eventHash: sha256Canonical(base) });
102
+ return result;
103
+ }, []);
104
+ const artifactContent = Buffer.from(JSON.stringify({ before, observed, targetAuditReconciled }, null, 2));
105
+ const artifactBase64 = artifactContent.toString("base64");
106
+ const sourceKeys = generateKeyPairSync("ed25519");
107
+ const serverKeys = generateKeyPairSync("ed25519");
108
+ const probeKeys = generateKeyPairSync("ed25519");
109
+ const scopeKeys = generateKeyPairSync("ed25519");
110
+ const packet = {
111
+ schemaVersion: "witnora.verifiable_evidence_packet.v0.1",
112
+ packetId: `packet_${randomUUID()}`,
113
+ issuedAt: now.toISOString(),
114
+ contract,
115
+ compiledContract,
116
+ privacy: { manifest: privacyManifest, transformed: privacy },
117
+ action: { canonical: canonicalAction, digest: actionDigest },
118
+ precondition: {
119
+ status: "SATISFIED",
120
+ normalizedObservation: { status: before.status },
121
+ rawObservationCommitment: sha256Canonical(before),
122
+ observedAt: preconditionObservedAt,
123
+ dataFreshnessMs: 0,
124
+ probeRef: contract.observation.probeRef,
125
+ probeVersion: "1.0.0",
126
+ probeDigest: sha256Canonical({ implementation: "local-reference-read-path", version: "1.0.0" }),
127
+ observationSource: "local-reference-target/read-only",
128
+ independentReadPath: true,
129
+ probeSignature: emptySignature(),
130
+ },
131
+ resourceScopeProof: {
132
+ decision: resourceScopeAllowed ? "ALLOW" : "DENY",
133
+ contractScopeDigest: sha256Canonical(contract.authority.resourceScope),
134
+ resourceCommitment: String(canonicalAction.resourceCommitment),
135
+ evaluatedAt: new Date(now.getTime() + 15).toISOString(),
136
+ evaluatorRef: "local-policy-gateway-v1",
137
+ signature: emptySignature(),
138
+ },
139
+ policy,
140
+ approval,
141
+ grant,
142
+ events,
143
+ outcome: {
144
+ status: outcomeEvaluation.status,
145
+ expectedDigest: sha256Canonical(contract.effect),
146
+ normalizedObservation,
147
+ rawObservationCommitment: sha256Canonical(observed),
148
+ observedAt: new Date(now.getTime() + 60).toISOString(),
149
+ firstSatisfiedAt: new Date(now.getTime() + 60).toISOString(),
150
+ dataFreshnessMs: 0,
151
+ probeRef: contract.observation.probeRef,
152
+ probeVersion: "1.0.0",
153
+ probeDigest: sha256Canonical({ implementation: "local-reference-read-path", version: "1.0.0" }),
154
+ observationSource: "local-reference-target/read-only",
155
+ independentReadPath: true,
156
+ targetAuditReconciled,
157
+ probeSignature: emptySignature(),
158
+ },
159
+ artifacts: [{
160
+ path: "outcome-observation.json",
161
+ kind: "outcome_observation",
162
+ size: artifactContent.byteLength,
163
+ sha256: createHash("sha256").update(artifactContent).digest("hex"),
164
+ contentBase64: artifactBase64,
165
+ }],
166
+ evidenceStrength: { reported: true, recorded: true, enforced: true, outcomeVerified: outcomeSatisfied && targetAuditReconciled, independentlyReviewed: false },
167
+ sourceSignature: emptySignature(),
168
+ serverAttestation: emptySignature(),
169
+ };
170
+ packet.resourceScopeProof.signature = signValue(resourceScopeProofSignedValue(packet), "local-scope-evaluator-v1", scopeKeys.privateKey, scopeKeys.publicKey.export({ type: "spki", format: "pem" }).toString());
171
+ packet.precondition.probeSignature = signValue(preconditionProbeSignedValue(packet), "local-outcome-probe-v1", probeKeys.privateKey, probeKeys.publicKey.export({ type: "spki", format: "pem" }).toString());
172
+ packet.outcome.probeSignature = signValue(outcomeProbeSignedValue(packet), "local-outcome-probe-v1", probeKeys.privateKey, probeKeys.publicKey.export({ type: "spki", format: "pem" }).toString());
173
+ packet.sourceSignature = signValue(sourceSignedValue(packet), "local-source-v1", sourceKeys.privateKey, sourceKeys.publicKey.export({ type: "spki", format: "pem" }).toString());
174
+ packet.serverAttestation = signValue(serverAttestedValue(packet), "local-reference-server-v1", serverKeys.privateKey, serverKeys.publicKey.export({ type: "spki", format: "pem" }).toString());
175
+ applyAttack(packet, attack);
176
+ const trustBundle = {
177
+ schemaVersion: "witnora.verification_trust_bundle.v0.1",
178
+ keys: {
179
+ "local-source-v1": trustKey(sourceKeys.publicKey.export({ type: "spki", format: "pem" }).toString(), "source"),
180
+ "local-reference-server-v1": trustKey(serverKeys.publicKey.export({ type: "spki", format: "pem" }).toString(), "server_attestor"),
181
+ "local-outcome-probe-v1": trustKey(probeKeys.publicKey.export({ type: "spki", format: "pem" }).toString(), "outcome_probe"),
182
+ "local-scope-evaluator-v1": trustKey(scopeKeys.publicKey.export({ type: "spki", format: "pem" }).toString(), "resource_scope_evaluator"),
183
+ },
184
+ };
185
+ const report = verifyEvidencePacket(packet, new Date(now.getTime() + 1_000), trustBundle);
186
+ const changed = structuredClone(contract);
187
+ changed.subject.agentId = `${contract.subject.agentId}-v2`;
188
+ const revalidation = diffAssuranceContracts(contract, changed);
189
+ const outDir = resolve(options.outDir);
190
+ await mkdir(outDir, { recursive: true });
191
+ const packetPath = resolve(outDir, "witnora-evidence-packet.json");
192
+ const reportPath = resolve(outDir, "witnora-offline-verification.json");
193
+ const revalidationPath = resolve(outDir, "witnora-revalidation-diff.json");
194
+ const trustBundlePath = resolve(outDir, "witnora-trust-bundle.json");
195
+ await Promise.all([
196
+ writeFile(packetPath, `${JSON.stringify(packet, null, 2)}\n`, "utf8"),
197
+ writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`, "utf8"),
198
+ writeFile(revalidationPath, `${JSON.stringify(revalidation, null, 2)}\n`, "utf8"),
199
+ writeFile(trustBundlePath, `${JSON.stringify(trustBundle, null, 2)}\n`, "utf8"),
200
+ ]);
201
+ return { packet, report, packetPath, reportPath, revalidationPath, trustBundlePath };
202
+ }
203
+ class LocalReferenceTarget {
204
+ record = { id: "record-001", status: "DRAFT", revision: 1 };
205
+ audit = [];
206
+ read(id) {
207
+ if (id !== this.record.id)
208
+ throw new Error("Record not found.");
209
+ return structuredClone(this.record);
210
+ }
211
+ controlledSubmit(id, input, actionDigest) {
212
+ if (id !== this.record.id || input.status !== "SUBMITTED" || input.expectedRevision !== this.record.revision)
213
+ throw new Error("Controlled execution rejected.");
214
+ this.record = { ...this.record, status: input.status, revision: this.record.revision + 1 };
215
+ this.audit.push({ sequence: this.audit.length + 1, actionDigest, revision: this.record.revision });
216
+ }
217
+ auditSince(sequence) {
218
+ return structuredClone(this.audit.filter((entry) => entry.sequence > sequence));
219
+ }
220
+ }
221
+ class AtomicLocalGrantStore {
222
+ state = new Map();
223
+ issue(id) { if (this.state.has(id))
224
+ throw new Error("Grant already exists."); this.state.set(id, "ISSUED"); }
225
+ consume(id) { if (this.state.get(id) !== "ISSUED")
226
+ throw new Error("Grant replay rejected."); this.state.set(id, "CONSUMED"); }
227
+ }
228
+ function signValue(value, keyId, privateKey, publicKeyPem) {
229
+ return { keyId, algorithm: "Ed25519", publicKeyPem, valueDigest: sha256Canonical(value), signature: sign(null, Buffer.from(canonicalJson(value)), privateKey).toString("base64") };
230
+ }
231
+ function emptySignature() { return { keyId: "", algorithm: "Ed25519", publicKeyPem: "", valueDigest: "", signature: "" }; }
232
+ function trustKey(publicKeyPem, usage) {
233
+ return { publicKeyPem, usage, tenantRef: "tenant:local-demo", projectRef: "project:assurance-loop", environment: "sandbox" };
234
+ }
235
+ function applyAttack(packet, attack) {
236
+ if (attack === "parameter-substitution")
237
+ packet.action.canonical.parameterCommitment = "0".repeat(64);
238
+ if (attack === "replay")
239
+ packet.grant.consumedCount = 2;
240
+ if (attack === "false-success") {
241
+ packet.outcome.normalizedObservation = { status: "DRAFT", prohibitedSideEffect: false };
242
+ }
243
+ if (attack === "event-tamper")
244
+ packet.events[2].payloadDigest = "f".repeat(64);
245
+ }