@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.
- package/LICENSE +191 -0
- package/README.md +227 -10
- package/dist/buffer.d.ts +7 -1
- package/dist/buffer.d.ts.map +1 -1
- package/dist/buffer.js +38 -3
- package/dist/buffer.js.map +1 -1
- package/dist/cli.d.ts +13 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +202 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +18 -5
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +346 -42
- package/dist/config.js.map +1 -1
- package/dist/demo.d.ts +1 -1
- package/dist/demo.d.ts.map +1 -1
- package/dist/demo.js +88 -4
- package/dist/demo.js.map +1 -1
- package/dist/doctor.d.ts +20 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +357 -0
- package/dist/doctor.js.map +1 -0
- package/dist/environment.d.ts +34 -0
- package/dist/environment.d.ts.map +1 -0
- package/dist/environment.js +99 -0
- package/dist/environment.js.map +1 -0
- package/dist/exporters/chain-monitor.d.ts +55 -0
- package/dist/exporters/chain-monitor.d.ts.map +1 -0
- package/dist/exporters/chain-monitor.js +172 -0
- package/dist/exporters/chain-monitor.js.map +1 -0
- package/dist/hardware.d.ts +96 -0
- package/dist/hardware.d.ts.map +1 -0
- package/dist/hardware.js +265 -0
- package/dist/hardware.js.map +1 -0
- package/dist/index.d.ts +19 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -2
- package/dist/index.js.map +1 -1
- package/dist/merkle.d.ts +107 -0
- package/dist/merkle.d.ts.map +1 -0
- package/dist/merkle.js +226 -0
- package/dist/merkle.js.map +1 -0
- package/dist/schema.d.ts +18 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +255 -0
- package/dist/schema.js.map +1 -0
- package/dist/trust.d.ts +100 -0
- package/dist/trust.d.ts.map +1 -0
- package/dist/trust.js +222 -0
- package/dist/trust.js.map +1 -0
- package/dist/types.d.ts +167 -11
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +42 -1
- package/dist/types.js.map +1 -1
- package/dist/wal.d.ts +69 -0
- package/dist/wal.d.ts.map +1 -0
- package/dist/wal.js +223 -0
- package/dist/wal.js.map +1 -0
- package/dist/witness.d.ts +293 -1
- package/dist/witness.d.ts.map +1 -1
- package/dist/witness.js +1234 -5
- package/dist/witness.js.map +1 -1
- package/package.json +7 -7
- package/templates/cost-conscious.yaml +35 -0
- package/templates/eu-ai-act-high-risk.yaml +56 -0
- package/templates/granite-sovereign.yaml +55 -0
- package/templates/minimal.yaml +38 -0
- package/templates/mythos-defense.yaml +65 -0
- package/templates/nist-ai-rmf.yaml +47 -0
- 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 {
|
|
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
|
|
75
|
-
|
|
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
|
-
|
|
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
|
|
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
|