@tenova/swt3-ai 0.5.1 → 0.5.3

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 (70) hide show
  1. package/LICENSE +191 -0
  2. package/README.md +227 -10
  3. package/dist/buffer.d.ts +7 -1
  4. package/dist/buffer.d.ts.map +1 -1
  5. package/dist/buffer.js +38 -3
  6. package/dist/buffer.js.map +1 -1
  7. package/dist/cli.d.ts +13 -0
  8. package/dist/cli.d.ts.map +1 -0
  9. package/dist/cli.js +202 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/config.d.ts +18 -5
  12. package/dist/config.d.ts.map +1 -1
  13. package/dist/config.js +346 -42
  14. package/dist/config.js.map +1 -1
  15. package/dist/demo.d.ts +1 -1
  16. package/dist/demo.d.ts.map +1 -1
  17. package/dist/demo.js +88 -4
  18. package/dist/demo.js.map +1 -1
  19. package/dist/doctor.d.ts +20 -0
  20. package/dist/doctor.d.ts.map +1 -0
  21. package/dist/doctor.js +357 -0
  22. package/dist/doctor.js.map +1 -0
  23. package/dist/environment.d.ts +34 -0
  24. package/dist/environment.d.ts.map +1 -0
  25. package/dist/environment.js +99 -0
  26. package/dist/environment.js.map +1 -0
  27. package/dist/exporters/chain-monitor.d.ts +55 -0
  28. package/dist/exporters/chain-monitor.d.ts.map +1 -0
  29. package/dist/exporters/chain-monitor.js +172 -0
  30. package/dist/exporters/chain-monitor.js.map +1 -0
  31. package/dist/hardware.d.ts +96 -0
  32. package/dist/hardware.d.ts.map +1 -0
  33. package/dist/hardware.js +265 -0
  34. package/dist/hardware.js.map +1 -0
  35. package/dist/index.d.ts +19 -3
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +10 -2
  38. package/dist/index.js.map +1 -1
  39. package/dist/merkle.d.ts +107 -0
  40. package/dist/merkle.d.ts.map +1 -0
  41. package/dist/merkle.js +226 -0
  42. package/dist/merkle.js.map +1 -0
  43. package/dist/schema.d.ts +18 -0
  44. package/dist/schema.d.ts.map +1 -0
  45. package/dist/schema.js +255 -0
  46. package/dist/schema.js.map +1 -0
  47. package/dist/trust.d.ts +100 -0
  48. package/dist/trust.d.ts.map +1 -0
  49. package/dist/trust.js +222 -0
  50. package/dist/trust.js.map +1 -0
  51. package/dist/types.d.ts +167 -11
  52. package/dist/types.d.ts.map +1 -1
  53. package/dist/types.js +42 -1
  54. package/dist/types.js.map +1 -1
  55. package/dist/wal.d.ts +69 -0
  56. package/dist/wal.d.ts.map +1 -0
  57. package/dist/wal.js +223 -0
  58. package/dist/wal.js.map +1 -0
  59. package/dist/witness.d.ts +293 -1
  60. package/dist/witness.d.ts.map +1 -1
  61. package/dist/witness.js +1234 -5
  62. package/dist/witness.js.map +1 -1
  63. package/package.json +7 -7
  64. package/templates/cost-conscious.yaml +35 -0
  65. package/templates/eu-ai-act-high-risk.yaml +56 -0
  66. package/templates/granite-sovereign.yaml +55 -0
  67. package/templates/minimal.yaml +38 -0
  68. package/templates/mythos-defense.yaml +65 -0
  69. package/templates/nist-ai-rmf.yaml +47 -0
  70. package/templates/owasp-agentic-top10.yaml +50 -0
package/dist/witness.js CHANGED
@@ -24,13 +24,158 @@ import { sha256Truncated, mintFingerprint, timestampMs } from "./fingerprint.js"
24
24
  import { extractPayloads, extractGatekeeperPayload, extractRevocationPayload, REVOCATION_REASONS } from "./clearing.js";
25
25
  import { signPayload } from "./signing.js";
26
26
  import { WitnessBuffer } from "./buffer.js";
27
+ import { WriteAheadLog } from "./wal.js";
27
28
  import { writeHandoffFiles } from "./handoff.js";
28
29
  import { wrapOpenAI } from "./adapters/openai.js";
29
30
  import { wrapAnthropic } from "./adapters/anthropic.js";
30
31
  import { wrapBedrock } from "./adapters/bedrock.js";
31
32
  import { wrapOllama, isOllamaClient } from "./adapters/ollama.js";
33
+ import { queryHardware as queryHw, topologyCode as topoCode, queryTPM as queryTpm, ZERO_PCR_HASH } from "./hardware.js";
34
+ import { queryEnvironment as queryEnv } from "./environment.js";
35
+ import { TrustRegistry, verifyCredential, signCredential, TRUST_LEVEL_NAMES, } from "./trust.js";
32
36
  import { createVercelOnFinish } from "./adapters/vercel-ai.js";
33
- import { loadConfig as loadConfigFromFile } from "./config.js";
37
+ import { QUANTIZATION_CODES, POLICY_CATEGORIES, BINDING_METHODS, APPROVAL_STATUS, PII_EVENT_TYPES } from "./types.js";
38
+ import { loadFullConfig, validatePolicy } from "./config.js";
39
+ import { MerkleAccumulator } from "./merkle.js";
40
+ // ── Chain Density Enforcement ──────────────────────────────────────────
41
+ function globToRegex(pattern) {
42
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&")
43
+ .replace(/\*/g, ".*")
44
+ .replace(/\?/g, ".");
45
+ return new RegExp("^" + escaped + "$");
46
+ }
47
+ function parseVelocity(spec) {
48
+ const parts = spec.split("/");
49
+ const limit = parseInt(parts[0], 10);
50
+ const windowS = parseInt(parts[1].replace("s", ""), 10);
51
+ return { limit, windowMs: windowS * 1000 };
52
+ }
53
+ export class PolicyViolationError extends Error {
54
+ violation;
55
+ constructor(violation) {
56
+ super(`Chain policy violation: ${violation.reason}`);
57
+ this.name = "PolicyViolationError";
58
+ this.violation = violation;
59
+ }
60
+ }
61
+ /**
62
+ * Chain density enforcement engine.
63
+ *
64
+ * Evaluates tool calls against rate limits, depth limits, allow/blocklists,
65
+ * and custom rules. All checks are in-memory, zero network calls.
66
+ * Instantiated from McpPolicyConfig by Witness.fromConfig().
67
+ */
68
+ export class ChainEnforcer {
69
+ velocityWindow = [];
70
+ velocityLimit = 0;
71
+ velocityWindowMs = 0;
72
+ chainDepth = 0;
73
+ maxChainDepth;
74
+ allowPatterns;
75
+ blockPatterns;
76
+ failSecure;
77
+ customRules;
78
+ lastToolName = null;
79
+ tokenCount = 0;
80
+ maxTokensPerSession;
81
+ _violations = [];
82
+ constructor(policy) {
83
+ if (policy.maxVelocity) {
84
+ const parsed = parseVelocity(policy.maxVelocity);
85
+ this.velocityLimit = parsed.limit;
86
+ this.velocityWindowMs = parsed.windowMs;
87
+ }
88
+ this.maxChainDepth = policy.maxChainDepth ?? Infinity;
89
+ this.maxTokensPerSession = policy.maxTokensPerSession ?? Infinity;
90
+ this.allowPatterns = (policy.toolAllowlist?.length)
91
+ ? policy.toolAllowlist.map(globToRegex)
92
+ : null;
93
+ this.blockPatterns = (policy.toolBlocklist ?? []).map(globToRegex);
94
+ this.failSecure = policy.failSecure ?? true;
95
+ this.customRules = (policy.rules ?? []).map((r) => ({
96
+ ...r,
97
+ regex: globToRegex(r.match),
98
+ }));
99
+ }
100
+ check(toolName) {
101
+ const now = Date.now();
102
+ // 1. Blocklist
103
+ for (const pattern of this.blockPatterns) {
104
+ if (pattern.test(toolName)) {
105
+ return this.violation("blocklist", toolName, "blocked", `Tool "${toolName}" is on the blocklist`, now);
106
+ }
107
+ }
108
+ // 2. Allowlist (skip if null = all allowed)
109
+ if (this.allowPatterns) {
110
+ const allowed = this.allowPatterns.some((p) => p.test(toolName));
111
+ if (!allowed) {
112
+ return this.violation("allowlist", toolName, "blocked", `Tool "${toolName}" is not on the allowlist`, now);
113
+ }
114
+ }
115
+ // 3. Velocity (sliding window)
116
+ if (this.velocityLimit > 0) {
117
+ const cutoff = now - this.velocityWindowMs;
118
+ while (this.velocityWindow.length > 0 && this.velocityWindow[0] <= cutoff) {
119
+ this.velocityWindow.shift();
120
+ }
121
+ if (this.velocityWindow.length >= this.velocityLimit) {
122
+ const action = this.failSecure ? "blocked" : "logged";
123
+ return this.violation("velocity", toolName, action, `Rate limit exceeded: ${this.velocityLimit} calls per ${this.velocityWindowMs / 1000}s`, now, { currentCount: this.velocityWindow.length, limit: this.velocityLimit });
124
+ }
125
+ this.velocityWindow.push(now);
126
+ }
127
+ // 4. Depth tracking
128
+ if (this.maxChainDepth < Infinity) {
129
+ if (toolName !== this.lastToolName && this.lastToolName !== null) {
130
+ this.chainDepth = 0;
131
+ }
132
+ this.chainDepth++;
133
+ this.lastToolName = toolName;
134
+ if (this.chainDepth > this.maxChainDepth) {
135
+ const action = this.failSecure ? "blocked" : "logged";
136
+ return this.violation("depth", toolName, action, `Chain depth ${this.chainDepth} exceeds max ${this.maxChainDepth}`, now, { currentDepth: this.chainDepth, maxDepth: this.maxChainDepth });
137
+ }
138
+ }
139
+ // 5. Token budget
140
+ if (this.maxTokensPerSession < Infinity && this.tokenCount >= this.maxTokensPerSession) {
141
+ const action = this.failSecure ? "blocked" : "logged";
142
+ const v = this.violation("token_budget", toolName, action, `Token budget exceeded: ${this.tokenCount} tokens consumed, limit is ${this.maxTokensPerSession}`, now, { currentTokens: this.tokenCount, limit: this.maxTokensPerSession });
143
+ this._violations.push(v);
144
+ return v;
145
+ }
146
+ // 6. Custom rules
147
+ for (const rule of this.customRules) {
148
+ if (rule.regex.test(toolName)) {
149
+ return this.violation(`custom:${rule.reason}`, toolName, rule.action === "block" ? "blocked" : "logged", rule.reason, now, rule.params);
150
+ }
151
+ }
152
+ return null;
153
+ }
154
+ resetDepth() {
155
+ this.chainDepth = 0;
156
+ this.lastToolName = null;
157
+ }
158
+ recordTokens(count) {
159
+ this.tokenCount += count;
160
+ }
161
+ resetTokens() {
162
+ this.tokenCount = 0;
163
+ }
164
+ get currentTokenCount() {
165
+ return this.tokenCount;
166
+ }
167
+ get violations() {
168
+ return this._violations;
169
+ }
170
+ clearViolations() {
171
+ this._violations = [];
172
+ }
173
+ violation(rule, toolName, action, reason, timestamp, context) {
174
+ const v = { rule, toolName, action, reason, timestamp, ...(context ? { context } : {}) };
175
+ this._violations.push(v);
176
+ return v;
177
+ }
178
+ }
34
179
  /**
35
180
  * Raised when strict (gatekeeper) mode blocks an inference due to
36
181
  * insufficient guardrails. The inference never reaches the AI model.
@@ -71,8 +216,128 @@ export class Witness {
71
216
  * Requires: `npm install yaml`
72
217
  */
73
218
  static fromConfig(path, overrides) {
74
- const config = loadConfigFromFile(path);
75
- return new Witness({ ...config, ...overrides });
219
+ const loaded = loadFullConfig(path);
220
+ const mergedOptions = { ...loaded.witnessOptions, ...overrides };
221
+ // Re-validate policy AFTER overrides to prevent silent downgrades
222
+ if (loaded.policy && overrides) {
223
+ validatePolicy(mergedOptions, {
224
+ require_signing: loaded.policy.requireSigning,
225
+ min_clearing_level: loaded.policy.minClearingLevel,
226
+ required_procedures: loaded.policy.requiredProcedures,
227
+ require_agent_id: loaded.policy.requireAgentId,
228
+ max_flush_interval: loaded.policy.maxFlushInterval,
229
+ require_jurisdiction: loaded.policy.requireJurisdiction,
230
+ });
231
+ }
232
+ const witness = new Witness(mergedOptions);
233
+ witness._configHash = loaded.configHash;
234
+ if (loaded.trustMesh) {
235
+ witness._configureTrustMesh(loaded.trustMesh);
236
+ }
237
+ if (loaded.hardware) {
238
+ witness._hardwareConfig = loaded.hardware;
239
+ if (loaded.hardware.requireAttestation) {
240
+ witness.witnessHardware();
241
+ }
242
+ }
243
+ if (loaded.densityPolicy) {
244
+ witness._densityPolicy = loaded.densityPolicy;
245
+ }
246
+ if (loaded.mcpPolicy) {
247
+ witness._mcpPolicy = loaded.mcpPolicy;
248
+ const p = loaded.mcpPolicy;
249
+ if (p.maxVelocity || p.maxChainDepth !== undefined || p.maxTokensPerSession !== undefined ||
250
+ p.toolAllowlist?.length || p.toolBlocklist?.length || p.rules?.length) {
251
+ witness._chainEnforcer = new ChainEnforcer(p);
252
+ }
253
+ }
254
+ if (loaded.merkle) {
255
+ witness._merkleConfig = loaded.merkle;
256
+ if (loaded.merkle.enabled) {
257
+ witness._merkleAccumulator = new MerkleAccumulator({
258
+ tenantId: loaded.witnessOptions.tenantId
259
+ ?? loaded.witnessOptions.tenant_id,
260
+ });
261
+ }
262
+ }
263
+ return witness;
264
+ }
265
+ _configHash;
266
+ _hardwareConfig;
267
+ _densityPolicy;
268
+ _mcpPolicy;
269
+ _merkleConfig;
270
+ _merkleAccumulator;
271
+ _chainEnforcer;
272
+ get configHash() {
273
+ return this._configHash;
274
+ }
275
+ /** Density policy from YAML config (null if not configured). */
276
+ get densityPolicy() {
277
+ return this._densityPolicy;
278
+ }
279
+ /** MCP tool witnessing policy from YAML config (null if not configured). */
280
+ get mcpPolicy() {
281
+ return this._mcpPolicy;
282
+ }
283
+ /** Merkle accumulator config from YAML config (null if not configured). */
284
+ get merkleConfig() {
285
+ return this._merkleConfig;
286
+ }
287
+ /** SDK-side Merkle accumulator (created when merkle.enabled is true). */
288
+ get merkleAccumulator() {
289
+ return this._merkleAccumulator;
290
+ }
291
+ /** Chain density enforcer (created when chain density fields are configured). */
292
+ get chainEnforcer() {
293
+ return this._chainEnforcer;
294
+ }
295
+ /** Record token usage against the chain enforcer's session budget. */
296
+ recordSessionTokens(count) {
297
+ if (this._chainEnforcer) {
298
+ this._chainEnforcer.recordTokens(count);
299
+ }
300
+ }
301
+ _recordChainViolation(violation) {
302
+ const record = {
303
+ modelId: violation.toolName,
304
+ modelHash: sha256Truncated(violation.toolName),
305
+ promptHash: sha256Truncated(violation.rule),
306
+ responseHash: sha256Truncated(violation.reason),
307
+ latencyMs: 0,
308
+ guardrailsActive: 0,
309
+ guardrailsRequired: 0,
310
+ guardrailPassed: false,
311
+ hasRefusal: true,
312
+ provider: "chain-enforcer",
313
+ guardrailNames: [],
314
+ toolName: violation.toolName,
315
+ toolCallId: `chain-${violation.timestamp}`,
316
+ };
317
+ this.record(record);
318
+ }
319
+ _configureTrustMesh(mesh) {
320
+ const registry = this.trustRegistry;
321
+ for (const t of mesh.trustedTenants)
322
+ registry.trustTenant(t);
323
+ for (const ta of mesh.trustedAgents)
324
+ registry.trustAgent(ta.tenant, ta.agent);
325
+ for (const a of mesh.denyAgents)
326
+ registry.denyAgent(a);
327
+ for (const t of mesh.denyTenants)
328
+ registry.denyTenant(t);
329
+ registry.setRequireSignature(mesh.requireSignature);
330
+ registry.setMinTrustLevel(mesh.minTrustLevel);
331
+ registry.setFreshnessWindow(mesh.freshnessWindow);
332
+ if (mesh.requiredProcedures.length > 0) {
333
+ registry.setRequiredProcedures(mesh.requiredProcedures);
334
+ }
335
+ for (const sk of mesh.signingKeys) {
336
+ registry.registerSigningKey(sk.agent, sk.key);
337
+ }
338
+ if (mesh.mode === "strict" && !mesh.requireSignature) {
339
+ registry.setRequireSignature(true);
340
+ }
76
341
  }
77
342
  constructor(options) {
78
343
  this._gatewayMode = options.gatewayMode ?? false;
@@ -115,10 +380,26 @@ export class Witness {
115
380
  jurisdiction: options.jurisdiction,
116
381
  legalBasis: options.legalBasis,
117
382
  purposeClass: options.purposeClass,
383
+ tokenBudget: options.tokenBudget,
118
384
  onFlush: options.onFlush,
119
385
  };
120
386
  this._strict = options.strict ?? false;
121
- this.buffer = new WitnessBuffer(this.config);
387
+ // WAL: crash-resilient buffer persistence + replay protection (patent pending)
388
+ let wal;
389
+ if (options.walPath) {
390
+ wal = new WriteAheadLog(this.config.tenantId, {
391
+ walDir: options.walPath,
392
+ replayWindow: options.replayWindow,
393
+ });
394
+ }
395
+ this.buffer = new WitnessBuffer(this.config, undefined, wal);
396
+ // Recover any unflushed payloads from a previous crash
397
+ if (wal) {
398
+ const recovered = wal.recover();
399
+ if (recovered.length > 0) {
400
+ this.buffer.enqueueMany(recovered);
401
+ }
402
+ }
122
403
  }
123
404
  /**
124
405
  * Wrap an AI client with transparent witnessing.
@@ -199,6 +480,16 @@ export class Witness {
199
480
  const name = toolName ?? fn.name ?? "anonymous";
200
481
  const self = this;
201
482
  const wrapper = function (...args) {
483
+ // Chain density enforcement -- BEFORE execution
484
+ if (self._chainEnforcer) {
485
+ const violation = self._chainEnforcer.check(name);
486
+ if (violation) {
487
+ if (violation.action === "blocked") {
488
+ throw new PolicyViolationError(violation);
489
+ }
490
+ self._recordChainViolation(violation);
491
+ }
492
+ }
202
493
  const callId = randomUUID().replace(/-/g, "").slice(0, 12);
203
494
  const start = performance.now();
204
495
  let succeeded = true;
@@ -433,6 +724,367 @@ export class Witness {
433
724
  }
434
725
  this.buffer.enqueueMany([payload]);
435
726
  }
727
+ /**
728
+ * Witness a RAG retrieval step (AI-RAG.1 + optional AI-RAG.2).
729
+ *
730
+ * Records what context chunks were retrieved and from which corpus.
731
+ * Chunk text is NEVER transmitted -- only SHA-256 hashes.
732
+ *
733
+ * Automatically emits AI-RAG.2 (Context Relevance) when
734
+ * similarityThreshold is set and chunks have similarityScore.
735
+ *
736
+ * @param options.chunks - Raw strings (auto-hashed) or RagChunk objects.
737
+ * @param options.corpusId - Identifier for the retrieval corpus/index.
738
+ * @param options.similarityThreshold - When set and chunks have scores,
739
+ * AI-RAG.2 is emitted alongside AI-RAG.1.
740
+ * @returns Array of WitnessPayload objects (1-2 payloads).
741
+ *
742
+ * @example
743
+ * witness.witnessRagContext({
744
+ * chunks: ["chunk text 1", "chunk text 2"],
745
+ * corpusId: "legal-docs-v3",
746
+ * });
747
+ */
748
+ witnessRagContext(options) {
749
+ const normalized = options.chunks.map((chunk) => {
750
+ if (typeof chunk === "string") {
751
+ return { contentHash: sha256Truncated(chunk) };
752
+ }
753
+ return chunk;
754
+ });
755
+ const payloads = [];
756
+ const policyHash = this.config.policyVersion
757
+ ? sha256Truncated(this.config.policyVersion, 12)
758
+ : undefined;
759
+ // --- AI-RAG.1: Context Retrieval Provenance ---
760
+ const [ts1, ep1] = timestampMs();
761
+ const fa1 = normalized.length;
762
+ const fb1 = options.corpusId ? 1 : 0;
763
+ const fc1 = 0;
764
+ const fp1 = mintFingerprint(this.config.tenantId, "AI-RAG.1", fa1, fb1, fc1, ts1);
765
+ const p1 = {
766
+ procedure_id: "AI-RAG.1",
767
+ factor_a: fa1,
768
+ factor_b: fb1,
769
+ factor_c: fc1,
770
+ clearing_level: this.config.clearingLevel,
771
+ anchor_fingerprint: fp1,
772
+ anchor_epoch: ep1,
773
+ fingerprint_timestamp_ms: ts1,
774
+ };
775
+ if (this.config.clearingLevel <= 1) {
776
+ p1.ai_model_id = options.embeddingModel ?? "rag-retrieval";
777
+ const ctx = {
778
+ provider: "rag",
779
+ chunk_count: normalized.length,
780
+ chunk_hashes: normalized.map((c) => c.contentHash),
781
+ };
782
+ if (options.corpusId)
783
+ ctx.corpus_id = options.corpusId;
784
+ if (options.corpusHash)
785
+ ctx.corpus_hash = options.corpusHash;
786
+ if (options.embeddingModel)
787
+ ctx.embedding_model = options.embeddingModel;
788
+ if (options.retrievalLatencyMs != null)
789
+ ctx.retrieval_latency_ms = options.retrievalLatencyMs;
790
+ if (options.topK != null)
791
+ ctx.top_k = options.topK;
792
+ p1.ai_context = ctx;
793
+ }
794
+ if (this.config.clearingLevel <= 2 && options.retrievalLatencyMs != null) {
795
+ p1.ai_latency_ms = options.retrievalLatencyMs;
796
+ }
797
+ this._applyOperationalMetadata(p1, policyHash);
798
+ payloads.push(p1);
799
+ // --- AI-RAG.2: Context Relevance (conditional) ---
800
+ const scoredChunks = normalized.filter((c) => c.similarityScore != null);
801
+ if (options.similarityThreshold != null && scoredChunks.length > 0) {
802
+ const scores = scoredChunks.map((c) => c.similarityScore);
803
+ const avgSim = scores.reduce((a, b) => a + b, 0) / scores.length;
804
+ const belowCount = scores.filter((s) => s < options.similarityThreshold).length;
805
+ const [ts2, ep2] = timestampMs();
806
+ const fa2 = Math.round(options.similarityThreshold * 1000);
807
+ const fb2 = Math.round(avgSim * 1000);
808
+ const fc2 = belowCount;
809
+ const fp2 = mintFingerprint(this.config.tenantId, "AI-RAG.2", fa2, fb2, fc2, ts2);
810
+ const p2 = {
811
+ procedure_id: "AI-RAG.2",
812
+ factor_a: fa2,
813
+ factor_b: fb2,
814
+ factor_c: fc2,
815
+ clearing_level: this.config.clearingLevel,
816
+ anchor_fingerprint: fp2,
817
+ anchor_epoch: ep2,
818
+ fingerprint_timestamp_ms: ts2,
819
+ };
820
+ if (this.config.clearingLevel <= 1) {
821
+ p2.ai_model_id = options.embeddingModel ?? "rag-retrieval";
822
+ p2.ai_context = {
823
+ provider: "rag",
824
+ similarity_threshold: options.similarityThreshold,
825
+ avg_similarity: Math.round(avgSim * 10000) / 10000,
826
+ min_similarity: Math.round(Math.min(...scores) * 10000) / 10000,
827
+ chunks_below_threshold: belowCount,
828
+ chunk_scores: scores.map((s) => Math.round(s * 10000) / 10000),
829
+ };
830
+ }
831
+ this._applyOperationalMetadata(p2, policyHash);
832
+ payloads.push(p2);
833
+ }
834
+ this.buffer.enqueueMany(payloads);
835
+ return payloads;
836
+ }
837
+ // -- Model Weight & Adapter Methods (AI-MDL.5/6/7) --
838
+ /**
839
+ * Hash a model weight file and return a ModelWeightInfo.
840
+ *
841
+ * Call ONCE at startup, not per-inference. File I/O is synchronous.
842
+ *
843
+ * @example
844
+ * const info = Witness.hashModelFile("/models/llama-3.1-70b.safetensors");
845
+ * witness.witnessModelWeights(info);
846
+ */
847
+ static hashModelFile(filePath, format) {
848
+ const { createHash } = require("node:crypto");
849
+ const fs = require("node:fs");
850
+ const path = require("node:path");
851
+ const hash = createHash("sha256");
852
+ const buf = fs.readFileSync(filePath);
853
+ hash.update(buf);
854
+ const ext = path.extname(filePath).replace(".", "");
855
+ return {
856
+ fileHash: hash.digest("hex"),
857
+ filePath,
858
+ fileSizeBytes: buf.length,
859
+ format: format ?? (ext || undefined),
860
+ };
861
+ }
862
+ /**
863
+ * Witness model weight file integrity (AI-MDL.5).
864
+ *
865
+ * @param weights - ModelWeightInfo (from hashModelFile() or manual),
866
+ * or file path string (blocks for large files -- prefer hashModelFile()).
867
+ * @param options.expectedHash - If provided and matches, PASS. If mismatches, FAIL.
868
+ */
869
+ witnessModelWeights(weights, options) {
870
+ const info = typeof weights === "string"
871
+ ? Witness.hashModelFile(weights)
872
+ : weights;
873
+ const match = options?.expectedHash ? info.fileHash === options.expectedHash : true;
874
+ const [ts, epoch] = timestampMs();
875
+ const fa = 1, fb = match ? 1 : 0, fc = 0;
876
+ const fp = mintFingerprint(this.config.tenantId, "AI-MDL.5", fa, fb, fc, ts);
877
+ const payload = {
878
+ procedure_id: "AI-MDL.5", factor_a: fa, factor_b: fb, factor_c: fc,
879
+ clearing_level: this.config.clearingLevel,
880
+ anchor_fingerprint: fp, anchor_epoch: epoch, fingerprint_timestamp_ms: ts,
881
+ };
882
+ if (this.config.clearingLevel <= 1) {
883
+ payload.ai_model_id = info.format ?? "model-weights";
884
+ const ctx = { provider: "model-weights", file_hash: info.fileHash };
885
+ if (info.filePath)
886
+ ctx.file_path = info.filePath;
887
+ if (info.fileSizeBytes != null)
888
+ ctx.file_size_bytes = info.fileSizeBytes;
889
+ if (info.format)
890
+ ctx.format = info.format;
891
+ if (options?.expectedHash)
892
+ ctx.expected_hash = options.expectedHash;
893
+ payload.ai_context = ctx;
894
+ }
895
+ const policyHash = this.config.policyVersion ? sha256Truncated(this.config.policyVersion, 12) : undefined;
896
+ this._applyOperationalMetadata(payload, policyHash);
897
+ this.buffer.enqueueMany([payload]);
898
+ return payload;
899
+ }
900
+ /**
901
+ * Witness active LoRA/QLoRA/PEFT adapter stack (AI-MDL.6).
902
+ */
903
+ witnessAdapterStack(adapters, baseModelId) {
904
+ const allVerified = adapters.length === 0 || adapters.every((a) => a.adapterHash);
905
+ const [ts, epoch] = timestampMs();
906
+ const fa = adapters.length, fb = allVerified ? 1 : 0, fc = 0;
907
+ const fp = mintFingerprint(this.config.tenantId, "AI-MDL.6", fa, fb, fc, ts);
908
+ const payload = {
909
+ procedure_id: "AI-MDL.6", factor_a: fa, factor_b: fb, factor_c: fc,
910
+ clearing_level: this.config.clearingLevel,
911
+ anchor_fingerprint: fp, anchor_epoch: epoch, fingerprint_timestamp_ms: ts,
912
+ };
913
+ if (this.config.clearingLevel <= 1) {
914
+ payload.ai_model_id = baseModelId ?? "unknown-base";
915
+ const adapterList = adapters.map((a) => {
916
+ const obj = { name: a.name, hash: a.adapterHash };
917
+ if (a.baseModel)
918
+ obj.base_model = a.baseModel;
919
+ return obj;
920
+ });
921
+ payload.ai_context = { provider: "adapter", adapters: adapterList };
922
+ if (baseModelId)
923
+ payload.ai_context.base_model_id = baseModelId;
924
+ }
925
+ const policyHash = this.config.policyVersion ? sha256Truncated(this.config.policyVersion, 12) : undefined;
926
+ this._applyOperationalMetadata(payload, policyHash);
927
+ this.buffer.enqueueMany([payload]);
928
+ return payload;
929
+ }
930
+ /**
931
+ * Witness model quantization method (AI-MDL.7).
932
+ *
933
+ * @param method - fp32, fp16, bf16, int8, int4, gptq, awq, gguf.
934
+ */
935
+ witnessQuantization(method, options) {
936
+ const code = QUANTIZATION_CODES[method.toLowerCase()] ?? 0;
937
+ const [ts, epoch] = timestampMs();
938
+ const fa = 1, fb = 1, fc = code;
939
+ const fp = mintFingerprint(this.config.tenantId, "AI-MDL.7", fa, fb, fc, ts);
940
+ const payload = {
941
+ procedure_id: "AI-MDL.7", factor_a: fa, factor_b: fb, factor_c: fc,
942
+ clearing_level: this.config.clearingLevel,
943
+ anchor_fingerprint: fp, anchor_epoch: epoch, fingerprint_timestamp_ms: ts,
944
+ };
945
+ if (this.config.clearingLevel <= 1) {
946
+ payload.ai_model_id = `quantization-${method.toLowerCase()}`;
947
+ const ctx = { provider: "quantization", method: method.toLowerCase() };
948
+ if (options?.bits != null)
949
+ ctx.bits = options.bits;
950
+ if (options?.groupSize != null)
951
+ ctx.group_size = options.groupSize;
952
+ payload.ai_context = ctx;
953
+ }
954
+ const policyHash = this.config.policyVersion ? sha256Truncated(this.config.policyVersion, 12) : undefined;
955
+ this._applyOperationalMetadata(payload, policyHash);
956
+ this.buffer.enqueueMany([payload]);
957
+ return payload;
958
+ }
959
+ // -- Procedural Knowledge / Skills Methods (AI-SKILL.1/2/3) --
960
+ /**
961
+ * Witness active skill/tool/plugin manifest (AI-SKILL.1).
962
+ *
963
+ * @param skills - Skill name strings (auto-hashed) or SkillInfo objects.
964
+ * @param options.expectedManifestHash - Expected hash of the full manifest.
965
+ *
966
+ * @example
967
+ * witness.witnessSkillManifest(["code_exec", "web_search", "file_read"]);
968
+ */
969
+ witnessSkillManifest(skills, options) {
970
+ const normalized = skills.map((s) => typeof s === "string" ? { name: s, skillHash: sha256Truncated(s) } : s);
971
+ const manifestParts = normalized
972
+ .map((si) => si.skillHash ?? sha256Truncated(si.name))
973
+ .sort();
974
+ const computedManifest = sha256Truncated(manifestParts.join(":"));
975
+ const match = options?.expectedManifestHash ? computedManifest === options.expectedManifestHash : true;
976
+ const [ts, epoch] = timestampMs();
977
+ const fa = normalized.length, fb = match ? 1 : 0, fc = 0;
978
+ const fp = mintFingerprint(this.config.tenantId, "AI-SKILL.1", fa, fb, fc, ts);
979
+ const payload = {
980
+ procedure_id: "AI-SKILL.1", factor_a: fa, factor_b: fb, factor_c: fc,
981
+ clearing_level: this.config.clearingLevel,
982
+ anchor_fingerprint: fp, anchor_epoch: epoch, fingerprint_timestamp_ms: ts,
983
+ };
984
+ if (this.config.clearingLevel <= 1) {
985
+ payload.ai_model_id = "skill-manifest";
986
+ payload.ai_context = {
987
+ provider: "skill-manifest",
988
+ skills: normalized.map((si) => {
989
+ const obj = { name: si.name };
990
+ if (si.version)
991
+ obj.version = si.version;
992
+ if (si.skillHash)
993
+ obj.hash = si.skillHash;
994
+ return obj;
995
+ }),
996
+ manifest_hash: computedManifest,
997
+ };
998
+ }
999
+ const policyHash = this.config.policyVersion ? sha256Truncated(this.config.policyVersion, 12) : undefined;
1000
+ this._applyOperationalMetadata(payload, policyHash);
1001
+ this.buffer.enqueueMany([payload]);
1002
+ return payload;
1003
+ }
1004
+ /**
1005
+ * Witness persistent memory sources influencing a decision (AI-SKILL.2).
1006
+ */
1007
+ witnessMemoryContext(sources) {
1008
+ const allIdentified = sources.length > 0 && sources.every((s) => s.sourceId || s.contentHash);
1009
+ const [ts, epoch] = timestampMs();
1010
+ const fa = sources.length, fb = allIdentified ? 1 : 0, fc = 0;
1011
+ const fp = mintFingerprint(this.config.tenantId, "AI-SKILL.2", fa, fb, fc, ts);
1012
+ const payload = {
1013
+ procedure_id: "AI-SKILL.2", factor_a: fa, factor_b: fb, factor_c: fc,
1014
+ clearing_level: this.config.clearingLevel,
1015
+ anchor_fingerprint: fp, anchor_epoch: epoch, fingerprint_timestamp_ms: ts,
1016
+ };
1017
+ if (this.config.clearingLevel <= 1) {
1018
+ payload.ai_model_id = "memory-context";
1019
+ payload.ai_context = {
1020
+ provider: "memory",
1021
+ sources: sources.map((s) => {
1022
+ const obj = { type: s.sourceType };
1023
+ if (s.sourceId)
1024
+ obj.id = s.sourceId;
1025
+ if (s.contentHash)
1026
+ obj.hash = s.contentHash;
1027
+ return obj;
1028
+ }),
1029
+ total_sources: sources.length,
1030
+ };
1031
+ }
1032
+ const policyHash = this.config.policyVersion ? sha256Truncated(this.config.policyVersion, 12) : undefined;
1033
+ this._applyOperationalMetadata(payload, policyHash);
1034
+ this.buffer.enqueueMany([payload]);
1035
+ return payload;
1036
+ }
1037
+ /**
1038
+ * Witness RLHF/DPO reward model binding (AI-SKILL.3).
1039
+ */
1040
+ witnessRewardModel(modelId, options) {
1041
+ const identified = Boolean(modelId?.trim());
1042
+ const [ts, epoch] = timestampMs();
1043
+ const fa = 1, fb = identified ? 1 : 0, fc = 0;
1044
+ const fp = mintFingerprint(this.config.tenantId, "AI-SKILL.3", fa, fb, fc, ts);
1045
+ const payload = {
1046
+ procedure_id: "AI-SKILL.3", factor_a: fa, factor_b: fb, factor_c: fc,
1047
+ clearing_level: this.config.clearingLevel,
1048
+ anchor_fingerprint: fp, anchor_epoch: epoch, fingerprint_timestamp_ms: ts,
1049
+ };
1050
+ if (this.config.clearingLevel <= 1) {
1051
+ payload.ai_model_id = modelId;
1052
+ const ctx = { provider: "reward-model", model_id: modelId };
1053
+ if (options?.modelHash)
1054
+ ctx.model_hash = options.modelHash;
1055
+ if (options?.method)
1056
+ ctx.method = options.method;
1057
+ payload.ai_context = ctx;
1058
+ }
1059
+ const policyHash = this.config.policyVersion ? sha256Truncated(this.config.policyVersion, 12) : undefined;
1060
+ this._applyOperationalMetadata(payload, policyHash);
1061
+ this.buffer.enqueueMany([payload]);
1062
+ return payload;
1063
+ }
1064
+ /** Apply operational metadata (agent_id, signing, CJT fields) to a payload. */
1065
+ _applyOperationalMetadata(payload, policyHash) {
1066
+ if (payload.procedure_id)
1067
+ this._witnessedProcedures.add(payload.procedure_id);
1068
+ if (this.config.agentId)
1069
+ payload.agent_id = this.config.agentId;
1070
+ if (this.config.cycleId)
1071
+ payload.cycle_id = this.config.cycleId;
1072
+ if (this.config.jurisdiction)
1073
+ payload.jurisdiction = this.config.jurisdiction;
1074
+ if (this.config.legalBasis)
1075
+ payload.legal_basis = this.config.legalBasis;
1076
+ if (this.config.purposeClass)
1077
+ payload.purpose_class = this.config.purposeClass;
1078
+ if (policyHash)
1079
+ payload.policy_version_hash = policyHash;
1080
+ if (this.config.signingKey) {
1081
+ payload.payload_signature = signPayload(this.config.signingKey, payload.anchor_fingerprint, this.config.agentId);
1082
+ if (this.config.signingKeyId)
1083
+ payload.signing_key_id = this.config.signingKeyId;
1084
+ if (this.config.signingKeyVersion !== undefined)
1085
+ payload.signing_key_version = this.config.signingKeyVersion;
1086
+ }
1087
+ }
436
1088
  /**
437
1089
  * Revoke a previously-issued witness anchor (AI-REV.1).
438
1090
  *
@@ -445,11 +1097,588 @@ export class Witness {
445
1097
  * error_correction, or unspecified.
446
1098
  * @returns The fingerprint of the revocation anchor itself.
447
1099
  */
1100
+ /**
1101
+ * Witness accelerator hardware inventory (AI-HW.1).
1102
+ *
1103
+ * Records what GPU/accelerator hardware is present. Call ONCE at
1104
+ * service startup, not per-inference. If no GPUs are detectable,
1105
+ * returns a payload with factor_a=0, factor_b=0 (graceful no-op).
1106
+ *
1107
+ * @param options.snapshot - Pre-computed HardwareSnapshot (from queryHardware()).
1108
+ * If omitted, auto-detects via nvidia-smi.
1109
+ * @param options.expectedTopology - Expected topology (e.g., "NVL72").
1110
+ * If provided and doesn't match detected topology, factor_b=0.
1111
+ */
1112
+ witnessHardware(options) {
1113
+ const snapshot = options?.snapshot ?? queryHw();
1114
+ const gpuCount = snapshot.gpus.length;
1115
+ let allHealthy = gpuCount > 0;
1116
+ if (options?.expectedTopology && snapshot.topology !== options.expectedTopology) {
1117
+ allHealthy = false;
1118
+ }
1119
+ const fa = gpuCount;
1120
+ const fb = allHealthy ? 1 : 0;
1121
+ const fc = topoCode(snapshot.topology);
1122
+ const [ts, epoch] = timestampMs();
1123
+ const fp = mintFingerprint(this.config.tenantId, "AI-HW.1", fa, fb, fc, ts);
1124
+ const payload = {
1125
+ procedure_id: "AI-HW.1",
1126
+ factor_a: fa,
1127
+ factor_b: fb,
1128
+ factor_c: fc,
1129
+ clearing_level: this.config.clearingLevel,
1130
+ anchor_fingerprint: fp,
1131
+ anchor_epoch: epoch,
1132
+ fingerprint_timestamp_ms: ts,
1133
+ };
1134
+ if (this.config.clearingLevel <= 1) {
1135
+ payload.ai_model_id = `hw-${snapshot.topology}`;
1136
+ const ctx = {
1137
+ provider: "nvidia-hw",
1138
+ topology: snapshot.topology,
1139
+ interconnect: snapshot.interconnect,
1140
+ total_memory_mb: snapshot.totalMemoryMb,
1141
+ gpu_count: gpuCount,
1142
+ hostname_hash: snapshot.hostnameHash,
1143
+ };
1144
+ if (snapshot.driverVersion)
1145
+ ctx.driver_version = snapshot.driverVersion;
1146
+ if (snapshot.cudaVersion)
1147
+ ctx.cuda_version = snapshot.cudaVersion;
1148
+ if (snapshot.gpus.length > 0) {
1149
+ ctx.gpus = snapshot.gpus.map((g) => ({
1150
+ name: g.name,
1151
+ memory_mb: g.memoryMb,
1152
+ bus_id_hash: g.busIdHash,
1153
+ uuid_hash: g.uuidHash,
1154
+ }));
1155
+ }
1156
+ if (options?.expectedTopology)
1157
+ ctx.expected_topology = options.expectedTopology;
1158
+ payload.ai_context = ctx;
1159
+ }
1160
+ const policyHash = this.config.policyVersion
1161
+ ? sha256Truncated(this.config.policyVersion, 12)
1162
+ : undefined;
1163
+ this._applyOperationalMetadata(payload, policyHash);
1164
+ this.buffer.enqueueMany([payload]);
1165
+ return payload;
1166
+ }
1167
+ /**
1168
+ * Witness TPM 2.0 platform attestation (AI-HW.3).
1169
+ *
1170
+ * Reads PCR registers 0-7 via tpm2-tools and mints an anchor proving
1171
+ * host firmware integrity. All raw PCR digests are SHA-256 hashed
1172
+ * before leaving the module.
1173
+ *
1174
+ * @param options.snapshot - Pre-computed TPMSnapshot (from queryTPM()).
1175
+ */
1176
+ witnessTPMAttestation(options) {
1177
+ const snapshot = options?.snapshot ?? queryTpm();
1178
+ const pcrCount = snapshot.pcrs.length;
1179
+ const allNonZero = pcrCount > 0 && snapshot.pcrs.every((pcr) => pcr.digestHash !== ZERO_PCR_HASH);
1180
+ const fa = pcrCount;
1181
+ const fb = allNonZero ? 1 : 0;
1182
+ const fc = 0; // reserved
1183
+ const [ts, epoch] = timestampMs();
1184
+ const fp = mintFingerprint(this.config.tenantId, "AI-HW.3", fa, fb, fc, ts);
1185
+ const payload = {
1186
+ procedure_id: "AI-HW.3",
1187
+ factor_a: fa,
1188
+ factor_b: fb,
1189
+ factor_c: fc,
1190
+ clearing_level: this.config.clearingLevel,
1191
+ anchor_fingerprint: fp,
1192
+ anchor_epoch: epoch,
1193
+ fingerprint_timestamp_ms: ts,
1194
+ };
1195
+ if (this.config.clearingLevel <= 1) {
1196
+ payload.ai_model_id = "tpm-attestation";
1197
+ const ctx = {
1198
+ provider: "tpm-2.0",
1199
+ pcr_count: pcrCount,
1200
+ all_non_zero: allNonZero,
1201
+ manufacturer_hash: snapshot.manufacturer,
1202
+ firmware_hash: snapshot.firmwareVersion,
1203
+ endorsement_key_hash: snapshot.endorsementKeyHash,
1204
+ hostname_hash: snapshot.hostnameHash,
1205
+ };
1206
+ if (snapshot.pcrs.length > 0) {
1207
+ ctx.pcrs = snapshot.pcrs.map((pcr) => ({
1208
+ index: pcr.index,
1209
+ bank: pcr.bank,
1210
+ digest_hash: pcr.digestHash,
1211
+ }));
1212
+ }
1213
+ payload.ai_context = ctx;
1214
+ }
1215
+ const policyHash = this.config.policyVersion
1216
+ ? sha256Truncated(this.config.policyVersion, 12)
1217
+ : undefined;
1218
+ this._applyOperationalMetadata(payload, policyHash);
1219
+ this.buffer.enqueueMany([payload]);
1220
+ return payload;
1221
+ }
1222
+ // ── Environment (AI-ENV.1 / AI-ENV.2) ────────────────────────────
1223
+ /**
1224
+ * Witness thermal integrity of the compute environment (AI-ENV.1).
1225
+ *
1226
+ * Call at service startup or on a periodic schedule, not per-inference.
1227
+ * Auto-detects Linux thermal zones. Pass manual values for XFRA/Span nodes.
1228
+ *
1229
+ * @param options.temperatureCelsius - Measured temperature (auto-detected if omitted)
1230
+ * @param options.thresholdCelsius - Safe maximum (default 85)
1231
+ * @param options.snapshot - Pre-computed EnvironmentSnapshot
1232
+ * @param options.nodeType - Node type: datacenter, edge, residential, mobile
1233
+ */
1234
+ witnessEnvironment(options) {
1235
+ const snapshot = options?.snapshot ?? queryEnv();
1236
+ const temp = options?.temperatureCelsius ?? snapshot.temperatureCelsius;
1237
+ const threshold = options?.thresholdCelsius ?? 85;
1238
+ const nodeType = options?.nodeType ?? snapshot.nodeType ?? "unknown";
1239
+ const fa = Math.round(Number(temp) || 0);
1240
+ const fb = Math.round(Number(threshold) || 85);
1241
+ const fc = fa <= fb ? 1 : 0;
1242
+ const [ts, epoch] = timestampMs();
1243
+ const fp = mintFingerprint(this.config.tenantId, "AI-ENV.1", fa, fb, fc, ts);
1244
+ const payload = {
1245
+ procedure_id: "AI-ENV.1",
1246
+ factor_a: fa, factor_b: fb, factor_c: fc,
1247
+ clearing_level: this.config.clearingLevel,
1248
+ anchor_fingerprint: fp, anchor_epoch: epoch, fingerprint_timestamp_ms: ts,
1249
+ };
1250
+ if (this.config.clearingLevel <= 1) {
1251
+ payload.ai_model_id = `env-thermal-${nodeType}`;
1252
+ payload.ai_context = {
1253
+ provider: "env-telemetry",
1254
+ node_type: nodeType,
1255
+ temperature_celsius: temp,
1256
+ threshold_celsius: threshold,
1257
+ thermal_zones: snapshot.thermalZones,
1258
+ hostname_hash: snapshot.hostnameHash,
1259
+ };
1260
+ }
1261
+ const policyHash = this.config.policyVersion
1262
+ ? sha256Truncated(this.config.policyVersion, 12)
1263
+ : undefined;
1264
+ this._applyOperationalMetadata(payload, policyHash);
1265
+ this.buffer.enqueueMany([payload]);
1266
+ return payload;
1267
+ }
1268
+ /**
1269
+ * Witness power integrity of the compute environment (AI-ENV.2).
1270
+ *
1271
+ * Call at service startup or on a periodic schedule, not per-inference.
1272
+ * Pass manual values from Span panel API, IPMI, or other power monitoring.
1273
+ *
1274
+ * @param options.powerWatts - Current power draw in watts
1275
+ * @param options.capacityWatts - Total available capacity in watts
1276
+ * @param options.throttled - Whether power throttling is active
1277
+ * @param options.snapshot - Pre-computed EnvironmentSnapshot
1278
+ * @param options.nodeType - Node type: datacenter, edge, residential, mobile
1279
+ */
1280
+ witnessEnergyDraw(options) {
1281
+ const snapshot = options?.snapshot ?? queryEnv();
1282
+ const power = Number(options?.powerWatts ?? snapshot.powerWatts) || 0;
1283
+ const capacity = Number(options?.capacityWatts ?? 0) || 0;
1284
+ const headroom = Math.max(0, capacity - power);
1285
+ const throttled = options?.throttled ?? false;
1286
+ const nodeType = options?.nodeType ?? snapshot.nodeType ?? "unknown";
1287
+ const fa = Math.round(power);
1288
+ const fb = Math.round(headroom);
1289
+ const fc = throttled ? 1 : 0;
1290
+ const [ts, epoch] = timestampMs();
1291
+ const fp = mintFingerprint(this.config.tenantId, "AI-ENV.2", fa, fb, fc, ts);
1292
+ const payload = {
1293
+ procedure_id: "AI-ENV.2",
1294
+ factor_a: fa, factor_b: fb, factor_c: fc,
1295
+ clearing_level: this.config.clearingLevel,
1296
+ anchor_fingerprint: fp, anchor_epoch: epoch, fingerprint_timestamp_ms: ts,
1297
+ };
1298
+ if (this.config.clearingLevel <= 1) {
1299
+ payload.ai_model_id = `env-power-${nodeType}`;
1300
+ payload.ai_context = {
1301
+ provider: "env-telemetry",
1302
+ node_type: nodeType,
1303
+ power_watts: power,
1304
+ capacity_watts: capacity,
1305
+ headroom_watts: headroom,
1306
+ throttled,
1307
+ power_domains: snapshot.powerDomains,
1308
+ hostname_hash: snapshot.hostnameHash,
1309
+ };
1310
+ }
1311
+ const policyHash = this.config.policyVersion
1312
+ ? sha256Truncated(this.config.policyVersion, 12)
1313
+ : undefined;
1314
+ this._applyOperationalMetadata(payload, policyHash);
1315
+ this.buffer.enqueueMany([payload]);
1316
+ return payload;
1317
+ }
1318
+ // ── Chain, Violation, Charter, Registry, Reviewer, Safe State ───
1319
+ /**
1320
+ * Witness a multi-agent chain handoff (AI-CHAIN.1).
1321
+ */
1322
+ witnessChainHandoff(depth, targetAgent, options) {
1323
+ const accepted = options?.accepted ?? true;
1324
+ const [ts, epoch] = timestampMs();
1325
+ const fa = depth;
1326
+ const fb = this.config.cycleId ? 1 : 0;
1327
+ const fc = accepted ? 1 : 0;
1328
+ const fp = mintFingerprint(this.config.tenantId, "AI-CHAIN.1", fa, fb, fc, ts);
1329
+ const payload = {
1330
+ procedure_id: "AI-CHAIN.1", factor_a: fa, factor_b: fb, factor_c: fc,
1331
+ clearing_level: this.config.clearingLevel,
1332
+ anchor_fingerprint: fp, anchor_epoch: epoch, fingerprint_timestamp_ms: ts,
1333
+ };
1334
+ if (this.config.clearingLevel <= 1) {
1335
+ payload.ai_model_id = targetAgent;
1336
+ payload.ai_context = {
1337
+ provider: "chain", target_agent: targetAgent, depth, accepted,
1338
+ };
1339
+ }
1340
+ const policyHash = this.config.policyVersion ? sha256Truncated(this.config.policyVersion, 12) : undefined;
1341
+ this._applyOperationalMetadata(payload, policyHash);
1342
+ this.buffer.enqueueMany([payload]);
1343
+ return payload;
1344
+ }
1345
+ /**
1346
+ * Witness a policy violation (AI-VIO.1).
1347
+ */
1348
+ witnessViolation(severity, description, options) {
1349
+ const autoDetected = options?.autoDetected ?? false;
1350
+ const policyCategory = options?.policyCategory ?? "unspecified";
1351
+ const [ts, epoch] = timestampMs();
1352
+ const fa = Math.max(1, Math.min(4, severity));
1353
+ const fb = autoDetected ? 1 : 0;
1354
+ const fc = POLICY_CATEGORIES[policyCategory] ?? 0;
1355
+ const fp = mintFingerprint(this.config.tenantId, "AI-VIO.1", fa, fb, fc, ts);
1356
+ const payload = {
1357
+ procedure_id: "AI-VIO.1", factor_a: fa, factor_b: fb, factor_c: fc,
1358
+ clearing_level: this.config.clearingLevel,
1359
+ anchor_fingerprint: fp, anchor_epoch: epoch, fingerprint_timestamp_ms: ts,
1360
+ };
1361
+ if (this.config.clearingLevel <= 1) {
1362
+ payload.ai_model_id = `violation-sev${fa}`;
1363
+ payload.ai_context = {
1364
+ provider: "violation", severity: fa, description, auto_detected: autoDetected, policy_category: policyCategory,
1365
+ };
1366
+ }
1367
+ const policyHash = this.config.policyVersion ? sha256Truncated(this.config.policyVersion, 12) : undefined;
1368
+ this._applyOperationalMetadata(payload, policyHash);
1369
+ this.buffer.enqueueMany([payload]);
1370
+ return payload;
1371
+ }
1372
+ /**
1373
+ * Witness an agent charter or system prompt hash (AI-CHR.1).
1374
+ */
1375
+ witnessCharter(options) {
1376
+ if (!options.charterText && !options.charterHash) {
1377
+ throw new Error("Provide charterText or charterHash");
1378
+ }
1379
+ const computed = options.charterHash ?? sha256Truncated(options.charterText);
1380
+ const match = options.expectedHash ? computed === options.expectedHash : true;
1381
+ const [ts, epoch] = timestampMs();
1382
+ const fa = 1, fb = match ? 1 : 0, fc = 0;
1383
+ const fp = mintFingerprint(this.config.tenantId, "AI-CHR.1", fa, fb, fc, ts);
1384
+ const payload = {
1385
+ procedure_id: "AI-CHR.1", factor_a: fa, factor_b: fb, factor_c: fc,
1386
+ clearing_level: this.config.clearingLevel,
1387
+ anchor_fingerprint: fp, anchor_epoch: epoch, fingerprint_timestamp_ms: ts,
1388
+ };
1389
+ if (this.config.clearingLevel <= 1) {
1390
+ payload.ai_model_id = "charter";
1391
+ const ctx = { provider: "charter", charter_hash: computed };
1392
+ if (options.expectedHash)
1393
+ ctx.expected_hash = options.expectedHash;
1394
+ payload.ai_context = ctx;
1395
+ }
1396
+ const policyHash = this.config.policyVersion ? sha256Truncated(this.config.policyVersion, 12) : undefined;
1397
+ this._applyOperationalMetadata(payload, policyHash);
1398
+ this.buffer.enqueueMany([payload]);
1399
+ return payload;
1400
+ }
1401
+ /**
1402
+ * Witness a model registry check (AI-MDL.8).
1403
+ */
1404
+ witnessModelRegistry(modelId, registryId, options) {
1405
+ const found = options?.found ?? true;
1406
+ const status = options?.status ?? "approved";
1407
+ const [ts, epoch] = timestampMs();
1408
+ const fa = 1, fb = found ? 1 : 0, fc = APPROVAL_STATUS[status] ?? 0;
1409
+ const fp = mintFingerprint(this.config.tenantId, "AI-MDL.8", fa, fb, fc, ts);
1410
+ const payload = {
1411
+ procedure_id: "AI-MDL.8", factor_a: fa, factor_b: fb, factor_c: fc,
1412
+ clearing_level: this.config.clearingLevel,
1413
+ anchor_fingerprint: fp, anchor_epoch: epoch, fingerprint_timestamp_ms: ts,
1414
+ };
1415
+ if (this.config.clearingLevel <= 1) {
1416
+ payload.ai_model_id = modelId;
1417
+ payload.ai_context = {
1418
+ provider: "model-registry", model_id: modelId, registry_id: registryId, found, status,
1419
+ };
1420
+ }
1421
+ const policyHash = this.config.policyVersion ? sha256Truncated(this.config.policyVersion, 12) : undefined;
1422
+ this._applyOperationalMetadata(payload, policyHash);
1423
+ this.buffer.enqueueMany([payload]);
1424
+ return payload;
1425
+ }
1426
+ /**
1427
+ * Witness reviewer identity binding (AI-HITL.3).
1428
+ */
1429
+ witnessReviewerIdentity(required, actual, options) {
1430
+ const method = options?.method ?? "session";
1431
+ const [ts, epoch] = timestampMs();
1432
+ const fa = required, fb = actual, fc = BINDING_METHODS[method] ?? 0;
1433
+ const fp = mintFingerprint(this.config.tenantId, "AI-HITL.3", fa, fb, fc, ts);
1434
+ const payload = {
1435
+ procedure_id: "AI-HITL.3", factor_a: fa, factor_b: fb, factor_c: fc,
1436
+ clearing_level: this.config.clearingLevel,
1437
+ anchor_fingerprint: fp, anchor_epoch: epoch, fingerprint_timestamp_ms: ts,
1438
+ };
1439
+ if (this.config.clearingLevel <= 1) {
1440
+ payload.ai_model_id = "reviewer-identity";
1441
+ const ctx = {
1442
+ provider: "reviewer", required, actual, method,
1443
+ };
1444
+ if (options?.reviewerIdHash)
1445
+ ctx.reviewer_id_hash = options.reviewerIdHash;
1446
+ payload.ai_context = ctx;
1447
+ }
1448
+ const policyHash = this.config.policyVersion ? sha256Truncated(this.config.policyVersion, 12) : undefined;
1449
+ this._applyOperationalMetadata(payload, policyHash);
1450
+ this.buffer.enqueueMany([payload]);
1451
+ return payload;
1452
+ }
1453
+ /**
1454
+ * Witness safe state attestation (AI-SAFE.1).
1455
+ */
1456
+ witnessSafeState(options) {
1457
+ const mechanismExists = options?.mechanismExists ?? true;
1458
+ const safeStateConfirmed = options?.safeStateConfirmed ?? true;
1459
+ const [ts, epoch] = timestampMs();
1460
+ const fa = 1, fb = mechanismExists ? 1 : 0, fc = safeStateConfirmed ? 1 : 0;
1461
+ const fp = mintFingerprint(this.config.tenantId, "AI-SAFE.1", fa, fb, fc, ts);
1462
+ const payload = {
1463
+ procedure_id: "AI-SAFE.1", factor_a: fa, factor_b: fb, factor_c: fc,
1464
+ clearing_level: this.config.clearingLevel,
1465
+ anchor_fingerprint: fp, anchor_epoch: epoch, fingerprint_timestamp_ms: ts,
1466
+ };
1467
+ if (this.config.clearingLevel <= 1) {
1468
+ payload.ai_model_id = "safe-state";
1469
+ const ctx = {
1470
+ provider: "safe-state", mechanism_exists: mechanismExists, safe_state_confirmed: safeStateConfirmed,
1471
+ };
1472
+ if (options?.mechanismType)
1473
+ ctx.mechanism_type = options.mechanismType;
1474
+ payload.ai_context = ctx;
1475
+ }
1476
+ const policyHash = this.config.policyVersion ? sha256Truncated(this.config.policyVersion, 12) : undefined;
1477
+ this._applyOperationalMetadata(payload, policyHash);
1478
+ this.buffer.enqueueMany([payload]);
1479
+ return payload;
1480
+ }
1481
+ // ── Training Data (AI-DATA.3 / AI-DATA.4) ──────────────────────
1482
+ /**
1483
+ * Witness training dataset summary statistics (AI-DATA.3).
1484
+ */
1485
+ witnessTrainingStats(rowCount, featureCount, options) {
1486
+ const [ts, epoch] = timestampMs();
1487
+ const fa = rowCount;
1488
+ const fb = featureCount;
1489
+ const fc = options?.classBalanceRatio != null ? Math.floor(options.classBalanceRatio * 1000) : 0;
1490
+ const fp = mintFingerprint(this.config.tenantId, "AI-DATA.3", fa, fb, fc, ts);
1491
+ const payload = {
1492
+ procedure_id: "AI-DATA.3", factor_a: fa, factor_b: fb, factor_c: fc,
1493
+ clearing_level: this.config.clearingLevel,
1494
+ anchor_fingerprint: fp, anchor_epoch: epoch, fingerprint_timestamp_ms: ts,
1495
+ };
1496
+ if (this.config.clearingLevel <= 1) {
1497
+ payload.ai_model_id = "training-stats";
1498
+ const ctx = {
1499
+ provider: "training-stats", row_count: rowCount, feature_count: featureCount,
1500
+ };
1501
+ if (options?.classBalanceRatio != null)
1502
+ ctx.class_balance_ratio = options.classBalanceRatio;
1503
+ if (options?.distributionHash)
1504
+ ctx.distribution_hash = options.distributionHash;
1505
+ if (options?.classLabels)
1506
+ ctx.class_labels = options.classLabels;
1507
+ if (options?.summary)
1508
+ ctx.summary = options.summary;
1509
+ payload.ai_context = ctx;
1510
+ }
1511
+ const policyHash = this.config.policyVersion ? sha256Truncated(this.config.policyVersion, 12) : undefined;
1512
+ this._applyOperationalMetadata(payload, policyHash);
1513
+ this.buffer.enqueueMany([payload]);
1514
+ return payload;
1515
+ }
1516
+ /**
1517
+ * Witness a training data PII lifecycle event (AI-DATA.4).
1518
+ */
1519
+ witnessTrainingPiiLifecycle(recordsAffected, options) {
1520
+ const completed = options?.completed ?? true;
1521
+ const eventType = options?.eventType ?? "unspecified";
1522
+ const [ts, epoch] = timestampMs();
1523
+ const fa = recordsAffected;
1524
+ const fb = completed ? 1 : 0;
1525
+ const fc = PII_EVENT_TYPES[eventType] ?? 0;
1526
+ const fp = mintFingerprint(this.config.tenantId, "AI-DATA.4", fa, fb, fc, ts);
1527
+ const payload = {
1528
+ procedure_id: "AI-DATA.4", factor_a: fa, factor_b: fb, factor_c: fc,
1529
+ clearing_level: this.config.clearingLevel,
1530
+ anchor_fingerprint: fp, anchor_epoch: epoch, fingerprint_timestamp_ms: ts,
1531
+ };
1532
+ if (this.config.clearingLevel <= 1) {
1533
+ payload.ai_model_id = "pii-lifecycle";
1534
+ const ctx = {
1535
+ provider: "pii-lifecycle", event_type: eventType,
1536
+ records_affected: recordsAffected, completed,
1537
+ };
1538
+ if (options?.datasetId)
1539
+ ctx.dataset_id = options.datasetId;
1540
+ if (options?.scope)
1541
+ ctx.scope = options.scope;
1542
+ payload.ai_context = ctx;
1543
+ }
1544
+ const policyHash = this.config.policyVersion ? sha256Truncated(this.config.policyVersion, 12) : undefined;
1545
+ this._applyOperationalMetadata(payload, policyHash);
1546
+ this.buffer.enqueueMany([payload]);
1547
+ return payload;
1548
+ }
1549
+ // ── Bias Assessment (AI-FAIR.3) ─────────────────────────────────
1550
+ /**
1551
+ * Witness a bias assessment (AI-FAIR.3).
1552
+ * Records that a bias assessment was conducted, how many protected
1553
+ * attributes were tested, and whether all fairness thresholds were met.
1554
+ *
1555
+ * @param protectedAttributeCount - Number of demographic dimensions tested
1556
+ * @param allThresholdsMet - true if all fairness thresholds passed
1557
+ * @param options.maxDisparityPct - Worst-case disparity percentage (0-100)
1558
+ * @param options.methodology - Assessment methodology name
1559
+ * @param options.protectedAttributes - List of protected attributes tested
1560
+ */
1561
+ witnessBiasAssessment(protectedAttributeCount, allThresholdsMet, options) {
1562
+ const [ts, epoch] = timestampMs();
1563
+ const fa = protectedAttributeCount;
1564
+ const fb = allThresholdsMet ? 1 : 0;
1565
+ const fc = options?.maxDisparityPct != null ? Math.round(options.maxDisparityPct) : 0;
1566
+ const fp = mintFingerprint(this.config.tenantId, "AI-FAIR.3", fa, fb, fc, ts);
1567
+ const payload = {
1568
+ procedure_id: "AI-FAIR.3", factor_a: fa, factor_b: fb, factor_c: fc,
1569
+ clearing_level: this.config.clearingLevel,
1570
+ anchor_fingerprint: fp, anchor_epoch: epoch, fingerprint_timestamp_ms: ts,
1571
+ };
1572
+ if (this.config.clearingLevel <= 1) {
1573
+ payload.ai_model_id = "bias-assessment";
1574
+ const ctx = {
1575
+ provider: "bias-assessment",
1576
+ protected_attribute_count: protectedAttributeCount,
1577
+ all_thresholds_met: allThresholdsMet,
1578
+ };
1579
+ if (options?.methodology)
1580
+ ctx.methodology = options.methodology;
1581
+ if (options?.protectedAttributes)
1582
+ ctx.protected_attributes = options.protectedAttributes;
1583
+ if (options?.maxDisparityPct != null)
1584
+ ctx.max_disparity_pct = options.maxDisparityPct;
1585
+ payload.ai_context = ctx;
1586
+ }
1587
+ const policyHash = this.config.policyVersion ? sha256Truncated(this.config.policyVersion, 12) : undefined;
1588
+ this._applyOperationalMetadata(payload, policyHash);
1589
+ this.buffer.enqueueMany([payload]);
1590
+ return payload;
1591
+ }
1592
+ // ── Trust Mesh (AI-TRUST.1 / AI-TRUST.2) ────────────────────────
1593
+ _trustRegistry;
1594
+ _witnessedProcedures = new Set();
1595
+ get trustRegistry() {
1596
+ if (!this._trustRegistry)
1597
+ this._trustRegistry = new TrustRegistry();
1598
+ return this._trustRegistry;
1599
+ }
1600
+ set trustRegistry(registry) {
1601
+ this._trustRegistry = registry;
1602
+ }
1603
+ presentCredential() {
1604
+ const ts = Date.now();
1605
+ const fpInput = `${this.config.agentId || "anonymous"}:${this.config.tenantId}:${ts}`;
1606
+ const credential = {
1607
+ agentId: this.config.agentId || "anonymous",
1608
+ tenantId: this.config.tenantId,
1609
+ anchorFingerprint: sha256Truncated(fpInput, 12),
1610
+ anchorTimestampMs: ts,
1611
+ isSigned: Boolean(this.config.signingKey),
1612
+ procedures: [...this._witnessedProcedures],
1613
+ clearingLevel: this.config.clearingLevel,
1614
+ hasHardwareAttestation: this._witnessedProcedures.has("AI-HW.1") || this._witnessedProcedures.has("AI-HW.3"),
1615
+ hasGuardrails: this.config.guardrailsRequired > 0 || this.config.guardrailNames.length > 0,
1616
+ };
1617
+ if (this.config.signingKey) {
1618
+ credential.credentialSignature = signCredential(credential, this.config.signingKey);
1619
+ }
1620
+ return credential;
1621
+ }
1622
+ verifyTrust(credential) {
1623
+ const result = verifyCredential(credential, this.trustRegistry, this.config.tenantId);
1624
+ // Mint AI-TRUST.1
1625
+ const [ts1, ep1] = timestampMs();
1626
+ const fa1 = 1, fb1 = result.granted ? 1 : 0, fc1 = result.trustLevel;
1627
+ const fp1 = mintFingerprint(this.config.tenantId, "AI-TRUST.1", fa1, fb1, fc1, ts1);
1628
+ const p1 = {
1629
+ procedure_id: "AI-TRUST.1",
1630
+ factor_a: fa1, factor_b: fb1, factor_c: fc1,
1631
+ clearing_level: this.config.clearingLevel,
1632
+ anchor_fingerprint: fp1, anchor_epoch: ep1, fingerprint_timestamp_ms: ts1,
1633
+ };
1634
+ if (this.config.clearingLevel <= 1) {
1635
+ p1.ai_model_id = `trust-${TRUST_LEVEL_NAMES[result.trustLevel] ?? "unknown"}`;
1636
+ const ctx = {
1637
+ provider: "trust-mesh",
1638
+ counterpart_agent_id: credential.agentId,
1639
+ counterpart_tenant_id: credential.tenantId,
1640
+ trust_level: result.trustLevel,
1641
+ trust_level_name: TRUST_LEVEL_NAMES[result.trustLevel] ?? "unknown",
1642
+ checks_performed: result.checksPerformed,
1643
+ checks_passed: result.checksPassed,
1644
+ granted: result.granted,
1645
+ };
1646
+ if (result.denialReason)
1647
+ ctx.denial_reason = result.denialReason;
1648
+ p1.ai_context = ctx;
1649
+ }
1650
+ const policyHash = this.config.policyVersion
1651
+ ? sha256Truncated(this.config.policyVersion, 12) : undefined;
1652
+ this._applyOperationalMetadata(p1, policyHash);
1653
+ this.buffer.enqueueMany([p1]);
1654
+ // Mint AI-TRUST.2
1655
+ const [ts2, ep2] = timestampMs();
1656
+ const fa2 = result.checksPerformed, fb2 = result.checksPassed;
1657
+ const fc2 = result.granted ? 1 : 0;
1658
+ const fp2 = mintFingerprint(this.config.tenantId, "AI-TRUST.2", fa2, fb2, fc2, ts2);
1659
+ const p2 = {
1660
+ procedure_id: "AI-TRUST.2",
1661
+ factor_a: fa2, factor_b: fb2, factor_c: fc2,
1662
+ clearing_level: this.config.clearingLevel,
1663
+ anchor_fingerprint: fp2, anchor_epoch: ep2, fingerprint_timestamp_ms: ts2,
1664
+ };
1665
+ if (this.config.clearingLevel <= 1) {
1666
+ p2.ai_model_id = "trust-handshake";
1667
+ p2.ai_context = {
1668
+ provider: "trust-mesh",
1669
+ counterpart_agent_id: credential.agentId,
1670
+ handshake_result: result.granted ? "granted" : "denied",
1671
+ };
1672
+ }
1673
+ this._applyOperationalMetadata(p2, policyHash);
1674
+ this.buffer.enqueueMany([p2]);
1675
+ return result;
1676
+ }
448
1677
  revoke(fingerprint, reason = "unspecified") {
449
1678
  if (!fingerprint?.trim()) {
450
1679
  throw new Error("fingerprint is required for revocation");
451
1680
  }
452
- if (!(reason in REVOCATION_REASONS)) {
1681
+ if (!Object.prototype.hasOwnProperty.call(REVOCATION_REASONS, reason)) {
453
1682
  throw new Error(`Unknown revocation reason: "${reason}". Valid: ${Object.keys(REVOCATION_REASONS).sort().join(", ")}`);
454
1683
  }
455
1684
  const policyHash = this.config.policyVersion