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
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/
|
|
12
|
+
[GitHub source](https://github.com/Kakarottoooo/witnora)
|
|
13
13
|
|
|
14
|
-
## 5-minute
|
|
14
|
+
## 5-minute hosted path
|
|
15
15
|
|
|
16
|
-
|
|
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
|
|
20
|
+
npx witnora@latest onboard --project your-project-id
|
|
20
21
|
```
|
|
21
22
|
|
|
22
|
-
|
|
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
|
-
|
|
129
|
+
Onboard once, then push the validated evidence bundle into the same project:
|
|
96
130
|
|
|
97
131
|
```bash
|
|
98
|
-
npx witnora
|
|
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 `
|
|
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
|
+
}
|