@sanctuary-framework/mcp-server 1.0.0-rc.2 → 1.1.0
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/dist/cli.cjs +3588 -348
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +3590 -350
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +2849 -219
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +907 -3
- package/dist/index.d.ts +907 -3
- package/dist/index.js +2846 -223
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -3,8 +3,8 @@ import { gcm } from '@noble/ciphers/aes.js';
|
|
|
3
3
|
import { sha256 } from '@noble/hashes/sha256';
|
|
4
4
|
import { hmac } from '@noble/hashes/hmac';
|
|
5
5
|
import { RistrettoPoint, ed25519 } from '@noble/curves/ed25519';
|
|
6
|
-
import { readFile, mkdir, writeFile, stat, unlink, readdir, chmod, access } from 'fs/promises';
|
|
7
|
-
import { join,
|
|
6
|
+
import { readFile, mkdir, writeFile, stat, unlink, readdir, chmod, lstat, realpath, rm, access } from 'fs/promises';
|
|
7
|
+
import { join, resolve, dirname, sep, basename } from 'path';
|
|
8
8
|
import { platform, homedir } from 'os';
|
|
9
9
|
import { createRequire } from 'module';
|
|
10
10
|
import { argon2id } from 'hash-wasm';
|
|
@@ -13,7 +13,7 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
|
13
13
|
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
14
14
|
import { createServer as createServer$2 } from 'http';
|
|
15
15
|
import { createServer as createServer$1 } from 'https';
|
|
16
|
-
import { exec, execSync } from 'child_process';
|
|
16
|
+
import { exec, execSync, spawn } from 'child_process';
|
|
17
17
|
import { statSync, existsSync, readFileSync } from 'fs';
|
|
18
18
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
19
19
|
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
@@ -475,6 +475,15 @@ function defaultConfig() {
|
|
|
475
475
|
auto_publish_to_verascore: true,
|
|
476
476
|
// DELTA-04: default OFF for privacy. Enable explicitly per deployment.
|
|
477
477
|
auto_publish_handshakes: false
|
|
478
|
+
},
|
|
479
|
+
privacy_filter: {
|
|
480
|
+
mode: "local",
|
|
481
|
+
// Fail-closed by default per Sanctuary Invariant #5: never silently
|
|
482
|
+
// degrade to a less-secure behavior on error. Operators who need a
|
|
483
|
+
// legacy fallback path opt in explicitly via config or env var.
|
|
484
|
+
fail_mode: "closed",
|
|
485
|
+
command: "opf",
|
|
486
|
+
timeout_ms: 5e3
|
|
478
487
|
}
|
|
479
488
|
};
|
|
480
489
|
}
|
|
@@ -486,10 +495,14 @@ async function loadConfig(configPath) {
|
|
|
486
495
|
const raw = await readFile(path, "utf-8");
|
|
487
496
|
const fileConfig = JSON.parse(raw);
|
|
488
497
|
config = deepMerge(config, fileConfig);
|
|
498
|
+
assertSanctuaryConfigShape(config);
|
|
489
499
|
} catch (err) {
|
|
490
500
|
if (err instanceof Error && err.message.includes("unimplemented features")) {
|
|
491
501
|
throw err;
|
|
492
502
|
}
|
|
503
|
+
if (err instanceof Error && err.message.startsWith("Sanctuary config field")) {
|
|
504
|
+
throw err;
|
|
505
|
+
}
|
|
493
506
|
}
|
|
494
507
|
if (process.env.SANCTUARY_STORAGE_PATH) {
|
|
495
508
|
config.storage_path = process.env.SANCTUARY_STORAGE_PATH;
|
|
@@ -560,6 +573,21 @@ async function loadConfig(configPath) {
|
|
|
560
573
|
if (process.env.SANCTUARY_AUTO_PUBLISH_HANDSHAKES === "false") {
|
|
561
574
|
config.verascore.auto_publish_handshakes = false;
|
|
562
575
|
}
|
|
576
|
+
if (process.env.SANCTUARY_PRIVACY_FILTER) {
|
|
577
|
+
config.privacy_filter.mode = process.env.SANCTUARY_PRIVACY_FILTER;
|
|
578
|
+
}
|
|
579
|
+
if (process.env.SANCTUARY_PRIVACY_FILTER_FAIL_MODE) {
|
|
580
|
+
config.privacy_filter.fail_mode = process.env.SANCTUARY_PRIVACY_FILTER_FAIL_MODE;
|
|
581
|
+
}
|
|
582
|
+
if (process.env.SANCTUARY_PRIVACY_FILTER_COMMAND) {
|
|
583
|
+
config.privacy_filter.command = process.env.SANCTUARY_PRIVACY_FILTER_COMMAND;
|
|
584
|
+
}
|
|
585
|
+
if (process.env.SANCTUARY_PRIVACY_FILTER_TIMEOUT_MS) {
|
|
586
|
+
config.privacy_filter.timeout_ms = parseInt(
|
|
587
|
+
process.env.SANCTUARY_PRIVACY_FILTER_TIMEOUT_MS,
|
|
588
|
+
10
|
|
589
|
+
);
|
|
590
|
+
}
|
|
563
591
|
config.version = PKG_VERSION;
|
|
564
592
|
validateConfig(config);
|
|
565
593
|
return config;
|
|
@@ -600,6 +628,23 @@ function validateConfig(config) {
|
|
|
600
628
|
`Unimplemented config value: reputation.mode = "${config.reputation.mode}". Only ${[...implementedReputationMode].map((v) => `"${v}"`).join(", ")} is currently implemented. Using an unimplemented reputation mode would silently skip reputation verification.`
|
|
601
629
|
);
|
|
602
630
|
}
|
|
631
|
+
const implementedPrivacyModes = /* @__PURE__ */ new Set(["local", "opf", "off"]);
|
|
632
|
+
if (!implementedPrivacyModes.has(config.privacy_filter.mode)) {
|
|
633
|
+
errors.push(
|
|
634
|
+
`Invalid config value: privacy_filter.mode = "${config.privacy_filter.mode}". Use ${[...implementedPrivacyModes].map((v) => `"${v}"`).join(", ")}.`
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
const implementedPrivacyFailModes = /* @__PURE__ */ new Set(["closed", "fallback"]);
|
|
638
|
+
if (!implementedPrivacyFailModes.has(config.privacy_filter.fail_mode)) {
|
|
639
|
+
errors.push(
|
|
640
|
+
`Invalid config value: privacy_filter.fail_mode = "${config.privacy_filter.fail_mode}". Use ${[...implementedPrivacyFailModes].map((v) => `"${v}"`).join(", ")}.`
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
if (!Number.isFinite(config.privacy_filter.timeout_ms) || config.privacy_filter.timeout_ms < 100) {
|
|
644
|
+
errors.push(
|
|
645
|
+
`Invalid config value: privacy_filter.timeout_ms = "${config.privacy_filter.timeout_ms}". Use an integer timeout of at least 100 ms.`
|
|
646
|
+
);
|
|
647
|
+
}
|
|
603
648
|
if (errors.length > 0) {
|
|
604
649
|
throw new Error(
|
|
605
650
|
`Sanctuary configuration references unimplemented features:
|
|
@@ -621,6 +666,39 @@ function deepMerge(base, override) {
|
|
|
621
666
|
}
|
|
622
667
|
return result;
|
|
623
668
|
}
|
|
669
|
+
function assertSanctuaryConfigShape(c) {
|
|
670
|
+
const requiredObjectKeys = [
|
|
671
|
+
"state",
|
|
672
|
+
"execution",
|
|
673
|
+
"disclosure",
|
|
674
|
+
"reputation",
|
|
675
|
+
"dashboard",
|
|
676
|
+
"webhook",
|
|
677
|
+
"verascore",
|
|
678
|
+
"privacy_filter"
|
|
679
|
+
];
|
|
680
|
+
for (const k of requiredObjectKeys) {
|
|
681
|
+
if (typeof c[k] !== "object" || c[k] === null || Array.isArray(c[k])) {
|
|
682
|
+
throw new Error(
|
|
683
|
+
`Sanctuary config field "${k}" must be an object; got ${c[k] === null ? "null" : Array.isArray(c[k]) ? "array" : typeof c[k]}`
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
if (typeof c.version !== "string") {
|
|
688
|
+
throw new Error(`Sanctuary config field "version" must be a string`);
|
|
689
|
+
}
|
|
690
|
+
if (typeof c.storage_path !== "string") {
|
|
691
|
+
throw new Error(`Sanctuary config field "storage_path" must be a string`);
|
|
692
|
+
}
|
|
693
|
+
if (c.transport !== "stdio" && c.transport !== "http") {
|
|
694
|
+
throw new Error(
|
|
695
|
+
`Sanctuary config field "transport" must be "stdio" | "http"; got ${JSON.stringify(c.transport)}`
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
if (typeof c.http_port !== "number" || !Number.isFinite(c.http_port)) {
|
|
699
|
+
throw new Error(`Sanctuary config field "http_port" must be a finite number`);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
624
702
|
|
|
625
703
|
// src/storage/filesystem.ts
|
|
626
704
|
init_random();
|
|
@@ -817,6 +895,11 @@ var RESERVED_NAMESPACE_PREFIXES = [
|
|
|
817
895
|
"_context_gate_policies",
|
|
818
896
|
"_fortress_mode"
|
|
819
897
|
];
|
|
898
|
+
function isReservedNamespace(namespace) {
|
|
899
|
+
return RESERVED_NAMESPACE_PREFIXES.some(
|
|
900
|
+
(prefix) => namespace === prefix || namespace.startsWith(prefix + "/")
|
|
901
|
+
);
|
|
902
|
+
}
|
|
820
903
|
var StateStore = class _StateStore {
|
|
821
904
|
storage;
|
|
822
905
|
masterKey;
|
|
@@ -4145,6 +4228,25 @@ var DEFAULT_POLICY = {
|
|
|
4145
4228
|
// Creates new Ed25519 identity + publishes — always requires approval
|
|
4146
4229
|
"sanctuary_export_identity_bundle",
|
|
4147
4230
|
// Exports portable identity — always requires approval
|
|
4231
|
+
"exit_bundle_export",
|
|
4232
|
+
// Complete portability bundle export. Always requires approval.
|
|
4233
|
+
"exit_bundle_import",
|
|
4234
|
+
// External durable-record import. Always requires approval.
|
|
4235
|
+
"exit_bundle_import_activate",
|
|
4236
|
+
// Activates imported material. Always requires approval.
|
|
4237
|
+
"exit_bundle_rekey",
|
|
4238
|
+
// Re-encrypts imported state under destination keys.
|
|
4239
|
+
// v1.1 hub-control surfaces. Every operation_category in
|
|
4240
|
+
// server/src/contracts/v1.1/hub-events.ts that is not already canonical
|
|
4241
|
+
// here MUST be enrolled under Tier 1 in the same PR that lands the hub
|
|
4242
|
+
// endpoint that surfaces it. Drift between the contract enum and this
|
|
4243
|
+
// list is a release blocker per the contract comment.
|
|
4244
|
+
"policy_change",
|
|
4245
|
+
// Operator-driven policy bind on a wrapped agent.
|
|
4246
|
+
"lockdown",
|
|
4247
|
+
// Operator-driven hard-stop of a wrapped agent.
|
|
4248
|
+
"unwrap",
|
|
4249
|
+
// Operator-driven removal of the Sanctuary wrap from an agent.
|
|
4148
4250
|
// WP-MVP-2 Operator Console: federation-node-join requires explicit
|
|
4149
4251
|
// operator confirmation per Key 8. No auto-approve path. The console's
|
|
4150
4252
|
// JoinApprover drives this gate via `MeshConsoleClient.makeJoinApprover`.
|
|
@@ -4334,6 +4436,13 @@ tier1_always_approve:
|
|
|
4334
4436
|
- governor_reset
|
|
4335
4437
|
- sanctuary_bootstrap
|
|
4336
4438
|
- sanctuary_export_identity_bundle
|
|
4439
|
+
- exit_bundle_export
|
|
4440
|
+
- exit_bundle_import
|
|
4441
|
+
- exit_bundle_import_activate
|
|
4442
|
+
- exit_bundle_rekey
|
|
4443
|
+
- policy_change
|
|
4444
|
+
- lockdown
|
|
4445
|
+
- unwrap
|
|
4337
4446
|
|
|
4338
4447
|
# \u2500\u2500\u2500 Tier 2: Behavioral Anomaly Detection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
4339
4448
|
# Triggers approval when agent behavior deviates from its baseline.
|
|
@@ -8931,7 +9040,7 @@ var DashboardApprovalChannel = class {
|
|
|
8931
9040
|
server = createServer$2(handler);
|
|
8932
9041
|
}
|
|
8933
9042
|
this.httpServer = server;
|
|
8934
|
-
return new Promise((
|
|
9043
|
+
return new Promise((resolve4, reject) => {
|
|
8935
9044
|
const protocol = this.useTLS ? "https" : "http";
|
|
8936
9045
|
const baseUrl = `${protocol}://${this.config.host}:${this.config.port}`;
|
|
8937
9046
|
server.listen(this.config.port, this.config.host, () => {
|
|
@@ -8956,7 +9065,7 @@ var DashboardApprovalChannel = class {
|
|
|
8956
9065
|
if (shouldAutoOpen) {
|
|
8957
9066
|
this.openInBrowser(sessionUrl);
|
|
8958
9067
|
}
|
|
8959
|
-
|
|
9068
|
+
resolve4();
|
|
8960
9069
|
});
|
|
8961
9070
|
server.on("error", (err) => {
|
|
8962
9071
|
if (err.code === "EADDRINUSE") {
|
|
@@ -9002,8 +9111,8 @@ var DashboardApprovalChannel = class {
|
|
|
9002
9111
|
}
|
|
9003
9112
|
this.rateLimits.clear();
|
|
9004
9113
|
if (this.httpServer) {
|
|
9005
|
-
return new Promise((
|
|
9006
|
-
this.httpServer.close(() =>
|
|
9114
|
+
return new Promise((resolve4) => {
|
|
9115
|
+
this.httpServer.close(() => resolve4());
|
|
9007
9116
|
});
|
|
9008
9117
|
}
|
|
9009
9118
|
}
|
|
@@ -9017,7 +9126,7 @@ var DashboardApprovalChannel = class {
|
|
|
9017
9126
|
`[Sanctuary] Approval required: ${request.operation} (Tier ${request.tier}) \u2014 open dashboard to respond
|
|
9018
9127
|
`
|
|
9019
9128
|
);
|
|
9020
|
-
return new Promise((
|
|
9129
|
+
return new Promise((resolve4) => {
|
|
9021
9130
|
const timer = setTimeout(() => {
|
|
9022
9131
|
this.pending.delete(id);
|
|
9023
9132
|
const response = {
|
|
@@ -9031,12 +9140,12 @@ var DashboardApprovalChannel = class {
|
|
|
9031
9140
|
decision: response.decision,
|
|
9032
9141
|
decided_by: "timeout"
|
|
9033
9142
|
});
|
|
9034
|
-
|
|
9143
|
+
resolve4(response);
|
|
9035
9144
|
}, this.config.timeout_seconds * 1e3);
|
|
9036
9145
|
const pending = {
|
|
9037
9146
|
id,
|
|
9038
9147
|
request,
|
|
9039
|
-
resolve:
|
|
9148
|
+
resolve: resolve4,
|
|
9040
9149
|
timer,
|
|
9041
9150
|
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
9042
9151
|
};
|
|
@@ -9882,7 +9991,7 @@ var WebhookApprovalChannel = class {
|
|
|
9882
9991
|
* Start the callback listener server.
|
|
9883
9992
|
*/
|
|
9884
9993
|
async start() {
|
|
9885
|
-
return new Promise((
|
|
9994
|
+
return new Promise((resolve4, reject) => {
|
|
9886
9995
|
this.callbackServer = createServer$2(
|
|
9887
9996
|
(req, res) => this.handleCallback(req, res)
|
|
9888
9997
|
);
|
|
@@ -9897,7 +10006,7 @@ var WebhookApprovalChannel = class {
|
|
|
9897
10006
|
|
|
9898
10007
|
`
|
|
9899
10008
|
);
|
|
9900
|
-
|
|
10009
|
+
resolve4();
|
|
9901
10010
|
}
|
|
9902
10011
|
);
|
|
9903
10012
|
this.callbackServer.on("error", reject);
|
|
@@ -9917,8 +10026,8 @@ var WebhookApprovalChannel = class {
|
|
|
9917
10026
|
}
|
|
9918
10027
|
this.pending.clear();
|
|
9919
10028
|
if (this.callbackServer) {
|
|
9920
|
-
return new Promise((
|
|
9921
|
-
this.callbackServer.close(() =>
|
|
10029
|
+
return new Promise((resolve4) => {
|
|
10030
|
+
this.callbackServer.close(() => resolve4());
|
|
9922
10031
|
});
|
|
9923
10032
|
}
|
|
9924
10033
|
}
|
|
@@ -9931,7 +10040,7 @@ var WebhookApprovalChannel = class {
|
|
|
9931
10040
|
`[Sanctuary] Webhook approval sent: ${request.operation} (Tier ${request.tier}) \u2014 awaiting callback
|
|
9932
10041
|
`
|
|
9933
10042
|
);
|
|
9934
|
-
return new Promise((
|
|
10043
|
+
return new Promise((resolve4) => {
|
|
9935
10044
|
const timer = setTimeout(() => {
|
|
9936
10045
|
this.pending.delete(id);
|
|
9937
10046
|
const response = {
|
|
@@ -9940,12 +10049,12 @@ var WebhookApprovalChannel = class {
|
|
|
9940
10049
|
decided_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9941
10050
|
decided_by: "timeout"
|
|
9942
10051
|
};
|
|
9943
|
-
|
|
10052
|
+
resolve4(response);
|
|
9944
10053
|
}, this.config.timeout_seconds * 1e3);
|
|
9945
10054
|
const pending = {
|
|
9946
10055
|
id,
|
|
9947
10056
|
request,
|
|
9948
|
-
resolve:
|
|
10057
|
+
resolve: resolve4,
|
|
9949
10058
|
timer,
|
|
9950
10059
|
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
9951
10060
|
};
|
|
@@ -11492,7 +11601,7 @@ var ApprovalGate = class {
|
|
|
11492
11601
|
return {
|
|
11493
11602
|
allowed: response.decision === "approve",
|
|
11494
11603
|
tier,
|
|
11495
|
-
reason: response.decision === "approve" ? `Approved by ${response.decided_by}` :
|
|
11604
|
+
reason: response.decision === "approve" ? `Approved by ${response.decided_by}` : `Tier ${tier} operation requires approval`,
|
|
11496
11605
|
approval_required: true,
|
|
11497
11606
|
approval_response: response
|
|
11498
11607
|
};
|
|
@@ -14254,6 +14363,140 @@ function createAuditTools(config) {
|
|
|
14254
14363
|
return { tools };
|
|
14255
14364
|
}
|
|
14256
14365
|
|
|
14366
|
+
// src/audit/reset-history.ts
|
|
14367
|
+
init_hashing();
|
|
14368
|
+
init_encoding();
|
|
14369
|
+
var RESET_HISTORY_FILENAME = ".reset-history.log";
|
|
14370
|
+
var RECOVERED_FROM_RESET_OPERATION = "fortress_recovered_from_reset";
|
|
14371
|
+
var ResetHistoryMalformedError = class extends Error {
|
|
14372
|
+
constructor(markerPath, lineNumber, cause) {
|
|
14373
|
+
const reason = cause instanceof Error ? cause.message : String(cause);
|
|
14374
|
+
super(
|
|
14375
|
+
`Reset-history marker at ${markerPath} is malformed at line ${lineNumber}: ${reason}.
|
|
14376
|
+
Inspect the file and either correct the JSON or delete it manually before re-running.`
|
|
14377
|
+
);
|
|
14378
|
+
this.markerPath = markerPath;
|
|
14379
|
+
this.lineNumber = lineNumber;
|
|
14380
|
+
this.cause = cause;
|
|
14381
|
+
this.name = "ResetHistoryMalformedError";
|
|
14382
|
+
}
|
|
14383
|
+
};
|
|
14384
|
+
function parseResetHistory(content, markerPath) {
|
|
14385
|
+
const markerHash = hashToString(stringToBytes(content));
|
|
14386
|
+
const markers = [];
|
|
14387
|
+
const lines = content.split("\n");
|
|
14388
|
+
for (let i = 0; i < lines.length; i++) {
|
|
14389
|
+
const raw = lines[i] ?? "";
|
|
14390
|
+
if (raw.trim().length === 0) continue;
|
|
14391
|
+
let parsed;
|
|
14392
|
+
try {
|
|
14393
|
+
parsed = JSON.parse(raw);
|
|
14394
|
+
} catch (err) {
|
|
14395
|
+
throw new ResetHistoryMalformedError(markerPath, i + 1, err);
|
|
14396
|
+
}
|
|
14397
|
+
markers.push(coerceMarker(parsed, markerPath, i + 1));
|
|
14398
|
+
}
|
|
14399
|
+
return { markers, markerHash };
|
|
14400
|
+
}
|
|
14401
|
+
function coerceMarker(raw, markerPath, lineNumber) {
|
|
14402
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
14403
|
+
throw new ResetHistoryMalformedError(
|
|
14404
|
+
markerPath,
|
|
14405
|
+
lineNumber,
|
|
14406
|
+
new Error("expected a JSON object")
|
|
14407
|
+
);
|
|
14408
|
+
}
|
|
14409
|
+
const obj = raw;
|
|
14410
|
+
const required = [
|
|
14411
|
+
"started_at",
|
|
14412
|
+
"completed_at",
|
|
14413
|
+
"recovery_mode",
|
|
14414
|
+
"fortress_name",
|
|
14415
|
+
"storage_path",
|
|
14416
|
+
"keychain_cleared"
|
|
14417
|
+
];
|
|
14418
|
+
for (const field of required) {
|
|
14419
|
+
if (!(field in obj)) {
|
|
14420
|
+
throw new ResetHistoryMalformedError(
|
|
14421
|
+
markerPath,
|
|
14422
|
+
lineNumber,
|
|
14423
|
+
new Error(`missing required field "${field}"`)
|
|
14424
|
+
);
|
|
14425
|
+
}
|
|
14426
|
+
}
|
|
14427
|
+
if (typeof obj.started_at !== "string")
|
|
14428
|
+
throw typed(markerPath, lineNumber, "started_at", "string");
|
|
14429
|
+
if (typeof obj.completed_at !== "string")
|
|
14430
|
+
throw typed(markerPath, lineNumber, "completed_at", "string");
|
|
14431
|
+
if (typeof obj.fortress_name !== "string")
|
|
14432
|
+
throw typed(markerPath, lineNumber, "fortress_name", "string");
|
|
14433
|
+
if (typeof obj.storage_path !== "string")
|
|
14434
|
+
throw typed(markerPath, lineNumber, "storage_path", "string");
|
|
14435
|
+
if (typeof obj.keychain_cleared !== "boolean")
|
|
14436
|
+
throw typed(markerPath, lineNumber, "keychain_cleared", "boolean");
|
|
14437
|
+
if (obj.recovery_mode !== "shares" && obj.recovery_mode !== "guardian" && obj.recovery_mode !== "nuke") {
|
|
14438
|
+
throw new ResetHistoryMalformedError(
|
|
14439
|
+
markerPath,
|
|
14440
|
+
lineNumber,
|
|
14441
|
+
new Error(
|
|
14442
|
+
`recovery_mode must be one of shares|guardian|nuke (got ${JSON.stringify(obj.recovery_mode)})`
|
|
14443
|
+
)
|
|
14444
|
+
);
|
|
14445
|
+
}
|
|
14446
|
+
return {
|
|
14447
|
+
started_at: obj.started_at,
|
|
14448
|
+
completed_at: obj.completed_at,
|
|
14449
|
+
recovery_mode: obj.recovery_mode,
|
|
14450
|
+
fortress_name: obj.fortress_name,
|
|
14451
|
+
storage_path: obj.storage_path,
|
|
14452
|
+
keychain_cleared: obj.keychain_cleared
|
|
14453
|
+
};
|
|
14454
|
+
}
|
|
14455
|
+
function typed(markerPath, lineNumber, field, expected) {
|
|
14456
|
+
return new ResetHistoryMalformedError(
|
|
14457
|
+
markerPath,
|
|
14458
|
+
lineNumber,
|
|
14459
|
+
new Error(`field "${field}" must be a ${expected}`)
|
|
14460
|
+
);
|
|
14461
|
+
}
|
|
14462
|
+
async function consumeResetHistoryMarker(options) {
|
|
14463
|
+
const markerPath = join(options.storagePath, RESET_HISTORY_FILENAME);
|
|
14464
|
+
if (!await fileExists2(markerPath)) {
|
|
14465
|
+
return { emitted: 0, markerPath };
|
|
14466
|
+
}
|
|
14467
|
+
const content = await readFile(markerPath, "utf-8");
|
|
14468
|
+
const { markers, markerHash } = parseResetHistory(content, markerPath);
|
|
14469
|
+
if (markers.length === 0) {
|
|
14470
|
+
await rm(markerPath, { force: true });
|
|
14471
|
+
return { emitted: 0, markerHash, markerPath };
|
|
14472
|
+
}
|
|
14473
|
+
for (let i = 0; i < markers.length; i++) {
|
|
14474
|
+
const marker = markers[i];
|
|
14475
|
+
options.auditLog.append("l2", RECOVERED_FROM_RESET_OPERATION, "system", {
|
|
14476
|
+
reset_at_started: marker.started_at,
|
|
14477
|
+
reset_at_completed: marker.completed_at,
|
|
14478
|
+
recovery_mode: marker.recovery_mode,
|
|
14479
|
+
fortress_name: marker.fortress_name,
|
|
14480
|
+
reset_storage_path: marker.storage_path,
|
|
14481
|
+
keychain_cleared: marker.keychain_cleared,
|
|
14482
|
+
reset_marker_hash: markerHash,
|
|
14483
|
+
reset_marker_index: i,
|
|
14484
|
+
reset_marker_total: markers.length
|
|
14485
|
+
});
|
|
14486
|
+
}
|
|
14487
|
+
await options.auditLog.flush();
|
|
14488
|
+
await rm(markerPath, { force: true });
|
|
14489
|
+
return { emitted: markers.length, markerHash, markerPath };
|
|
14490
|
+
}
|
|
14491
|
+
async function fileExists2(path) {
|
|
14492
|
+
try {
|
|
14493
|
+
await access(path);
|
|
14494
|
+
return true;
|
|
14495
|
+
} catch {
|
|
14496
|
+
return false;
|
|
14497
|
+
}
|
|
14498
|
+
}
|
|
14499
|
+
|
|
14257
14500
|
// src/audit/siem-formatter.ts
|
|
14258
14501
|
function parseGateDecision(details) {
|
|
14259
14502
|
if (!details || typeof details.gate_decision !== "string") {
|
|
@@ -15321,6 +15564,607 @@ function matchesFieldPattern(normalizedField, pattern) {
|
|
|
15321
15564
|
// src/l2-operational/context-gate-enforcer.ts
|
|
15322
15565
|
init_encoding();
|
|
15323
15566
|
init_hashing();
|
|
15567
|
+
|
|
15568
|
+
// src/l2-operational/privacy-filter.ts
|
|
15569
|
+
init_encryption();
|
|
15570
|
+
init_encoding();
|
|
15571
|
+
init_hashing();
|
|
15572
|
+
var SPAN_PATTERNS = [
|
|
15573
|
+
{
|
|
15574
|
+
class: "email",
|
|
15575
|
+
pattern: /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi,
|
|
15576
|
+
replacement: "[EMAIL_REDACTED]",
|
|
15577
|
+
placeholderPrefix: "EMAIL",
|
|
15578
|
+
detectorClass: "person"
|
|
15579
|
+
},
|
|
15580
|
+
{
|
|
15581
|
+
class: "ssn",
|
|
15582
|
+
pattern: /\b\d{3}-\d{2}-\d{4}\b/g,
|
|
15583
|
+
replacement: "[SSN_REDACTED]",
|
|
15584
|
+
placeholderPrefix: "SSN",
|
|
15585
|
+
detectorClass: "person"
|
|
15586
|
+
},
|
|
15587
|
+
{
|
|
15588
|
+
class: "credit_card",
|
|
15589
|
+
pattern: /\b(?:\d[ -]*?){13,19}\b/g,
|
|
15590
|
+
replacement: "[CARD_REDACTED]",
|
|
15591
|
+
placeholderPrefix: "CARD",
|
|
15592
|
+
detectorClass: "account"
|
|
15593
|
+
},
|
|
15594
|
+
{
|
|
15595
|
+
class: "phone",
|
|
15596
|
+
pattern: /\b(?:\+?1[-.\s]?)?(?:\(?\d{3}\)?[-.\s]?)\d{3}[-.\s]?\d{4}\b/g,
|
|
15597
|
+
replacement: "[PHONE_REDACTED]",
|
|
15598
|
+
placeholderPrefix: "PHONE",
|
|
15599
|
+
detectorClass: "person"
|
|
15600
|
+
},
|
|
15601
|
+
{
|
|
15602
|
+
class: "secret_assignment",
|
|
15603
|
+
pattern: /\b(api[_-]?key|access[_-]?token|refresh[_-]?token|password|secret)\s*[:=]\s*["']?[^"',\s}]+/gi,
|
|
15604
|
+
replacement: "$1=[SECRET_REDACTED]",
|
|
15605
|
+
placeholderPrefix: "SECRET",
|
|
15606
|
+
detectorClass: "secret"
|
|
15607
|
+
},
|
|
15608
|
+
{
|
|
15609
|
+
class: "secret",
|
|
15610
|
+
pattern: /\b(?:Bearer\s+[A-Za-z0-9._~+/-]{16,}|sk-[A-Za-z0-9_-]{12,}|sk_(?:live|test)_[A-Za-z0-9]{12,}|ghp_[A-Za-z0-9_]{20,}|xox[baprs]-[A-Za-z0-9-]{10,}|AKIA[0-9A-Z]{16}|eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,})\b/g,
|
|
15611
|
+
replacement: "[SECRET_REDACTED]",
|
|
15612
|
+
placeholderPrefix: "SECRET",
|
|
15613
|
+
detectorClass: "secret"
|
|
15614
|
+
},
|
|
15615
|
+
{
|
|
15616
|
+
class: "credential",
|
|
15617
|
+
pattern: /\b(?:credential|credentials|client[_-]?secret|private[_-]?key)\s*[:=]\s*["']?[^"',\s}]+/gi,
|
|
15618
|
+
replacement: "[CREDENTIAL_REDACTED]",
|
|
15619
|
+
placeholderPrefix: "CREDENTIAL",
|
|
15620
|
+
detectorClass: "credential"
|
|
15621
|
+
},
|
|
15622
|
+
{
|
|
15623
|
+
class: "account_number",
|
|
15624
|
+
pattern: /\b(?:(?:acct|account|customer|tenant|org)[_-][A-Za-z0-9]{4,}|(?:acct|account)[0-9]{4,})\b/gi,
|
|
15625
|
+
replacement: "[ACCOUNT_REDACTED]",
|
|
15626
|
+
placeholderPrefix: "ACCOUNT",
|
|
15627
|
+
detectorClass: "account"
|
|
15628
|
+
},
|
|
15629
|
+
{
|
|
15630
|
+
class: "file_path",
|
|
15631
|
+
pattern: /(?:~\/|\/(?:Users|home|var|tmp|etc|opt)\/|[A-Za-z]:\\|\.{1,2}\/)(?:[A-Za-z0-9._-]+[\\/])+[A-Za-z0-9._-]+/g,
|
|
15632
|
+
replacement: "[FILE_PATH_REDACTED]",
|
|
15633
|
+
placeholderPrefix: "FILE_PATH",
|
|
15634
|
+
detectorClass: "file_path"
|
|
15635
|
+
}
|
|
15636
|
+
];
|
|
15637
|
+
var MAX_DEPTH = 20;
|
|
15638
|
+
var VAULT_NAMESPACE = "_privacy_placeholder_vault";
|
|
15639
|
+
var PRIVACY_VAULT_CACHE_MAX = 5e3;
|
|
15640
|
+
var LruCache = class {
|
|
15641
|
+
max;
|
|
15642
|
+
map = /* @__PURE__ */ new Map();
|
|
15643
|
+
constructor(max) {
|
|
15644
|
+
this.max = max;
|
|
15645
|
+
}
|
|
15646
|
+
get(key) {
|
|
15647
|
+
const value = this.map.get(key);
|
|
15648
|
+
if (value === void 0) return void 0;
|
|
15649
|
+
this.map.delete(key);
|
|
15650
|
+
this.map.set(key, value);
|
|
15651
|
+
return value;
|
|
15652
|
+
}
|
|
15653
|
+
set(key, value) {
|
|
15654
|
+
if (this.map.has(key)) {
|
|
15655
|
+
this.map.delete(key);
|
|
15656
|
+
} else if (this.map.size >= this.max) {
|
|
15657
|
+
const oldestKey = this.map.keys().next().value;
|
|
15658
|
+
if (oldestKey !== void 0) {
|
|
15659
|
+
this.map.delete(oldestKey);
|
|
15660
|
+
}
|
|
15661
|
+
}
|
|
15662
|
+
this.map.set(key, value);
|
|
15663
|
+
}
|
|
15664
|
+
has(key) {
|
|
15665
|
+
return this.map.has(key);
|
|
15666
|
+
}
|
|
15667
|
+
get size() {
|
|
15668
|
+
return this.map.size;
|
|
15669
|
+
}
|
|
15670
|
+
};
|
|
15671
|
+
var PrivacyPlaceholderVault = class {
|
|
15672
|
+
storage;
|
|
15673
|
+
encryptionKey;
|
|
15674
|
+
lookupKey;
|
|
15675
|
+
cache = new LruCache(PRIVACY_VAULT_CACHE_MAX);
|
|
15676
|
+
pathCache = new LruCache(PRIVACY_VAULT_CACHE_MAX);
|
|
15677
|
+
constructor(storage, masterKey) {
|
|
15678
|
+
this.storage = storage;
|
|
15679
|
+
this.encryptionKey = derivePurposeKey(masterKey, "l2-privacy-placeholders");
|
|
15680
|
+
this.lookupKey = derivePurposeKey(masterKey, "l2-privacy-placeholder-lookup");
|
|
15681
|
+
}
|
|
15682
|
+
async placeholderFor(spanClass, rawValue, scope = "default") {
|
|
15683
|
+
const key = this.recordKey(spanClass, rawValue, scope);
|
|
15684
|
+
const cached = this.cache.get(key);
|
|
15685
|
+
if (cached) return cached.placeholder;
|
|
15686
|
+
const existing = await this.readRecord(key);
|
|
15687
|
+
if (existing) {
|
|
15688
|
+
this.cache.set(key, existing);
|
|
15689
|
+
return existing.placeholder;
|
|
15690
|
+
}
|
|
15691
|
+
const index = await this.readIndex(scope);
|
|
15692
|
+
const next = (index.counters[spanClass] ?? 0) + 1;
|
|
15693
|
+
index.counters[spanClass] = next;
|
|
15694
|
+
const placeholder = `${placeholderPrefixFor(spanClass)}_${next}`;
|
|
15695
|
+
const record = {
|
|
15696
|
+
version: 1,
|
|
15697
|
+
kind: "placeholder",
|
|
15698
|
+
scope,
|
|
15699
|
+
class: spanClass,
|
|
15700
|
+
placeholder,
|
|
15701
|
+
raw_value: rawValue,
|
|
15702
|
+
raw_hash: this.hmacString(`raw:${rawValue}`),
|
|
15703
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
15704
|
+
};
|
|
15705
|
+
await this.writeRecord(key, record);
|
|
15706
|
+
await this.writeIndex(scope, index);
|
|
15707
|
+
this.cache.set(key, record);
|
|
15708
|
+
return placeholder;
|
|
15709
|
+
}
|
|
15710
|
+
async resolvePlaceholder(placeholder, scope = "default") {
|
|
15711
|
+
const entries = await this.storage.list(VAULT_NAMESPACE, `${scope}__record__`);
|
|
15712
|
+
for (const meta of entries) {
|
|
15713
|
+
const record = await this.readRecord(meta.key);
|
|
15714
|
+
if (record?.placeholder === placeholder) {
|
|
15715
|
+
return record.raw_value;
|
|
15716
|
+
}
|
|
15717
|
+
}
|
|
15718
|
+
return null;
|
|
15719
|
+
}
|
|
15720
|
+
async aliasForFieldPath(path, scope = "default") {
|
|
15721
|
+
const key = this.pathRecordKey(path, scope);
|
|
15722
|
+
const cached = this.pathCache.get(key);
|
|
15723
|
+
if (cached) return cached.alias;
|
|
15724
|
+
const existing = await this.readPathRecord(key);
|
|
15725
|
+
if (existing) {
|
|
15726
|
+
this.pathCache.set(key, existing);
|
|
15727
|
+
return existing.alias;
|
|
15728
|
+
}
|
|
15729
|
+
const index = await this.readPathIndex(scope);
|
|
15730
|
+
const alias = `$${index.next}`;
|
|
15731
|
+
const nextIndex = { version: 1, next: index.next + 1 };
|
|
15732
|
+
const record = {
|
|
15733
|
+
version: 1,
|
|
15734
|
+
kind: "field_path",
|
|
15735
|
+
scope,
|
|
15736
|
+
alias,
|
|
15737
|
+
raw_path: path,
|
|
15738
|
+
raw_hash: this.hmacString(`path:${path}`),
|
|
15739
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
15740
|
+
};
|
|
15741
|
+
await this.writePathRecord(key, record);
|
|
15742
|
+
await this.writePathIndex(scope, nextIndex);
|
|
15743
|
+
this.pathCache.set(key, record);
|
|
15744
|
+
return alias;
|
|
15745
|
+
}
|
|
15746
|
+
async resolveFieldPathAlias(alias, scope = "default") {
|
|
15747
|
+
const entries = await this.storage.list(VAULT_NAMESPACE, `${scope}__path__`);
|
|
15748
|
+
for (const meta of entries) {
|
|
15749
|
+
const record = await this.readPathRecord(meta.key);
|
|
15750
|
+
if (record?.alias === alias) {
|
|
15751
|
+
return record.raw_path;
|
|
15752
|
+
}
|
|
15753
|
+
}
|
|
15754
|
+
return null;
|
|
15755
|
+
}
|
|
15756
|
+
recordKey(spanClass, rawValue, scope) {
|
|
15757
|
+
const rawHash = this.hmacString(`${scope}:${spanClass}:${rawValue}`);
|
|
15758
|
+
return `${scope}__record__${spanClass}__${rawHash}`;
|
|
15759
|
+
}
|
|
15760
|
+
indexKey(scope) {
|
|
15761
|
+
return `${scope}__index`;
|
|
15762
|
+
}
|
|
15763
|
+
pathRecordKey(path, scope) {
|
|
15764
|
+
return `${scope}__path__${this.hmacString(`${scope}:field_path:${path}`)}`;
|
|
15765
|
+
}
|
|
15766
|
+
pathIndexKey(scope) {
|
|
15767
|
+
return `${scope}__path_index`;
|
|
15768
|
+
}
|
|
15769
|
+
async readIndex(scope) {
|
|
15770
|
+
const raw = await this.storage.read(VAULT_NAMESPACE, this.indexKey(scope));
|
|
15771
|
+
if (!raw) return { version: 1, counters: {} };
|
|
15772
|
+
try {
|
|
15773
|
+
const encrypted = JSON.parse(bytesToString(raw));
|
|
15774
|
+
const decrypted = decrypt(encrypted, this.encryptionKey);
|
|
15775
|
+
return JSON.parse(bytesToString(decrypted));
|
|
15776
|
+
} catch (err) {
|
|
15777
|
+
throw new PrivacyVaultError("privacy_vault_index_unreadable", err);
|
|
15778
|
+
}
|
|
15779
|
+
}
|
|
15780
|
+
async writeIndex(scope, index) {
|
|
15781
|
+
const encrypted = encrypt(stringToBytes(JSON.stringify(index)), this.encryptionKey);
|
|
15782
|
+
await this.storage.write(
|
|
15783
|
+
VAULT_NAMESPACE,
|
|
15784
|
+
this.indexKey(scope),
|
|
15785
|
+
stringToBytes(JSON.stringify(encrypted))
|
|
15786
|
+
);
|
|
15787
|
+
}
|
|
15788
|
+
async readRecord(key) {
|
|
15789
|
+
const raw = await this.storage.read(VAULT_NAMESPACE, key);
|
|
15790
|
+
if (!raw) return null;
|
|
15791
|
+
try {
|
|
15792
|
+
const encrypted = JSON.parse(bytesToString(raw));
|
|
15793
|
+
const decrypted = decrypt(encrypted, this.encryptionKey);
|
|
15794
|
+
const parsed = JSON.parse(bytesToString(decrypted));
|
|
15795
|
+
return parsed.kind === "placeholder" || parsed.kind === void 0 ? parsed : null;
|
|
15796
|
+
} catch (err) {
|
|
15797
|
+
throw new PrivacyVaultError("privacy_vault_record_unreadable", err);
|
|
15798
|
+
}
|
|
15799
|
+
}
|
|
15800
|
+
async writeRecord(key, record) {
|
|
15801
|
+
const encrypted = encrypt(stringToBytes(JSON.stringify(record)), this.encryptionKey);
|
|
15802
|
+
await this.storage.write(
|
|
15803
|
+
VAULT_NAMESPACE,
|
|
15804
|
+
key,
|
|
15805
|
+
stringToBytes(JSON.stringify(encrypted))
|
|
15806
|
+
);
|
|
15807
|
+
}
|
|
15808
|
+
async readPathIndex(scope) {
|
|
15809
|
+
const raw = await this.storage.read(VAULT_NAMESPACE, this.pathIndexKey(scope));
|
|
15810
|
+
if (!raw) return { version: 1, next: 0 };
|
|
15811
|
+
try {
|
|
15812
|
+
const encrypted = JSON.parse(bytesToString(raw));
|
|
15813
|
+
const decrypted = decrypt(encrypted, this.encryptionKey);
|
|
15814
|
+
return JSON.parse(bytesToString(decrypted));
|
|
15815
|
+
} catch (err) {
|
|
15816
|
+
throw new PrivacyVaultError("privacy_vault_path_index_unreadable", err);
|
|
15817
|
+
}
|
|
15818
|
+
}
|
|
15819
|
+
async writePathIndex(scope, index) {
|
|
15820
|
+
const encrypted = encrypt(stringToBytes(JSON.stringify(index)), this.encryptionKey);
|
|
15821
|
+
await this.storage.write(
|
|
15822
|
+
VAULT_NAMESPACE,
|
|
15823
|
+
this.pathIndexKey(scope),
|
|
15824
|
+
stringToBytes(JSON.stringify(encrypted))
|
|
15825
|
+
);
|
|
15826
|
+
}
|
|
15827
|
+
async readPathRecord(key) {
|
|
15828
|
+
const raw = await this.storage.read(VAULT_NAMESPACE, key);
|
|
15829
|
+
if (!raw) return null;
|
|
15830
|
+
try {
|
|
15831
|
+
const encrypted = JSON.parse(bytesToString(raw));
|
|
15832
|
+
const decrypted = decrypt(encrypted, this.encryptionKey);
|
|
15833
|
+
const parsed = JSON.parse(bytesToString(decrypted));
|
|
15834
|
+
return parsed.kind === "field_path" ? parsed : null;
|
|
15835
|
+
} catch (err) {
|
|
15836
|
+
throw new PrivacyVaultError("privacy_vault_path_record_unreadable", err);
|
|
15837
|
+
}
|
|
15838
|
+
}
|
|
15839
|
+
async writePathRecord(key, record) {
|
|
15840
|
+
const encrypted = encrypt(stringToBytes(JSON.stringify(record)), this.encryptionKey);
|
|
15841
|
+
await this.storage.write(
|
|
15842
|
+
VAULT_NAMESPACE,
|
|
15843
|
+
key,
|
|
15844
|
+
stringToBytes(JSON.stringify(encrypted))
|
|
15845
|
+
);
|
|
15846
|
+
}
|
|
15847
|
+
hmacString(value) {
|
|
15848
|
+
return bytesToHex(hmacSha256(this.lookupKey, stringToBytes(value)));
|
|
15849
|
+
}
|
|
15850
|
+
};
|
|
15851
|
+
var PrivacyVaultError = class extends Error {
|
|
15852
|
+
code;
|
|
15853
|
+
cause;
|
|
15854
|
+
constructor(code, cause) {
|
|
15855
|
+
super(code);
|
|
15856
|
+
this.name = "PrivacyVaultError";
|
|
15857
|
+
this.code = code;
|
|
15858
|
+
this.cause = cause;
|
|
15859
|
+
}
|
|
15860
|
+
};
|
|
15861
|
+
function applyLocalPrivacyFilter(value, path = "$") {
|
|
15862
|
+
const findings = [];
|
|
15863
|
+
const filtered = filterValue(value, path, findings, 0);
|
|
15864
|
+
return { value: filtered, findings };
|
|
15865
|
+
}
|
|
15866
|
+
async function applyPrivacyPlaceholders(value, vault, scope = "default", path = "$") {
|
|
15867
|
+
const findings = [];
|
|
15868
|
+
const filtered = await placeholderValue(value, path, findings, vault, scope, 0);
|
|
15869
|
+
return { value: filtered, findings };
|
|
15870
|
+
}
|
|
15871
|
+
async function applyOpenAIPrivacyFilterResult(result, vault, scope = "default", path = "$") {
|
|
15872
|
+
const text = result.text;
|
|
15873
|
+
const findings = [];
|
|
15874
|
+
const spans = result.detected_spans.map((span) => ({
|
|
15875
|
+
...span,
|
|
15876
|
+
class: mapOpenAIPrivacyLabel(span.label)
|
|
15877
|
+
})).filter(
|
|
15878
|
+
(span) => span.class !== null && Number.isInteger(span.start) && Number.isInteger(span.end) && span.start >= 0 && span.end > span.start && span.end <= text.length
|
|
15879
|
+
).sort((a, b) => a.start - b.start);
|
|
15880
|
+
let cursor = 0;
|
|
15881
|
+
const pieces = [];
|
|
15882
|
+
for (const span of spans) {
|
|
15883
|
+
if (span.start < cursor) continue;
|
|
15884
|
+
const raw = text.slice(span.start, span.end);
|
|
15885
|
+
const placeholder = await vault.placeholderFor(span.class, raw, scope);
|
|
15886
|
+
pieces.push(text.slice(cursor, span.start));
|
|
15887
|
+
pieces.push(placeholder);
|
|
15888
|
+
cursor = span.end;
|
|
15889
|
+
findings.push({
|
|
15890
|
+
path,
|
|
15891
|
+
class: span.class,
|
|
15892
|
+
action: "placeholder",
|
|
15893
|
+
placeholder
|
|
15894
|
+
});
|
|
15895
|
+
}
|
|
15896
|
+
pieces.push(text.slice(cursor));
|
|
15897
|
+
return {
|
|
15898
|
+
value: pieces.join(""),
|
|
15899
|
+
findings
|
|
15900
|
+
};
|
|
15901
|
+
}
|
|
15902
|
+
function detectSensitiveSpans(input, options = {}) {
|
|
15903
|
+
const spans = [];
|
|
15904
|
+
addConfiguredTermSpans(
|
|
15905
|
+
spans,
|
|
15906
|
+
input,
|
|
15907
|
+
options.clientNames ?? [],
|
|
15908
|
+
"client",
|
|
15909
|
+
"client",
|
|
15910
|
+
"CLIENT"
|
|
15911
|
+
);
|
|
15912
|
+
addConfiguredTermSpans(
|
|
15913
|
+
spans,
|
|
15914
|
+
input,
|
|
15915
|
+
options.projectNames ?? [],
|
|
15916
|
+
"project",
|
|
15917
|
+
"project",
|
|
15918
|
+
"PROJECT"
|
|
15919
|
+
);
|
|
15920
|
+
addConfiguredTermSpans(
|
|
15921
|
+
spans,
|
|
15922
|
+
input,
|
|
15923
|
+
options.domainTerms ?? [],
|
|
15924
|
+
"domain_term",
|
|
15925
|
+
"domain_term",
|
|
15926
|
+
"TERM"
|
|
15927
|
+
);
|
|
15928
|
+
addConfiguredTermSpans(
|
|
15929
|
+
spans,
|
|
15930
|
+
input,
|
|
15931
|
+
options.personNames ?? [],
|
|
15932
|
+
"person",
|
|
15933
|
+
"person",
|
|
15934
|
+
"PERSON"
|
|
15935
|
+
);
|
|
15936
|
+
const pathHint = options.pathHint?.toLowerCase() ?? "";
|
|
15937
|
+
if (input.length <= 120 && /(?:^|[^a-z0-9])(?:name|owner|recipient|contact|person)(?:$|[^a-z0-9])/.test(pathHint) && /^[A-Z][A-Za-z'-]+(?:\s+[A-Z][A-Za-z'-]+){1,3}$/.test(input.trim())) {
|
|
15938
|
+
const start = input.indexOf(input.trim());
|
|
15939
|
+
spans.push({
|
|
15940
|
+
class: "person",
|
|
15941
|
+
detectorClass: "person",
|
|
15942
|
+
start,
|
|
15943
|
+
end: start + input.trim().length,
|
|
15944
|
+
text: input.trim(),
|
|
15945
|
+
placeholderPrefix: "PERSON"
|
|
15946
|
+
});
|
|
15947
|
+
}
|
|
15948
|
+
for (const pattern of SPAN_PATTERNS) {
|
|
15949
|
+
pattern.pattern.lastIndex = 0;
|
|
15950
|
+
for (const match of input.matchAll(pattern.pattern)) {
|
|
15951
|
+
if (match.index === void 0 || match[0].length === 0) continue;
|
|
15952
|
+
spans.push({
|
|
15953
|
+
class: pattern.class,
|
|
15954
|
+
detectorClass: pattern.detectorClass ?? detectorClassForSpan(pattern.class),
|
|
15955
|
+
start: match.index,
|
|
15956
|
+
end: match.index + match[0].length,
|
|
15957
|
+
text: match[0],
|
|
15958
|
+
placeholderPrefix: pattern.placeholderPrefix
|
|
15959
|
+
});
|
|
15960
|
+
}
|
|
15961
|
+
}
|
|
15962
|
+
return removeOverlappingSpans(spans);
|
|
15963
|
+
}
|
|
15964
|
+
function detectorClassForSpan(spanClass) {
|
|
15965
|
+
switch (spanClass) {
|
|
15966
|
+
case "client":
|
|
15967
|
+
return "client";
|
|
15968
|
+
case "project":
|
|
15969
|
+
return "project";
|
|
15970
|
+
case "secret":
|
|
15971
|
+
case "secret_assignment":
|
|
15972
|
+
return "secret";
|
|
15973
|
+
case "credential":
|
|
15974
|
+
return "credential";
|
|
15975
|
+
case "account_number":
|
|
15976
|
+
case "credit_card":
|
|
15977
|
+
return "account";
|
|
15978
|
+
case "file_path":
|
|
15979
|
+
return "file_path";
|
|
15980
|
+
case "domain_term":
|
|
15981
|
+
return "domain_term";
|
|
15982
|
+
case "custom":
|
|
15983
|
+
return "custom";
|
|
15984
|
+
case "email":
|
|
15985
|
+
case "phone":
|
|
15986
|
+
case "ssn":
|
|
15987
|
+
case "address":
|
|
15988
|
+
case "person":
|
|
15989
|
+
case "url":
|
|
15990
|
+
case "date":
|
|
15991
|
+
default:
|
|
15992
|
+
return "person";
|
|
15993
|
+
}
|
|
15994
|
+
}
|
|
15995
|
+
function placeholderPrefixFor(spanClass) {
|
|
15996
|
+
return SPAN_PATTERNS.find((p) => p.class === spanClass)?.placeholderPrefix ?? OPENAI_LABEL_PREFIXES[spanClass] ?? CONTRACT_CLASS_PREFIXES[spanClass] ?? "PRIVATE";
|
|
15997
|
+
}
|
|
15998
|
+
function filterValue(value, path, findings, depth) {
|
|
15999
|
+
if (depth > MAX_DEPTH) return value;
|
|
16000
|
+
if (typeof value === "string") {
|
|
16001
|
+
return filterString(value, path, findings);
|
|
16002
|
+
}
|
|
16003
|
+
if (Array.isArray(value)) {
|
|
16004
|
+
return value.map(
|
|
16005
|
+
(item, index) => filterValue(item, `${path}[${index}]`, findings, depth + 1)
|
|
16006
|
+
);
|
|
16007
|
+
}
|
|
16008
|
+
if (value && typeof value === "object") {
|
|
16009
|
+
const filtered = {};
|
|
16010
|
+
for (const [key, child] of Object.entries(value)) {
|
|
16011
|
+
filtered[key] = filterValue(child, `${path}.${key}`, findings, depth + 1);
|
|
16012
|
+
}
|
|
16013
|
+
return filtered;
|
|
16014
|
+
}
|
|
16015
|
+
return value;
|
|
16016
|
+
}
|
|
16017
|
+
function filterString(input, path, findings) {
|
|
16018
|
+
let output = input;
|
|
16019
|
+
for (const span of SPAN_PATTERNS) {
|
|
16020
|
+
const before = output;
|
|
16021
|
+
output = output.replace(span.pattern, span.replacement);
|
|
16022
|
+
if (output !== before) {
|
|
16023
|
+
findings.push({ path, class: span.class, action: "redact" });
|
|
16024
|
+
}
|
|
16025
|
+
}
|
|
16026
|
+
return output;
|
|
16027
|
+
}
|
|
16028
|
+
async function placeholderValue(value, path, findings, vault, scope, depth) {
|
|
16029
|
+
if (depth > MAX_DEPTH) return value;
|
|
16030
|
+
if (typeof value === "string") {
|
|
16031
|
+
return placeholderString(value, path, findings, vault, scope);
|
|
16032
|
+
}
|
|
16033
|
+
if (Array.isArray(value)) {
|
|
16034
|
+
const out = [];
|
|
16035
|
+
for (let index = 0; index < value.length; index++) {
|
|
16036
|
+
out.push(await placeholderValue(
|
|
16037
|
+
value[index],
|
|
16038
|
+
`${path}[${index}]`,
|
|
16039
|
+
findings,
|
|
16040
|
+
vault,
|
|
16041
|
+
scope,
|
|
16042
|
+
depth + 1
|
|
16043
|
+
));
|
|
16044
|
+
}
|
|
16045
|
+
return out;
|
|
16046
|
+
}
|
|
16047
|
+
if (value && typeof value === "object") {
|
|
16048
|
+
const filtered = {};
|
|
16049
|
+
for (const [key, child] of Object.entries(value)) {
|
|
16050
|
+
filtered[key] = await placeholderValue(
|
|
16051
|
+
child,
|
|
16052
|
+
`${path}.${key}`,
|
|
16053
|
+
findings,
|
|
16054
|
+
vault,
|
|
16055
|
+
scope,
|
|
16056
|
+
depth + 1
|
|
16057
|
+
);
|
|
16058
|
+
}
|
|
16059
|
+
return filtered;
|
|
16060
|
+
}
|
|
16061
|
+
return value;
|
|
16062
|
+
}
|
|
16063
|
+
async function placeholderString(input, path, findings, vault, scope) {
|
|
16064
|
+
const spans = detectSensitiveSpans(input, { pathHint: path });
|
|
16065
|
+
if (spans.length === 0) return input;
|
|
16066
|
+
let cursor = 0;
|
|
16067
|
+
const pieces = [];
|
|
16068
|
+
for (const span of spans) {
|
|
16069
|
+
const placeholder = await vault.placeholderFor(span.class, span.text, scope);
|
|
16070
|
+
pieces.push(input.slice(cursor, span.start));
|
|
16071
|
+
pieces.push(placeholder);
|
|
16072
|
+
cursor = span.end;
|
|
16073
|
+
findings.push({
|
|
16074
|
+
path,
|
|
16075
|
+
class: span.class,
|
|
16076
|
+
action: "placeholder",
|
|
16077
|
+
placeholder
|
|
16078
|
+
});
|
|
16079
|
+
}
|
|
16080
|
+
pieces.push(input.slice(cursor));
|
|
16081
|
+
return pieces.join("");
|
|
16082
|
+
}
|
|
16083
|
+
var OPENAI_LABEL_PREFIXES = {
|
|
16084
|
+
account_number: "ACCOUNT",
|
|
16085
|
+
address: "ADDRESS",
|
|
16086
|
+
client: "CLIENT",
|
|
16087
|
+
credential: "CREDENTIAL",
|
|
16088
|
+
domain_term: "TERM",
|
|
16089
|
+
person: "PERSON",
|
|
16090
|
+
project: "PROJECT",
|
|
16091
|
+
file_path: "FILE_PATH",
|
|
16092
|
+
secret: "SECRET",
|
|
16093
|
+
url: "URL",
|
|
16094
|
+
date: "DATE"
|
|
16095
|
+
};
|
|
16096
|
+
var CONTRACT_CLASS_PREFIXES = {
|
|
16097
|
+
account_number: "ACCOUNT",
|
|
16098
|
+
client: "CLIENT",
|
|
16099
|
+
credential: "CREDENTIAL",
|
|
16100
|
+
domain_term: "TERM",
|
|
16101
|
+
file_path: "FILE_PATH",
|
|
16102
|
+
project: "PROJECT",
|
|
16103
|
+
secret: "SECRET"
|
|
16104
|
+
};
|
|
16105
|
+
function addConfiguredTermSpans(spans, input, terms, spanClass, detectorClass, placeholderPrefix) {
|
|
16106
|
+
const sortedTerms = [...new Set(terms.map((term) => term.trim()).filter(Boolean))].sort((a, b) => b.length - a.length);
|
|
16107
|
+
for (const term of sortedTerms) {
|
|
16108
|
+
const pattern = new RegExp(escapeRegExp(term), "gi");
|
|
16109
|
+
for (const match of input.matchAll(pattern)) {
|
|
16110
|
+
if (match.index === void 0 || match[0].length === 0) continue;
|
|
16111
|
+
spans.push({
|
|
16112
|
+
class: spanClass,
|
|
16113
|
+
detectorClass,
|
|
16114
|
+
start: match.index,
|
|
16115
|
+
end: match.index + match[0].length,
|
|
16116
|
+
text: match[0],
|
|
16117
|
+
placeholderPrefix
|
|
16118
|
+
});
|
|
16119
|
+
}
|
|
16120
|
+
}
|
|
16121
|
+
}
|
|
16122
|
+
function removeOverlappingSpans(spans) {
|
|
16123
|
+
const sorted = spans.sort((a, b) => {
|
|
16124
|
+
if (a.start !== b.start) return a.start - b.start;
|
|
16125
|
+
return b.end - b.start - (a.end - a.start);
|
|
16126
|
+
});
|
|
16127
|
+
const accepted = [];
|
|
16128
|
+
let cursor = -1;
|
|
16129
|
+
for (const span of sorted) {
|
|
16130
|
+
if (span.start < cursor) continue;
|
|
16131
|
+
accepted.push(span);
|
|
16132
|
+
cursor = span.end;
|
|
16133
|
+
}
|
|
16134
|
+
return accepted;
|
|
16135
|
+
}
|
|
16136
|
+
function escapeRegExp(input) {
|
|
16137
|
+
return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
16138
|
+
}
|
|
16139
|
+
function bytesToHex(bytes) {
|
|
16140
|
+
return Buffer.from(bytes).toString("hex");
|
|
16141
|
+
}
|
|
16142
|
+
function mapOpenAIPrivacyLabel(label) {
|
|
16143
|
+
switch (label) {
|
|
16144
|
+
case "account_number":
|
|
16145
|
+
return "account_number";
|
|
16146
|
+
case "private_address":
|
|
16147
|
+
return "address";
|
|
16148
|
+
case "private_email":
|
|
16149
|
+
return "email";
|
|
16150
|
+
case "private_person":
|
|
16151
|
+
return "person";
|
|
16152
|
+
case "private_phone":
|
|
16153
|
+
return "phone";
|
|
16154
|
+
case "private_url":
|
|
16155
|
+
return "url";
|
|
16156
|
+
case "private_date":
|
|
16157
|
+
return "date";
|
|
16158
|
+
case "secret":
|
|
16159
|
+
return "secret_assignment";
|
|
16160
|
+
case "redacted":
|
|
16161
|
+
return "secret_assignment";
|
|
16162
|
+
default:
|
|
16163
|
+
return null;
|
|
16164
|
+
}
|
|
16165
|
+
}
|
|
16166
|
+
|
|
16167
|
+
// src/l2-operational/context-gate-enforcer.ts
|
|
15324
16168
|
var BUILTIN_SENSITIVE_PATTERNS = [
|
|
15325
16169
|
"*_key",
|
|
15326
16170
|
"*_token",
|
|
@@ -15346,6 +16190,7 @@ var BUILTIN_SENSITIVE_PATTERNS = [
|
|
|
15346
16190
|
var ContextGateEnforcer = class {
|
|
15347
16191
|
policyStore;
|
|
15348
16192
|
auditLog;
|
|
16193
|
+
privacyVault;
|
|
15349
16194
|
config;
|
|
15350
16195
|
stats = {
|
|
15351
16196
|
calls_inspected: 0,
|
|
@@ -15355,9 +16200,10 @@ var ContextGateEnforcer = class {
|
|
|
15355
16200
|
fields_blocked: 0,
|
|
15356
16201
|
calls_blocked: 0
|
|
15357
16202
|
};
|
|
15358
|
-
constructor(policyStore, auditLog, config) {
|
|
16203
|
+
constructor(policyStore, auditLog, config, privacyVault) {
|
|
15359
16204
|
this.policyStore = policyStore;
|
|
15360
16205
|
this.auditLog = auditLog;
|
|
16206
|
+
this.privacyVault = privacyVault;
|
|
15361
16207
|
this.config = config;
|
|
15362
16208
|
}
|
|
15363
16209
|
/**
|
|
@@ -15434,6 +16280,7 @@ var ContextGateEnforcer = class {
|
|
|
15434
16280
|
}
|
|
15435
16281
|
}
|
|
15436
16282
|
const filteredArgs = this.buildFilteredArgs(args, result.decisions);
|
|
16283
|
+
const privacyFiltered = this.privacyVault ? await applyPrivacyPlaceholders(filteredArgs, this.privacyVault, policy.policy_id) : applyLocalPrivacyFilter(filteredArgs);
|
|
15437
16284
|
if (this.config.log_only) {
|
|
15438
16285
|
this.auditLog.append(
|
|
15439
16286
|
"l2",
|
|
@@ -15447,6 +16294,8 @@ var ContextGateEnforcer = class {
|
|
|
15447
16294
|
fields_redacted: result.fields_redacted,
|
|
15448
16295
|
fields_hashed: result.fields_hashed,
|
|
15449
16296
|
fields_blocked: deniedFields.length,
|
|
16297
|
+
privacy_findings: privacyFiltered.findings.length,
|
|
16298
|
+
privacy_classes: [...new Set(privacyFiltered.findings.map((f) => f.class))],
|
|
15450
16299
|
original_context_hash: result.original_context_hash
|
|
15451
16300
|
}
|
|
15452
16301
|
);
|
|
@@ -15467,13 +16316,15 @@ var ContextGateEnforcer = class {
|
|
|
15467
16316
|
fields_redacted: result.fields_redacted,
|
|
15468
16317
|
fields_hashed: result.fields_hashed,
|
|
15469
16318
|
fields_blocked: deniedFields.length,
|
|
16319
|
+
privacy_findings: privacyFiltered.findings.length,
|
|
16320
|
+
privacy_classes: [...new Set(privacyFiltered.findings.map((f) => f.class))],
|
|
15470
16321
|
original_context_hash: result.original_context_hash
|
|
15471
16322
|
}
|
|
15472
16323
|
);
|
|
15473
16324
|
this.stats.fields_redacted += result.fields_redacted;
|
|
15474
16325
|
this.stats.fields_hashed += result.fields_hashed;
|
|
15475
16326
|
this.stats.fields_blocked += deniedFields.length;
|
|
15476
|
-
return originalHandler(
|
|
16327
|
+
return originalHandler(privacyFiltered.value);
|
|
15477
16328
|
}
|
|
15478
16329
|
/**
|
|
15479
16330
|
* Filter tool arguments using built-in sensitive patterns.
|
|
@@ -15490,6 +16341,39 @@ var ContextGateEnforcer = class {
|
|
|
15490
16341
|
}
|
|
15491
16342
|
}
|
|
15492
16343
|
if (fieldsToRedact.length === 0) {
|
|
16344
|
+
const privacyFiltered2 = this.privacyVault ? await applyPrivacyPlaceholders(args, this.privacyVault, `builtin:${toolName}`) : applyLocalPrivacyFilter(args);
|
|
16345
|
+
if (privacyFiltered2.findings.length > 0) {
|
|
16346
|
+
const filteredHash2 = hashToString(
|
|
16347
|
+
stringToBytes(JSON.stringify(privacyFiltered2.value))
|
|
16348
|
+
);
|
|
16349
|
+
if (this.config.log_only) {
|
|
16350
|
+
this.auditLog.append(
|
|
16351
|
+
"l2",
|
|
16352
|
+
"context_gate_enforcer_builtin_privacy_log_only",
|
|
16353
|
+
"system",
|
|
16354
|
+
{
|
|
16355
|
+
tool_name: toolName,
|
|
16356
|
+
privacy_findings: privacyFiltered2.findings.length,
|
|
16357
|
+
privacy_classes: [...new Set(privacyFiltered2.findings.map((f) => f.class))],
|
|
16358
|
+
original_context_hash: originalHash
|
|
16359
|
+
}
|
|
16360
|
+
);
|
|
16361
|
+
return originalHandler(args);
|
|
16362
|
+
}
|
|
16363
|
+
this.auditLog.append(
|
|
16364
|
+
"l2",
|
|
16365
|
+
"context_gate_enforcer_builtin_privacy_filter",
|
|
16366
|
+
"system",
|
|
16367
|
+
{
|
|
16368
|
+
tool_name: toolName,
|
|
16369
|
+
privacy_findings: privacyFiltered2.findings.length,
|
|
16370
|
+
privacy_classes: [...new Set(privacyFiltered2.findings.map((f) => f.class))],
|
|
16371
|
+
original_context_hash: originalHash,
|
|
16372
|
+
filtered_context_hash: filteredHash2
|
|
16373
|
+
}
|
|
16374
|
+
);
|
|
16375
|
+
return originalHandler(privacyFiltered2.value);
|
|
16376
|
+
}
|
|
15493
16377
|
this.auditLog.append(
|
|
15494
16378
|
"l2",
|
|
15495
16379
|
"context_gate_enforcer_builtin_pass",
|
|
@@ -15509,8 +16393,9 @@ var ContextGateEnforcer = class {
|
|
|
15509
16393
|
filteredArgs[key] = value;
|
|
15510
16394
|
}
|
|
15511
16395
|
}
|
|
16396
|
+
const privacyFiltered = this.privacyVault ? await applyPrivacyPlaceholders(filteredArgs, this.privacyVault, `builtin:${toolName}`) : applyLocalPrivacyFilter(filteredArgs);
|
|
15512
16397
|
const filteredHash = hashToString(
|
|
15513
|
-
stringToBytes(JSON.stringify(
|
|
16398
|
+
stringToBytes(JSON.stringify(privacyFiltered.value))
|
|
15514
16399
|
);
|
|
15515
16400
|
if (this.config.log_only) {
|
|
15516
16401
|
this.auditLog.append(
|
|
@@ -15521,6 +16406,8 @@ var ContextGateEnforcer = class {
|
|
|
15521
16406
|
tool_name: toolName,
|
|
15522
16407
|
fields_redacted: fieldsToRedact.length,
|
|
15523
16408
|
redacted_fields: fieldsToRedact,
|
|
16409
|
+
privacy_findings: privacyFiltered.findings.length,
|
|
16410
|
+
privacy_classes: [...new Set(privacyFiltered.findings.map((f) => f.class))],
|
|
15524
16411
|
original_context_hash: originalHash
|
|
15525
16412
|
}
|
|
15526
16413
|
);
|
|
@@ -15535,12 +16422,14 @@ var ContextGateEnforcer = class {
|
|
|
15535
16422
|
tool_name: toolName,
|
|
15536
16423
|
fields_redacted: fieldsToRedact.length,
|
|
15537
16424
|
redacted_fields: fieldsToRedact,
|
|
16425
|
+
privacy_findings: privacyFiltered.findings.length,
|
|
16426
|
+
privacy_classes: [...new Set(privacyFiltered.findings.map((f) => f.class))],
|
|
15538
16427
|
original_context_hash: originalHash,
|
|
15539
16428
|
filtered_context_hash: filteredHash
|
|
15540
16429
|
}
|
|
15541
16430
|
);
|
|
15542
16431
|
this.stats.fields_redacted += fieldsToRedact.length;
|
|
15543
|
-
return originalHandler(
|
|
16432
|
+
return originalHandler(privacyFiltered.value);
|
|
15544
16433
|
}
|
|
15545
16434
|
/**
|
|
15546
16435
|
* Check if a tool should be filtered based on bypass prefixes.
|
|
@@ -15646,21 +16535,166 @@ var ContextGateEnforcer = class {
|
|
|
15646
16535
|
};
|
|
15647
16536
|
}
|
|
15648
16537
|
};
|
|
15649
|
-
|
|
15650
|
-
|
|
15651
|
-
|
|
15652
|
-
|
|
15653
|
-
|
|
15654
|
-
|
|
15655
|
-
|
|
15656
|
-
|
|
15657
|
-
|
|
15658
|
-
|
|
16538
|
+
var PrivacyFilterRuntimeError = class extends Error {
|
|
16539
|
+
constructor(message) {
|
|
16540
|
+
super(message);
|
|
16541
|
+
this.name = "PrivacyFilterRuntimeError";
|
|
16542
|
+
}
|
|
16543
|
+
};
|
|
16544
|
+
async function applyConfiguredPrivacyFilter(value, vault, scope, config) {
|
|
16545
|
+
if (config.mode === "off") {
|
|
16546
|
+
return { value, findings: [], mode: "off" };
|
|
16547
|
+
}
|
|
16548
|
+
if (config.mode === "local") {
|
|
16549
|
+
const result = await applyPrivacyPlaceholders(value, vault, scope);
|
|
16550
|
+
return { ...result, mode: "local" };
|
|
16551
|
+
}
|
|
16552
|
+
try {
|
|
16553
|
+
const result = await applyOpenAIPrivacyFilter(value, vault, scope, config);
|
|
16554
|
+
return { ...result, mode: "opf" };
|
|
16555
|
+
} catch (err) {
|
|
16556
|
+
if (config.fail_mode === "fallback") {
|
|
16557
|
+
const result = await applyPrivacyPlaceholders(value, vault, scope);
|
|
16558
|
+
return { ...result, mode: "local", fallback_from: "opf" };
|
|
16559
|
+
}
|
|
16560
|
+
throw err;
|
|
16561
|
+
}
|
|
16562
|
+
}
|
|
16563
|
+
async function applyOpenAIPrivacyFilter(value, vault, scope, config) {
|
|
16564
|
+
if (typeof value === "string") {
|
|
16565
|
+
const opf = await runOpenAIPrivacyFilter(value, config);
|
|
16566
|
+
const filtered = await applyOpenAIPrivacyFilterResult(opf, vault, scope);
|
|
16567
|
+
return filtered;
|
|
16568
|
+
}
|
|
16569
|
+
if (Array.isArray(value)) {
|
|
16570
|
+
const findings = [];
|
|
16571
|
+
const out = [];
|
|
16572
|
+
for (let index = 0; index < value.length; index++) {
|
|
16573
|
+
const filtered = await applyOpenAIPrivacyFilter(
|
|
16574
|
+
value[index],
|
|
16575
|
+
vault,
|
|
16576
|
+
scope,
|
|
16577
|
+
config
|
|
16578
|
+
);
|
|
16579
|
+
out.push(filtered.value);
|
|
16580
|
+
findings.push(...filtered.findings.map((f) => ({
|
|
16581
|
+
...f,
|
|
16582
|
+
path: `$[${index}]${f.path === "$" ? "" : f.path.slice(1)}`
|
|
16583
|
+
})));
|
|
16584
|
+
}
|
|
16585
|
+
return { value: out, findings };
|
|
16586
|
+
}
|
|
16587
|
+
if (value && typeof value === "object") {
|
|
16588
|
+
const findings = [];
|
|
16589
|
+
const out = {};
|
|
16590
|
+
for (const [key, child] of Object.entries(value)) {
|
|
16591
|
+
const filtered = await applyOpenAIPrivacyFilter(child, vault, scope, config);
|
|
16592
|
+
out[key] = filtered.value;
|
|
16593
|
+
findings.push(...filtered.findings.map((f) => ({
|
|
16594
|
+
...f,
|
|
16595
|
+
path: `$.${key}${f.path === "$" ? "" : f.path.slice(1)}`
|
|
16596
|
+
})));
|
|
16597
|
+
}
|
|
16598
|
+
return { value: out, findings };
|
|
16599
|
+
}
|
|
16600
|
+
return { value, findings: [] };
|
|
16601
|
+
}
|
|
16602
|
+
var OPF_STDOUT_MAX_BYTES = 1e7;
|
|
16603
|
+
async function runOpenAIPrivacyFilter(text, config) {
|
|
16604
|
+
const stdout = await runCommand(config.command, text, config.timeout_ms);
|
|
16605
|
+
if (Buffer.byteLength(stdout, "utf8") > OPF_STDOUT_MAX_BYTES) {
|
|
16606
|
+
throw new PrivacyFilterRuntimeError(
|
|
16607
|
+
`OpenAI privacy-filter stdout exceeded ${OPF_STDOUT_MAX_BYTES}-byte cap`
|
|
16608
|
+
);
|
|
16609
|
+
}
|
|
16610
|
+
let parsed;
|
|
16611
|
+
try {
|
|
16612
|
+
parsed = JSON.parse(stdout);
|
|
16613
|
+
} catch {
|
|
16614
|
+
throw new PrivacyFilterRuntimeError("OpenAI privacy-filter returned invalid JSON");
|
|
16615
|
+
}
|
|
16616
|
+
if (!isOpenAIPrivacyFilterResult(parsed)) {
|
|
16617
|
+
throw new PrivacyFilterRuntimeError(
|
|
16618
|
+
"OpenAI privacy-filter JSON did not match the expected span schema"
|
|
16619
|
+
);
|
|
16620
|
+
}
|
|
16621
|
+
return parsed;
|
|
16622
|
+
}
|
|
16623
|
+
function runCommand(command, input, timeoutMs) {
|
|
16624
|
+
return new Promise((resolve4, reject) => {
|
|
16625
|
+
const child = spawn(command, [], {
|
|
16626
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
16627
|
+
shell: false
|
|
16628
|
+
});
|
|
16629
|
+
let stdout = "";
|
|
16630
|
+
let stderr = "";
|
|
16631
|
+
const timer = setTimeout(() => {
|
|
16632
|
+
child.kill("SIGTERM");
|
|
16633
|
+
reject(new PrivacyFilterRuntimeError("OpenAI privacy-filter timed out"));
|
|
16634
|
+
}, timeoutMs);
|
|
16635
|
+
child.stdout.setEncoding("utf8");
|
|
16636
|
+
child.stderr.setEncoding("utf8");
|
|
16637
|
+
child.stdout.on("data", (chunk) => {
|
|
16638
|
+
stdout += chunk;
|
|
16639
|
+
});
|
|
16640
|
+
child.stderr.on("data", (chunk) => {
|
|
16641
|
+
stderr += chunk;
|
|
16642
|
+
});
|
|
16643
|
+
child.on("error", (err) => {
|
|
16644
|
+
clearTimeout(timer);
|
|
16645
|
+
reject(new PrivacyFilterRuntimeError(`OpenAI privacy-filter failed to start: ${err.message}`));
|
|
16646
|
+
});
|
|
16647
|
+
child.on("close", (code) => {
|
|
16648
|
+
clearTimeout(timer);
|
|
16649
|
+
if (code !== 0) {
|
|
16650
|
+
reject(new PrivacyFilterRuntimeError(
|
|
16651
|
+
`OpenAI privacy-filter exited with code ${code}: ${stderr.trim()}`
|
|
16652
|
+
));
|
|
16653
|
+
return;
|
|
16654
|
+
}
|
|
16655
|
+
resolve4(stdout);
|
|
16656
|
+
});
|
|
16657
|
+
child.stdin.end(input);
|
|
16658
|
+
});
|
|
16659
|
+
}
|
|
16660
|
+
function isOpenAIPrivacyFilterResult(value) {
|
|
16661
|
+
if (!value || typeof value !== "object") return false;
|
|
16662
|
+
const v = value;
|
|
16663
|
+
if (typeof v.text !== "string") return false;
|
|
16664
|
+
if (!Array.isArray(v.detected_spans)) return false;
|
|
16665
|
+
return v.detected_spans.every((span) => {
|
|
16666
|
+
if (!span || typeof span !== "object") return false;
|
|
16667
|
+
const s = span;
|
|
16668
|
+
return typeof s.label === "string" && typeof s.start === "number" && typeof s.end === "number" && typeof s.text === "string";
|
|
16669
|
+
});
|
|
16670
|
+
}
|
|
16671
|
+
|
|
16672
|
+
// src/l2-operational/context-gate-tools.ts
|
|
16673
|
+
function createContextGateTools(storage, masterKey, auditLog, options = {}) {
|
|
16674
|
+
const policyStore = new ContextGatePolicyStore(storage, masterKey);
|
|
16675
|
+
const privacyVault = new PrivacyPlaceholderVault(storage, masterKey);
|
|
16676
|
+
const privacyFilterConfig = options.privacyFilter ?? {
|
|
16677
|
+
mode: "local",
|
|
16678
|
+
fail_mode: "closed",
|
|
16679
|
+
command: "opf",
|
|
16680
|
+
timeout_ms: 5e3
|
|
16681
|
+
};
|
|
16682
|
+
const enforcerConfig = {
|
|
16683
|
+
enabled: false,
|
|
16684
|
+
// Off by default; agents must explicitly enable it
|
|
16685
|
+
bypass_prefixes: ["*"],
|
|
16686
|
+
// Skip all Sanctuary-internal tools; only proxy/ tools get filtered
|
|
16687
|
+
log_only: false,
|
|
15659
16688
|
// Filter immediately
|
|
15660
16689
|
on_deny: "block"
|
|
15661
16690
|
// Block requests with denied fields
|
|
15662
16691
|
};
|
|
15663
|
-
const enforcer = new ContextGateEnforcer(
|
|
16692
|
+
const enforcer = new ContextGateEnforcer(
|
|
16693
|
+
policyStore,
|
|
16694
|
+
auditLog,
|
|
16695
|
+
enforcerConfig,
|
|
16696
|
+
privacyVault
|
|
16697
|
+
);
|
|
15664
16698
|
const tools = [
|
|
15665
16699
|
// ── Set Policy ──────────────────────────────────────────────────
|
|
15666
16700
|
{
|
|
@@ -15954,6 +16988,34 @@ function createContextGateTools(storage, masterKey, auditLog) {
|
|
|
15954
16988
|
break;
|
|
15955
16989
|
}
|
|
15956
16990
|
}
|
|
16991
|
+
let privacyFiltered;
|
|
16992
|
+
try {
|
|
16993
|
+
privacyFiltered = await applyConfiguredPrivacyFilter(
|
|
16994
|
+
safeContext,
|
|
16995
|
+
privacyVault,
|
|
16996
|
+
policyId,
|
|
16997
|
+
privacyFilterConfig
|
|
16998
|
+
);
|
|
16999
|
+
} catch (err) {
|
|
17000
|
+
if (err instanceof PrivacyFilterRuntimeError) {
|
|
17001
|
+
auditLog.append("l2", "context_gate_privacy_filter_failure", policy.identity_id ?? "system", {
|
|
17002
|
+
policy_id: policyId,
|
|
17003
|
+
provider,
|
|
17004
|
+
mode: privacyFilterConfig.mode,
|
|
17005
|
+
fail_mode: privacyFilterConfig.fail_mode,
|
|
17006
|
+
original_context_hash: result.original_context_hash,
|
|
17007
|
+
error: err.message
|
|
17008
|
+
}, "failure");
|
|
17009
|
+
return toolResult({
|
|
17010
|
+
blocked: true,
|
|
17011
|
+
error: "privacy_filter_failed",
|
|
17012
|
+
message: err.message,
|
|
17013
|
+
mode: privacyFilterConfig.mode,
|
|
17014
|
+
recommendation: privacyFilterConfig.fail_mode === "closed" ? "Install/configure the local privacy filter or switch SANCTUARY_PRIVACY_FILTER_FAIL_MODE to fallback." : "Check the local privacy filter configuration."
|
|
17015
|
+
});
|
|
17016
|
+
}
|
|
17017
|
+
throw err;
|
|
17018
|
+
}
|
|
15957
17019
|
auditLog.append("l2", "context_gate_filter", policy.identity_id ?? "system", {
|
|
15958
17020
|
policy_id: policyId,
|
|
15959
17021
|
provider,
|
|
@@ -15962,20 +17024,32 @@ function createContextGateTools(storage, masterKey, auditLog) {
|
|
|
15962
17024
|
fields_redacted: result.fields_redacted,
|
|
15963
17025
|
fields_hashed: result.fields_hashed,
|
|
15964
17026
|
fields_summarized: result.fields_summarized,
|
|
17027
|
+
privacy_findings: privacyFiltered.findings.length,
|
|
17028
|
+
privacy_classes: [...new Set(privacyFiltered.findings.map((f) => f.class))],
|
|
17029
|
+
privacy_filter_mode: privacyFiltered.mode,
|
|
17030
|
+
privacy_filter_configured_mode: privacyFilterConfig.mode,
|
|
17031
|
+
privacy_filter_fallback_from: privacyFiltered.fallback_from,
|
|
15965
17032
|
original_context_hash: result.original_context_hash,
|
|
15966
17033
|
filtered_context_hash: result.filtered_context_hash
|
|
15967
17034
|
});
|
|
15968
17035
|
return toolResult({
|
|
15969
17036
|
blocked: false,
|
|
15970
|
-
safe_context:
|
|
17037
|
+
safe_context: privacyFiltered.value,
|
|
15971
17038
|
summary: {
|
|
15972
17039
|
total_fields: Object.keys(context).length,
|
|
15973
17040
|
allowed: result.fields_allowed,
|
|
15974
17041
|
redacted: result.fields_redacted,
|
|
15975
17042
|
hashed: result.fields_hashed,
|
|
15976
|
-
summarized: result.fields_summarized
|
|
17043
|
+
summarized: result.fields_summarized,
|
|
17044
|
+
privacy_filtered_spans: privacyFiltered.findings.length
|
|
15977
17045
|
},
|
|
15978
17046
|
decisions: result.decisions,
|
|
17047
|
+
privacy_filter: {
|
|
17048
|
+
mode: privacyFiltered.mode,
|
|
17049
|
+
configured_mode: privacyFilterConfig.mode,
|
|
17050
|
+
fallback_from: privacyFiltered.fallback_from,
|
|
17051
|
+
findings: privacyFiltered.findings
|
|
17052
|
+
},
|
|
15979
17053
|
audit: {
|
|
15980
17054
|
original_context_hash: result.original_context_hash,
|
|
15981
17055
|
filtered_context_hash: result.filtered_context_hash,
|
|
@@ -17256,6 +18330,55 @@ var ProxyRouter = class {
|
|
|
17256
18330
|
return toolResult(govResult.cached_result ?? {});
|
|
17257
18331
|
}
|
|
17258
18332
|
}
|
|
18333
|
+
let privacyPolicy = null;
|
|
18334
|
+
let privacyDestination = "tool-api";
|
|
18335
|
+
let outboundFiltered = false;
|
|
18336
|
+
if (this.options.privacyEnforcement) {
|
|
18337
|
+
const serverConfig = this.clientManager.getServerConfig(serverName);
|
|
18338
|
+
privacyDestination = serverConfig?.destination_category ?? "tool-api";
|
|
18339
|
+
const identityId = serverConfig?.privacy_identity_id;
|
|
18340
|
+
try {
|
|
18341
|
+
privacyPolicy = await this.options.privacyEnforcement.policyResolver(
|
|
18342
|
+
serverName,
|
|
18343
|
+
identityId
|
|
18344
|
+
);
|
|
18345
|
+
} catch {
|
|
18346
|
+
privacyPolicy = null;
|
|
18347
|
+
}
|
|
18348
|
+
const decision = await this.options.privacyEnforcement.engine.filterOutbound({
|
|
18349
|
+
payload: filteredArgs,
|
|
18350
|
+
policy: privacyPolicy,
|
|
18351
|
+
identity_id: identityId,
|
|
18352
|
+
agent_id: `proxy:${serverName}`,
|
|
18353
|
+
destination_category: privacyDestination,
|
|
18354
|
+
audit_log: this.auditLog
|
|
18355
|
+
});
|
|
18356
|
+
if (decision.status === "denied") {
|
|
18357
|
+
this.auditLog.append("l2", `proxy_privacy_denied:${proxyName}`, "system", {
|
|
18358
|
+
server: serverName,
|
|
18359
|
+
tool: toolName,
|
|
18360
|
+
tier,
|
|
18361
|
+
denial_reason_class: decision.audit_payload.denial_reason_class,
|
|
18362
|
+
latency_ms: Date.now() - start
|
|
18363
|
+
}, "failure");
|
|
18364
|
+
this.notifyProxyCall(
|
|
18365
|
+
proxyName,
|
|
18366
|
+
serverName,
|
|
18367
|
+
"blocked",
|
|
18368
|
+
"privacy_denied",
|
|
18369
|
+
tier
|
|
18370
|
+
);
|
|
18371
|
+
return toolResult({
|
|
18372
|
+
error: "Operation not permitted",
|
|
18373
|
+
proxy: true,
|
|
18374
|
+
privacy_denied: true
|
|
18375
|
+
});
|
|
18376
|
+
}
|
|
18377
|
+
if (decision.status === "filtered") {
|
|
18378
|
+
outboundFiltered = true;
|
|
18379
|
+
filteredArgs = decision.payload;
|
|
18380
|
+
}
|
|
18381
|
+
}
|
|
17259
18382
|
const result = await this.callWithTimeout(
|
|
17260
18383
|
serverName,
|
|
17261
18384
|
toolName,
|
|
@@ -17274,6 +18397,26 @@ var ProxyRouter = class {
|
|
|
17274
18397
|
latency_ms: latencyMs
|
|
17275
18398
|
});
|
|
17276
18399
|
this.notifyProxyCall(proxyName, serverName, "allowed", void 0, tier);
|
|
18400
|
+
if (outboundFiltered && this.options.privacyEnforcement && privacyPolicy) {
|
|
18401
|
+
const serverConfig = this.clientManager.getServerConfig(serverName);
|
|
18402
|
+
const identityId = serverConfig?.privacy_identity_id;
|
|
18403
|
+
const rehydrated = await this.options.privacyEnforcement.engine.rehydrateResponse({
|
|
18404
|
+
response: result,
|
|
18405
|
+
policy: privacyPolicy,
|
|
18406
|
+
identity_id: identityId,
|
|
18407
|
+
agent_id: `proxy:${serverName}`,
|
|
18408
|
+
destination_category: privacyDestination,
|
|
18409
|
+
audit_log: this.auditLog
|
|
18410
|
+
});
|
|
18411
|
+
if (rehydrated.status === "rehydrated") {
|
|
18412
|
+
return this.normalizeResponse(
|
|
18413
|
+
rehydrated.response
|
|
18414
|
+
);
|
|
18415
|
+
}
|
|
18416
|
+
return this.normalizeResponse(
|
|
18417
|
+
rehydrated.response
|
|
18418
|
+
);
|
|
18419
|
+
}
|
|
17277
18420
|
return this.normalizeResponse(result);
|
|
17278
18421
|
} catch (err) {
|
|
17279
18422
|
const latencyMs = Date.now() - start;
|
|
@@ -17330,13 +18473,13 @@ var ProxyRouter = class {
|
|
|
17330
18473
|
* Call an upstream tool with a timeout.
|
|
17331
18474
|
*/
|
|
17332
18475
|
async callWithTimeout(serverName, toolName, args, timeoutMs) {
|
|
17333
|
-
return new Promise((
|
|
18476
|
+
return new Promise((resolve4, reject) => {
|
|
17334
18477
|
const timer = setTimeout(() => {
|
|
17335
18478
|
reject(new Error(`Upstream tool call timed out after ${timeoutMs}ms`));
|
|
17336
18479
|
}, timeoutMs);
|
|
17337
18480
|
this.clientManager.callTool(serverName, toolName, args).then((result) => {
|
|
17338
18481
|
clearTimeout(timer);
|
|
17339
|
-
|
|
18482
|
+
resolve4(result);
|
|
17340
18483
|
}).catch((err) => {
|
|
17341
18484
|
clearTimeout(timer);
|
|
17342
18485
|
reject(err);
|
|
@@ -17378,7 +18521,7 @@ var ProxyRouter = class {
|
|
|
17378
18521
|
function strToBytes(s) {
|
|
17379
18522
|
return new TextEncoder().encode(s);
|
|
17380
18523
|
}
|
|
17381
|
-
function
|
|
18524
|
+
function bytesToHex2(bytes) {
|
|
17382
18525
|
let hex = "";
|
|
17383
18526
|
for (let i = 0; i < bytes.length; i++) {
|
|
17384
18527
|
hex += bytes[i].toString(16).padStart(2, "0");
|
|
@@ -17386,7 +18529,7 @@ function bytesToHex(bytes) {
|
|
|
17386
18529
|
return hex;
|
|
17387
18530
|
}
|
|
17388
18531
|
function sha256Hex(input) {
|
|
17389
|
-
return
|
|
18532
|
+
return bytesToHex2(sha256(strToBytes(input)));
|
|
17390
18533
|
}
|
|
17391
18534
|
var DEFAULT_CONFIG = {
|
|
17392
18535
|
volume_limit: 200,
|
|
@@ -18100,18 +19243,18 @@ function createSanctuaryTools(opts) {
|
|
|
18100
19243
|
const tagBytes = enc.encode(domainTag);
|
|
18101
19244
|
const purposeBytes = enc.encode(purpose);
|
|
18102
19245
|
const nonceBytes = enc.encode(nonce);
|
|
18103
|
-
const
|
|
19246
|
+
const sep2 = new Uint8Array([0]);
|
|
18104
19247
|
const message = new Uint8Array(
|
|
18105
19248
|
tagBytes.length + 1 + purposeBytes.length + 1 + nonceBytes.length
|
|
18106
19249
|
);
|
|
18107
19250
|
let offset = 0;
|
|
18108
19251
|
message.set(tagBytes, offset);
|
|
18109
19252
|
offset += tagBytes.length;
|
|
18110
|
-
message.set(
|
|
19253
|
+
message.set(sep2, offset);
|
|
18111
19254
|
offset += 1;
|
|
18112
19255
|
message.set(purposeBytes, offset);
|
|
18113
19256
|
offset += purposeBytes.length;
|
|
18114
|
-
message.set(
|
|
19257
|
+
message.set(sep2, offset);
|
|
18115
19258
|
offset += 1;
|
|
18116
19259
|
message.set(nonceBytes, offset);
|
|
18117
19260
|
let sigB64;
|
|
@@ -20411,7 +21554,7 @@ function classifyAgentDescription(description) {
|
|
|
20411
21554
|
}
|
|
20412
21555
|
|
|
20413
21556
|
// src/compliance/eu_ai_act/generator.ts
|
|
20414
|
-
function
|
|
21557
|
+
function bytesToHex3(bytes) {
|
|
20415
21558
|
let hex = "";
|
|
20416
21559
|
for (let i = 0; i < bytes.length; i++) {
|
|
20417
21560
|
hex += bytes[i].toString(16).padStart(2, "0");
|
|
@@ -20575,18 +21718,18 @@ function makeSigner(signer, masterKey) {
|
|
|
20575
21718
|
const sig = sign(digest, signer.encrypted_private_key, encryptionKey);
|
|
20576
21719
|
return toBase64url(sig);
|
|
20577
21720
|
},
|
|
20578
|
-
sha256Hex: (content) =>
|
|
21721
|
+
sha256Hex: (content) => bytesToHex3(hash(stringToBytes(content)))
|
|
20579
21722
|
};
|
|
20580
21723
|
}
|
|
20581
21724
|
function finaliseDocument(template, context, filename, ds) {
|
|
20582
21725
|
const content = render(template, context);
|
|
20583
|
-
const
|
|
21726
|
+
const sha256Hex4 = ds.sha256Hex(content);
|
|
20584
21727
|
const signature = ds.signContent(content);
|
|
20585
21728
|
return {
|
|
20586
21729
|
filename,
|
|
20587
21730
|
content,
|
|
20588
21731
|
content_type: "text/markdown",
|
|
20589
|
-
sha256:
|
|
21732
|
+
sha256: sha256Hex4,
|
|
20590
21733
|
signature
|
|
20591
21734
|
};
|
|
20592
21735
|
}
|
|
@@ -21384,102 +22527,1566 @@ var MemoryStorage = class {
|
|
|
21384
22527
|
}
|
|
21385
22528
|
};
|
|
21386
22529
|
|
|
21387
|
-
// src/
|
|
21388
|
-
var
|
|
21389
|
-
|
|
21390
|
-
|
|
21391
|
-
|
|
22530
|
+
// src/contracts/v1.1/constants.ts
|
|
22531
|
+
var SIGNATURE_SCHEME_V1 = "ed25519-v1";
|
|
22532
|
+
var EXIT_BUNDLE_MANIFEST_VERSION = "SANCTUARY_EXIT_BUNDLE_V1";
|
|
22533
|
+
var EXIT_BUNDLE_ARTIFACT_KINDS = [
|
|
22534
|
+
"public_identity",
|
|
22535
|
+
"encrypted_state",
|
|
22536
|
+
"policy_set",
|
|
22537
|
+
"audit_receipts",
|
|
22538
|
+
"reputation_bundle",
|
|
22539
|
+
"commitments",
|
|
22540
|
+
"placeholder_vault_metadata"
|
|
22541
|
+
];
|
|
22542
|
+
|
|
22543
|
+
// src/mesh/errors.ts
|
|
22544
|
+
var MeshError = class extends Error {
|
|
22545
|
+
constructor(message) {
|
|
22546
|
+
super(message);
|
|
22547
|
+
this.name = "MeshError";
|
|
22548
|
+
}
|
|
21392
22549
|
};
|
|
21393
|
-
|
|
21394
|
-
|
|
21395
|
-
|
|
21396
|
-
|
|
21397
|
-
score -= L4_DEGRADATION_IMPACT[deg.severity] ?? 10;
|
|
22550
|
+
var MeshEnvelopeError = class extends MeshError {
|
|
22551
|
+
constructor(message) {
|
|
22552
|
+
super(message);
|
|
22553
|
+
this.name = "MeshEnvelopeError";
|
|
21398
22554
|
}
|
|
21399
|
-
|
|
21400
|
-
|
|
21401
|
-
|
|
22555
|
+
};
|
|
22556
|
+
var MeshReservedExtensionKeyError = class extends MeshEnvelopeError {
|
|
22557
|
+
constructor(key) {
|
|
22558
|
+
super(
|
|
22559
|
+
`v0.1 emitters MUST NOT populate reserved extension_envelope key: ${key}`
|
|
22560
|
+
);
|
|
22561
|
+
this.name = "MeshReservedExtensionKeyError";
|
|
21402
22562
|
}
|
|
21403
|
-
|
|
22563
|
+
};
|
|
22564
|
+
var MeshReservedEventTypeError = class extends MeshEnvelopeError {
|
|
22565
|
+
constructor(eventType) {
|
|
22566
|
+
super(
|
|
22567
|
+
`v0.1 emitters MUST NOT emit reserved-namespace event_type: ${eventType}`
|
|
22568
|
+
);
|
|
22569
|
+
this.name = "MeshReservedEventTypeError";
|
|
22570
|
+
}
|
|
22571
|
+
};
|
|
22572
|
+
|
|
22573
|
+
// src/mesh/canonical-json.ts
|
|
22574
|
+
var MeshCanonicalJsonError = class extends MeshError {
|
|
22575
|
+
constructor(message) {
|
|
22576
|
+
super(message);
|
|
22577
|
+
this.name = "MeshCanonicalJsonError";
|
|
22578
|
+
}
|
|
22579
|
+
};
|
|
22580
|
+
function canonicalize2(value) {
|
|
22581
|
+
if (value === void 0) {
|
|
22582
|
+
throw new MeshCanonicalJsonError(
|
|
22583
|
+
"canonicalize(): top-level undefined is not serializable"
|
|
22584
|
+
);
|
|
22585
|
+
}
|
|
22586
|
+
return encode(value);
|
|
21404
22587
|
}
|
|
21405
|
-
|
|
21406
|
-
|
|
21407
|
-
|
|
21408
|
-
|
|
21409
|
-
|
|
21410
|
-
|
|
22588
|
+
function encode(value) {
|
|
22589
|
+
if (value === null) return "null";
|
|
22590
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
22591
|
+
if (typeof value === "number") {
|
|
22592
|
+
if (!Number.isFinite(value)) {
|
|
22593
|
+
throw new MeshCanonicalJsonError(
|
|
22594
|
+
`canonicalize(): non-finite number (${String(value)}) is not serializable`
|
|
22595
|
+
);
|
|
22596
|
+
}
|
|
22597
|
+
return JSON.stringify(value);
|
|
22598
|
+
}
|
|
22599
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
22600
|
+
if (Array.isArray(value)) return encodeArray(value);
|
|
22601
|
+
if (typeof value === "object") return encodeObject(value);
|
|
22602
|
+
throw new MeshCanonicalJsonError(
|
|
22603
|
+
`canonicalize(): unsupported type ${typeof value}`
|
|
22604
|
+
);
|
|
21411
22605
|
}
|
|
21412
|
-
function
|
|
21413
|
-
const
|
|
21414
|
-
|
|
21415
|
-
|
|
21416
|
-
|
|
21417
|
-
|
|
21418
|
-
if (isNaN(ts) || ts < cutoff) return false;
|
|
21419
|
-
const op = (e.operation ?? "").toLowerCase();
|
|
21420
|
-
return op.includes("injection") || op.includes("blocked");
|
|
21421
|
-
}).length;
|
|
22606
|
+
function encodeArray(arr) {
|
|
22607
|
+
const parts = [];
|
|
22608
|
+
for (const item of arr) {
|
|
22609
|
+
parts.push(item === void 0 ? "null" : encode(item));
|
|
22610
|
+
}
|
|
22611
|
+
return "[" + parts.join(",") + "]";
|
|
21422
22612
|
}
|
|
21423
|
-
|
|
21424
|
-
|
|
21425
|
-
|
|
21426
|
-
|
|
22613
|
+
function encodeObject(obj) {
|
|
22614
|
+
const keys = Object.keys(obj).filter((k) => obj[k] !== void 0).sort();
|
|
22615
|
+
const parts = [];
|
|
22616
|
+
for (const k of keys) {
|
|
22617
|
+
parts.push(JSON.stringify(k) + ":" + encode(obj[k]));
|
|
22618
|
+
}
|
|
22619
|
+
return "{" + parts.join(",") + "}";
|
|
22620
|
+
}
|
|
22621
|
+
function canonicalizeToBytes(value) {
|
|
22622
|
+
return new TextEncoder().encode(canonicalize2(value));
|
|
22623
|
+
}
|
|
22624
|
+
|
|
22625
|
+
// src/exit/bundle.ts
|
|
22626
|
+
init_hashing();
|
|
22627
|
+
init_encoding();
|
|
22628
|
+
init_encryption();
|
|
22629
|
+
init_identity();
|
|
22630
|
+
|
|
22631
|
+
// src/contracts/v1.1/exit-bundle-manifest.ts
|
|
22632
|
+
var EXIT_BUNDLE_PATH_PATTERN = /^[a-z0-9._-]+(?:\/[a-z0-9._-]+)*$/;
|
|
22633
|
+
var EXIT_BUNDLE_PATH_MAX_BYTES = 256;
|
|
22634
|
+
|
|
22635
|
+
// src/exit/verifier.ts
|
|
22636
|
+
init_encoding();
|
|
22637
|
+
init_hashing();
|
|
22638
|
+
var PRIVATE_MATERIAL_KEYS = /* @__PURE__ */ new Set([
|
|
22639
|
+
"private_key",
|
|
22640
|
+
"privatekey",
|
|
22641
|
+
"encrypted_private_key",
|
|
22642
|
+
"encryptedprivatekey",
|
|
22643
|
+
"passphrase",
|
|
22644
|
+
"recovery_key",
|
|
22645
|
+
"recoverykey",
|
|
22646
|
+
"seed",
|
|
22647
|
+
"mnemonic"
|
|
21427
22648
|
]);
|
|
21428
|
-
function
|
|
21429
|
-
|
|
21430
|
-
startOfDay.setHours(0, 0, 0, 0);
|
|
21431
|
-
const cutoff = startOfDay.getTime();
|
|
21432
|
-
return audit.filter((e) => {
|
|
21433
|
-
if (e.layer !== "l3") return false;
|
|
21434
|
-
if (!PROOF_CREATION_OPS.has(e.operation)) return false;
|
|
21435
|
-
const ts = new Date(e.timestamp).getTime();
|
|
21436
|
-
return !isNaN(ts) && ts >= cutoff;
|
|
21437
|
-
}).length;
|
|
22649
|
+
function sha256Hex2(bytes) {
|
|
22650
|
+
return Array.from(hash(bytes)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
21438
22651
|
}
|
|
21439
|
-
function
|
|
21440
|
-
|
|
21441
|
-
return {
|
|
21442
|
-
display_name: "Unclaimed agent",
|
|
21443
|
-
did: null,
|
|
21444
|
-
did_fingerprint: null,
|
|
21445
|
-
identity_count: 0,
|
|
21446
|
-
primary_identity_id: null
|
|
21447
|
-
};
|
|
21448
|
-
}
|
|
21449
|
-
const primary = sources.identityManager.getDefault();
|
|
21450
|
-
const identities = sources.identityManager.list();
|
|
21451
|
-
if (!primary) {
|
|
21452
|
-
return {
|
|
21453
|
-
display_name: "Unclaimed agent",
|
|
21454
|
-
did: null,
|
|
21455
|
-
did_fingerprint: null,
|
|
21456
|
-
identity_count: identities.length,
|
|
21457
|
-
primary_identity_id: null
|
|
21458
|
-
};
|
|
21459
|
-
}
|
|
22652
|
+
function resultBase(bundleDir, manifest, warnings = [], unsupported = []) {
|
|
22653
|
+
const body = manifest?.body;
|
|
21460
22654
|
return {
|
|
21461
|
-
|
|
21462
|
-
|
|
21463
|
-
|
|
21464
|
-
|
|
21465
|
-
|
|
22655
|
+
version: "1.1",
|
|
22656
|
+
passed: false,
|
|
22657
|
+
verified_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
22658
|
+
manifest_path: join(bundleDir, "manifest.json"),
|
|
22659
|
+
manifest_hash: null,
|
|
22660
|
+
manifest_summary: {
|
|
22661
|
+
manifest_version: body?.manifest_version ?? EXIT_BUNDLE_MANIFEST_VERSION,
|
|
22662
|
+
fortress_id: body?.identity_binding?.fortress_id ?? "",
|
|
22663
|
+
identity_id: body?.identity_binding?.identity_id ?? "",
|
|
22664
|
+
exported_at: body?.exported_at ?? "",
|
|
22665
|
+
artifact_count: body?.artifacts?.length ?? 0
|
|
22666
|
+
},
|
|
22667
|
+
artifact_results: body?.artifacts?.map((artifact) => ({
|
|
22668
|
+
path: artifact.path,
|
|
22669
|
+
kind: artifact.kind,
|
|
22670
|
+
hash_passed: false,
|
|
22671
|
+
size_passed: false
|
|
22672
|
+
})) ?? [],
|
|
22673
|
+
warnings,
|
|
22674
|
+
unsupported_artifacts: unsupported
|
|
21466
22675
|
};
|
|
21467
22676
|
}
|
|
21468
|
-
function
|
|
21469
|
-
const hasIdentity = !!sources.identityManager?.getDefault();
|
|
21470
|
-
const state = hasIdentity ? "full" : "degraded";
|
|
22677
|
+
function fail(bundleDir, manifest, failureClass, warnings = [], unsupported = []) {
|
|
21471
22678
|
return {
|
|
21472
|
-
|
|
21473
|
-
|
|
21474
|
-
headline: hasIdentity ? "State encrypted at rest" : "No sovereign identity \u2014 run sanctuary_bootstrap",
|
|
21475
|
-
encryption: "AES-256-GCM + HKDF per namespace",
|
|
21476
|
-
injection_blocked_today: countInjectionsToday(audit),
|
|
21477
|
-
memory_attest_ready: hasIdentity
|
|
22679
|
+
...resultBase(bundleDir, manifest, warnings, unsupported),
|
|
22680
|
+
failure_class: failureClass
|
|
21478
22681
|
};
|
|
21479
22682
|
}
|
|
21480
|
-
function
|
|
21481
|
-
|
|
21482
|
-
|
|
22683
|
+
function isKnownKind(kind) {
|
|
22684
|
+
return EXIT_BUNDLE_ARTIFACT_KINDS.includes(kind);
|
|
22685
|
+
}
|
|
22686
|
+
function validateArtifactPath(path) {
|
|
22687
|
+
if (Buffer.byteLength(path, "utf8") > EXIT_BUNDLE_PATH_MAX_BYTES) {
|
|
22688
|
+
return "unsafe";
|
|
22689
|
+
}
|
|
22690
|
+
if (!EXIT_BUNDLE_PATH_PATTERN.test(path)) return "unsafe";
|
|
22691
|
+
if (path.startsWith("/") || path.includes("\\") || path.includes("\0")) {
|
|
22692
|
+
return "unsafe";
|
|
22693
|
+
}
|
|
22694
|
+
const normalized = decodeURIComponentSafe(path).normalize("NFKC");
|
|
22695
|
+
if (normalized.split("/").some((segment) => segment === "." || segment === "..")) {
|
|
22696
|
+
return "unsafe";
|
|
22697
|
+
}
|
|
22698
|
+
return "ok";
|
|
22699
|
+
}
|
|
22700
|
+
function decodeURIComponentSafe(value) {
|
|
22701
|
+
let current = value;
|
|
22702
|
+
for (let i = 0; i < 2; i++) {
|
|
22703
|
+
try {
|
|
22704
|
+
const decoded = decodeURIComponent(current);
|
|
22705
|
+
if (decoded === current) return decoded;
|
|
22706
|
+
current = decoded;
|
|
22707
|
+
} catch {
|
|
22708
|
+
return current;
|
|
22709
|
+
}
|
|
22710
|
+
}
|
|
22711
|
+
return current;
|
|
22712
|
+
}
|
|
22713
|
+
async function assertDescendant(root, candidate) {
|
|
22714
|
+
const rootReal = await realpath(root);
|
|
22715
|
+
const candidateDir = await realpath(dirname(candidate));
|
|
22716
|
+
const rootWithSep = rootReal.endsWith(sep) ? rootReal : rootReal + sep;
|
|
22717
|
+
return candidateDir === rootReal || candidateDir.startsWith(rootWithSep);
|
|
22718
|
+
}
|
|
22719
|
+
function findPrivateMaterial(value, path = "$") {
|
|
22720
|
+
if (value === null || typeof value !== "object") return [];
|
|
22721
|
+
if (Array.isArray(value)) {
|
|
22722
|
+
return value.flatMap(
|
|
22723
|
+
(item, index) => findPrivateMaterial(item, `${path}[${index}]`)
|
|
22724
|
+
);
|
|
22725
|
+
}
|
|
22726
|
+
const findings = [];
|
|
22727
|
+
for (const [key, child] of Object.entries(value)) {
|
|
22728
|
+
const normalized = key.toLowerCase().replace(/[-\s]/g, "_");
|
|
22729
|
+
if (PRIVATE_MATERIAL_KEYS.has(normalized)) {
|
|
22730
|
+
findings.push(`${path}.${key}`);
|
|
22731
|
+
continue;
|
|
22732
|
+
}
|
|
22733
|
+
findings.push(...findPrivateMaterial(child, `${path}.${key}`));
|
|
22734
|
+
}
|
|
22735
|
+
return findings;
|
|
22736
|
+
}
|
|
22737
|
+
async function readManifest(bundleDir) {
|
|
22738
|
+
const bytes = await readFile(join(bundleDir, "manifest.json"));
|
|
22739
|
+
return JSON.parse(Buffer.from(bytes).toString("utf8"));
|
|
22740
|
+
}
|
|
22741
|
+
async function loadExitArtifact(bundleDir, manifest, kind) {
|
|
22742
|
+
const entry = manifest.body.artifacts.find((artifact) => artifact.kind === kind);
|
|
22743
|
+
if (!entry) return null;
|
|
22744
|
+
const artifactPath = join(bundleDir, entry.path);
|
|
22745
|
+
const bytes = await readFile(artifactPath);
|
|
22746
|
+
return {
|
|
22747
|
+
entry,
|
|
22748
|
+
path: artifactPath,
|
|
22749
|
+
bytes: new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength),
|
|
22750
|
+
json: JSON.parse(Buffer.from(bytes).toString("utf8"))
|
|
22751
|
+
};
|
|
22752
|
+
}
|
|
22753
|
+
function verifyIdentityArtifact(identityArtifact) {
|
|
22754
|
+
const wrapper = identityArtifact;
|
|
22755
|
+
const bundle = wrapper.bundle;
|
|
22756
|
+
if (!bundle || typeof wrapper.signature !== "string") {
|
|
22757
|
+
return { signature_valid: false };
|
|
22758
|
+
}
|
|
22759
|
+
const publicKey = bundle.publicKey;
|
|
22760
|
+
if (typeof publicKey !== "string") {
|
|
22761
|
+
return { signature_valid: false };
|
|
22762
|
+
}
|
|
22763
|
+
const signatureValid = ed25519.verify(
|
|
22764
|
+
fromBase64url(wrapper.signature),
|
|
22765
|
+
canonicalizeToBytes(bundle),
|
|
22766
|
+
fromBase64url(publicKey)
|
|
22767
|
+
);
|
|
22768
|
+
return {
|
|
22769
|
+
signature_valid: signatureValid,
|
|
22770
|
+
identity_id: typeof bundle.identity_id === "string" ? bundle.identity_id : void 0,
|
|
22771
|
+
did: typeof bundle.did === "string" ? bundle.did : void 0,
|
|
22772
|
+
public_key: publicKey
|
|
22773
|
+
};
|
|
22774
|
+
}
|
|
22775
|
+
function verifyReputationArtifact(reputationArtifact, publicKeysByDid) {
|
|
22776
|
+
const bundle = reputationArtifact;
|
|
22777
|
+
const attestations = Array.isArray(bundle.attestations) ? bundle.attestations : [];
|
|
22778
|
+
let bundleSignatureValid = "unverifiable";
|
|
22779
|
+
if (bundle.version === "SANCTUARY_REP_V1" && typeof bundle.exporter_did === "string" && typeof bundle.bundle_signature === "string") {
|
|
22780
|
+
const exporterKey = publicKeysByDid.get(bundle.exporter_did);
|
|
22781
|
+
if (exporterKey) {
|
|
22782
|
+
const signedBody = {
|
|
22783
|
+
version: "SANCTUARY_REP_V1",
|
|
22784
|
+
attestations,
|
|
22785
|
+
exported_at: bundle.exported_at,
|
|
22786
|
+
exporter_did: bundle.exporter_did
|
|
22787
|
+
};
|
|
22788
|
+
bundleSignatureValid = ed25519.verify(
|
|
22789
|
+
fromBase64url(bundle.bundle_signature),
|
|
22790
|
+
stringToBytes(JSON.stringify(signedBody)),
|
|
22791
|
+
exporterKey
|
|
22792
|
+
);
|
|
22793
|
+
}
|
|
22794
|
+
}
|
|
22795
|
+
let verified = 0;
|
|
22796
|
+
let invalid = 0;
|
|
22797
|
+
let unverifiable = 0;
|
|
22798
|
+
for (const attestation of attestations) {
|
|
22799
|
+
const signerKey = publicKeysByDid.get(attestation.signer);
|
|
22800
|
+
if (!signerKey) {
|
|
22801
|
+
unverifiable++;
|
|
22802
|
+
continue;
|
|
22803
|
+
}
|
|
22804
|
+
const ok = ed25519.verify(
|
|
22805
|
+
fromBase64url(attestation.signature),
|
|
22806
|
+
stringToBytes(JSON.stringify(attestation.data)),
|
|
22807
|
+
signerKey
|
|
22808
|
+
);
|
|
22809
|
+
if (ok) verified++;
|
|
22810
|
+
else invalid++;
|
|
22811
|
+
}
|
|
22812
|
+
return {
|
|
22813
|
+
bundle_signature_valid: bundleSignatureValid,
|
|
22814
|
+
attestation_count: attestations.length,
|
|
22815
|
+
verified_attestations: verified,
|
|
22816
|
+
invalid_attestations: invalid,
|
|
22817
|
+
unverifiable_attestations: unverifiable
|
|
22818
|
+
};
|
|
22819
|
+
}
|
|
22820
|
+
async function verifyExitBundle(bundleDir) {
|
|
22821
|
+
const root = resolve(bundleDir);
|
|
22822
|
+
let manifest;
|
|
22823
|
+
let manifestBytes;
|
|
22824
|
+
try {
|
|
22825
|
+
const raw = await readFile(join(root, "manifest.json"));
|
|
22826
|
+
manifestBytes = new Uint8Array(raw.buffer, raw.byteOffset, raw.byteLength);
|
|
22827
|
+
manifest = JSON.parse(Buffer.from(raw).toString("utf8"));
|
|
22828
|
+
} catch {
|
|
22829
|
+
return fail(root, null, "other", ["manifest.json is missing or unreadable"]);
|
|
22830
|
+
}
|
|
22831
|
+
const warnings = [];
|
|
22832
|
+
const unsupportedArtifacts = [];
|
|
22833
|
+
const body = manifest.body;
|
|
22834
|
+
if (!body || body.manifest_version !== EXIT_BUNDLE_MANIFEST_VERSION) {
|
|
22835
|
+
return fail(root, manifest, "manifest_unknown_version", warnings, unsupportedArtifacts);
|
|
22836
|
+
}
|
|
22837
|
+
if (body.signature_scheme !== SIGNATURE_SCHEME_V1) {
|
|
22838
|
+
return fail(
|
|
22839
|
+
root,
|
|
22840
|
+
manifest,
|
|
22841
|
+
"manifest_signature_scheme_invalid",
|
|
22842
|
+
warnings,
|
|
22843
|
+
unsupportedArtifacts
|
|
22844
|
+
);
|
|
22845
|
+
}
|
|
22846
|
+
const seenPaths = /* @__PURE__ */ new Set();
|
|
22847
|
+
for (const artifact of body.artifacts) {
|
|
22848
|
+
if (!isKnownKind(artifact.kind)) {
|
|
22849
|
+
return fail(root, manifest, "other", [`unknown artifact kind: ${artifact.kind}`]);
|
|
22850
|
+
}
|
|
22851
|
+
if (seenPaths.has(artifact.path)) {
|
|
22852
|
+
return fail(root, manifest, "artifact_path_duplicate", warnings, unsupportedArtifacts);
|
|
22853
|
+
}
|
|
22854
|
+
seenPaths.add(artifact.path);
|
|
22855
|
+
if (validateArtifactPath(artifact.path) !== "ok") {
|
|
22856
|
+
return fail(root, manifest, "artifact_path_unsafe", warnings, unsupportedArtifacts);
|
|
22857
|
+
}
|
|
22858
|
+
}
|
|
22859
|
+
const signatureOk = ed25519.verify(
|
|
22860
|
+
fromBase64url(manifest.signature),
|
|
22861
|
+
canonicalizeToBytes(body),
|
|
22862
|
+
fromBase64url(body.identity_binding.fortress_master_pubkey)
|
|
22863
|
+
);
|
|
22864
|
+
if (!signatureOk) {
|
|
22865
|
+
return fail(root, manifest, "manifest_signature_invalid", warnings, unsupportedArtifacts);
|
|
22866
|
+
}
|
|
22867
|
+
const expectedAggregate = sha256Hex2(
|
|
22868
|
+
stringToBytes(canonicalize2(body.artifacts))
|
|
22869
|
+
);
|
|
22870
|
+
if (expectedAggregate !== body.artifacts_aggregate_hash) {
|
|
22871
|
+
return fail(root, manifest, "aggregate_hash_mismatch", warnings, unsupportedArtifacts);
|
|
22872
|
+
}
|
|
22873
|
+
const artifactResults = [];
|
|
22874
|
+
let artifactFailure = null;
|
|
22875
|
+
for (const artifact of body.artifacts) {
|
|
22876
|
+
const artifactPath = join(root, artifact.path);
|
|
22877
|
+
let bytes;
|
|
22878
|
+
let fileSize = -1;
|
|
22879
|
+
try {
|
|
22880
|
+
const linkStat = await lstat(artifactPath);
|
|
22881
|
+
if (linkStat.isSymbolicLink()) {
|
|
22882
|
+
artifactFailure = "archive_contains_symlink";
|
|
22883
|
+
artifactResults.push({
|
|
22884
|
+
path: artifact.path,
|
|
22885
|
+
kind: artifact.kind,
|
|
22886
|
+
hash_passed: false,
|
|
22887
|
+
size_passed: false
|
|
22888
|
+
});
|
|
22889
|
+
continue;
|
|
22890
|
+
}
|
|
22891
|
+
const descends = await assertDescendant(root, artifactPath);
|
|
22892
|
+
if (!descends) {
|
|
22893
|
+
artifactFailure = "artifact_path_escapes_root";
|
|
22894
|
+
artifactResults.push({
|
|
22895
|
+
path: artifact.path,
|
|
22896
|
+
kind: artifact.kind,
|
|
22897
|
+
hash_passed: false,
|
|
22898
|
+
size_passed: false
|
|
22899
|
+
});
|
|
22900
|
+
continue;
|
|
22901
|
+
}
|
|
22902
|
+
const fileStat = await stat(artifactPath);
|
|
22903
|
+
fileSize = fileStat.size;
|
|
22904
|
+
const raw = await readFile(artifactPath);
|
|
22905
|
+
bytes = new Uint8Array(raw.buffer, raw.byteOffset, raw.byteLength);
|
|
22906
|
+
} catch {
|
|
22907
|
+
artifactFailure = "artifact_missing";
|
|
22908
|
+
artifactResults.push({
|
|
22909
|
+
path: artifact.path,
|
|
22910
|
+
kind: artifact.kind,
|
|
22911
|
+
hash_passed: false,
|
|
22912
|
+
size_passed: false
|
|
22913
|
+
});
|
|
22914
|
+
continue;
|
|
22915
|
+
}
|
|
22916
|
+
const hashPassed = sha256Hex2(bytes) === artifact.hash;
|
|
22917
|
+
const sizePassed = fileSize === artifact.size_bytes;
|
|
22918
|
+
if (!hashPassed && !artifactFailure) artifactFailure = "artifact_hash_mismatch";
|
|
22919
|
+
if (!sizePassed && !artifactFailure) artifactFailure = "artifact_size_mismatch";
|
|
22920
|
+
artifactResults.push({
|
|
22921
|
+
path: artifact.path,
|
|
22922
|
+
kind: artifact.kind,
|
|
22923
|
+
hash_passed: hashPassed,
|
|
22924
|
+
size_passed: sizePassed
|
|
22925
|
+
});
|
|
22926
|
+
try {
|
|
22927
|
+
const parsed = JSON.parse(Buffer.from(bytes).toString("utf8"));
|
|
22928
|
+
const privateFindings = findPrivateMaterial(parsed);
|
|
22929
|
+
if (privateFindings.length > 0) {
|
|
22930
|
+
artifactFailure = "private_material_present";
|
|
22931
|
+
warnings.push(
|
|
22932
|
+
`${artifact.path} contains private-material field(s): ${privateFindings.join(", ")}`
|
|
22933
|
+
);
|
|
22934
|
+
}
|
|
22935
|
+
} catch {
|
|
22936
|
+
warnings.push(`${artifact.path} is not parseable JSON`);
|
|
22937
|
+
}
|
|
22938
|
+
}
|
|
22939
|
+
if (artifactFailure) {
|
|
22940
|
+
return {
|
|
22941
|
+
...fail(root, manifest, artifactFailure, warnings, unsupportedArtifacts),
|
|
22942
|
+
manifest_hash: sha256Hex2(manifestBytes),
|
|
22943
|
+
artifact_results: artifactResults
|
|
22944
|
+
};
|
|
22945
|
+
}
|
|
22946
|
+
const publicKeysByDid = /* @__PURE__ */ new Map();
|
|
22947
|
+
const identityArtifact = await loadExitArtifact(root, manifest, "public_identity");
|
|
22948
|
+
let identity;
|
|
22949
|
+
if (identityArtifact) {
|
|
22950
|
+
const identityVerification = verifyIdentityArtifact(identityArtifact.json);
|
|
22951
|
+
identity = {
|
|
22952
|
+
signature_valid: identityVerification.signature_valid,
|
|
22953
|
+
identity_id: identityVerification.identity_id,
|
|
22954
|
+
did: identityVerification.did
|
|
22955
|
+
};
|
|
22956
|
+
if (identityVerification.did && identityVerification.public_key) {
|
|
22957
|
+
publicKeysByDid.set(
|
|
22958
|
+
identityVerification.did,
|
|
22959
|
+
fromBase64url(identityVerification.public_key)
|
|
22960
|
+
);
|
|
22961
|
+
}
|
|
22962
|
+
if (!identityVerification.signature_valid) {
|
|
22963
|
+
warnings.push("public identity artifact signature is invalid");
|
|
22964
|
+
}
|
|
22965
|
+
}
|
|
22966
|
+
const auditArtifact = await loadExitArtifact(root, manifest, "audit_receipts");
|
|
22967
|
+
const audit = auditArtifact ? {
|
|
22968
|
+
receipt_count: Array.isArray(auditArtifact.json.entries) ? auditArtifact.json.entries.length : 0,
|
|
22969
|
+
individual_signatures_verified: false
|
|
22970
|
+
} : void 0;
|
|
22971
|
+
if (auditArtifact) {
|
|
22972
|
+
unsupportedArtifacts.push(
|
|
22973
|
+
"audit_receipts: individual audit entries are not signed in the legacy L2 audit log; verifier pins them by signed manifest hash"
|
|
22974
|
+
);
|
|
22975
|
+
}
|
|
22976
|
+
const reputationArtifact = await loadExitArtifact(root, manifest, "reputation_bundle");
|
|
22977
|
+
const reputation = reputationArtifact ? verifyReputationArtifact(reputationArtifact.json, publicKeysByDid) : void 0;
|
|
22978
|
+
if (reputation) {
|
|
22979
|
+
if (reputation.bundle_signature_valid === "unverifiable") {
|
|
22980
|
+
warnings.push("reputation bundle signature is unverifiable from included public identities");
|
|
22981
|
+
} else if (!reputation.bundle_signature_valid) {
|
|
22982
|
+
warnings.push("reputation bundle signature is invalid");
|
|
22983
|
+
}
|
|
22984
|
+
if (reputation.unverifiable_attestations > 0) {
|
|
22985
|
+
warnings.push(
|
|
22986
|
+
`${reputation.unverifiable_attestations} reputation attestation(s) have unknown signer public keys`
|
|
22987
|
+
);
|
|
22988
|
+
}
|
|
22989
|
+
if (reputation.invalid_attestations > 0) {
|
|
22990
|
+
warnings.push(
|
|
22991
|
+
`${reputation.invalid_attestations} reputation attestation(s) failed signature verification`
|
|
22992
|
+
);
|
|
22993
|
+
}
|
|
22994
|
+
}
|
|
22995
|
+
const reputationFailed = reputation?.bundle_signature_valid === false || (reputation?.invalid_attestations ?? 0) > 0;
|
|
22996
|
+
const identityFailed = identity ? !identity.signature_valid : false;
|
|
22997
|
+
return {
|
|
22998
|
+
version: "1.1",
|
|
22999
|
+
passed: !reputationFailed && !identityFailed,
|
|
23000
|
+
verified_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
23001
|
+
manifest_path: join(root, "manifest.json"),
|
|
23002
|
+
manifest_hash: sha256Hex2(manifestBytes),
|
|
23003
|
+
manifest_summary: {
|
|
23004
|
+
manifest_version: body.manifest_version,
|
|
23005
|
+
fortress_id: body.identity_binding.fortress_id,
|
|
23006
|
+
identity_id: body.identity_binding.identity_id,
|
|
23007
|
+
exported_at: body.exported_at,
|
|
23008
|
+
artifact_count: body.artifacts.length
|
|
23009
|
+
},
|
|
23010
|
+
artifact_results: artifactResults,
|
|
23011
|
+
warnings,
|
|
23012
|
+
unsupported_artifacts: unsupportedArtifacts,
|
|
23013
|
+
identity,
|
|
23014
|
+
audit,
|
|
23015
|
+
reputation,
|
|
23016
|
+
failure_class: reputationFailed || identityFailed ? "other" : void 0
|
|
23017
|
+
};
|
|
23018
|
+
}
|
|
23019
|
+
|
|
23020
|
+
// src/exit/bundle.ts
|
|
23021
|
+
var ARTIFACT_DIR = "artifacts";
|
|
23022
|
+
var EXIT_IMPORT_NAMESPACE = "_exit_imports";
|
|
23023
|
+
var EXIT_PUBLIC_IDENTITIES_NAMESPACE = "_exit_public_identities";
|
|
23024
|
+
var EXIT_AUDIT_RECEIPTS_NAMESPACE = "_exit_audit_receipts";
|
|
23025
|
+
var EXIT_POLICY_SETS_NAMESPACE = "_exit_policy_sets";
|
|
23026
|
+
var EXIT_COMMITMENTS_NAMESPACE = "_exit_commitments";
|
|
23027
|
+
var EXIT_PLACEHOLDER_METADATA_NAMESPACE = "_exit_placeholder_metadata";
|
|
23028
|
+
var PRIVACY_PLACEHOLDER_NAMESPACE = "_privacy_placeholder_vault";
|
|
23029
|
+
function sha256Hex3(bytes) {
|
|
23030
|
+
return Array.from(hash(bytes)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
23031
|
+
}
|
|
23032
|
+
function jsonBytes(value) {
|
|
23033
|
+
return stringToBytes(JSON.stringify(value, null, 2) + "\n");
|
|
23034
|
+
}
|
|
23035
|
+
async function writeJsonArtifact(bundleDir, path, value, kind) {
|
|
23036
|
+
const bytes = jsonBytes(value);
|
|
23037
|
+
const fullPath = join(bundleDir, path);
|
|
23038
|
+
await mkdir(join(bundleDir, ARTIFACT_DIR), { recursive: true, mode: 448 });
|
|
23039
|
+
await writeFile(fullPath, bytes, { mode: 384 });
|
|
23040
|
+
return {
|
|
23041
|
+
kind,
|
|
23042
|
+
path,
|
|
23043
|
+
hash_alg: "sha256",
|
|
23044
|
+
hash: sha256Hex3(bytes),
|
|
23045
|
+
size_bytes: bytes.length
|
|
23046
|
+
};
|
|
23047
|
+
}
|
|
23048
|
+
async function readSourceKeyParams(storage) {
|
|
23049
|
+
const raw = await storage.read("_meta", "key-params");
|
|
23050
|
+
if (!raw) return void 0;
|
|
23051
|
+
return JSON.parse(bytesToString(raw));
|
|
23052
|
+
}
|
|
23053
|
+
async function discoverFilesystemStateNamespaces(stateStoragePath) {
|
|
23054
|
+
if (!stateStoragePath) return [];
|
|
23055
|
+
try {
|
|
23056
|
+
const names = await readdir(stateStoragePath);
|
|
23057
|
+
const namespaces = [];
|
|
23058
|
+
for (const name of names) {
|
|
23059
|
+
const full = join(stateStoragePath, name);
|
|
23060
|
+
const entryStat = await stat(full);
|
|
23061
|
+
if (!entryStat.isDirectory()) continue;
|
|
23062
|
+
if (name.startsWith("_")) continue;
|
|
23063
|
+
namespaces.push(name);
|
|
23064
|
+
}
|
|
23065
|
+
return namespaces.sort();
|
|
23066
|
+
} catch {
|
|
23067
|
+
return [];
|
|
23068
|
+
}
|
|
23069
|
+
}
|
|
23070
|
+
async function exportEncryptedState(opts) {
|
|
23071
|
+
const namespaceSet = new Set(
|
|
23072
|
+
opts.stateNamespaces ?? await discoverFilesystemStateNamespaces(opts.stateStoragePath)
|
|
23073
|
+
);
|
|
23074
|
+
const entries = [];
|
|
23075
|
+
for (const namespace of [...namespaceSet].sort()) {
|
|
23076
|
+
if (isReservedNamespace(namespace)) continue;
|
|
23077
|
+
const metas = await opts.storage.list(namespace);
|
|
23078
|
+
for (const meta of metas) {
|
|
23079
|
+
const raw = await opts.storage.read(namespace, meta.key);
|
|
23080
|
+
if (!raw) continue;
|
|
23081
|
+
try {
|
|
23082
|
+
entries.push({
|
|
23083
|
+
namespace,
|
|
23084
|
+
key: meta.key,
|
|
23085
|
+
entry: JSON.parse(bytesToString(raw))
|
|
23086
|
+
});
|
|
23087
|
+
} catch {
|
|
23088
|
+
}
|
|
23089
|
+
}
|
|
23090
|
+
}
|
|
23091
|
+
return {
|
|
23092
|
+
format: "SANCTUARY_EXIT_ENCRYPTED_STATE_V1",
|
|
23093
|
+
exported_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
23094
|
+
key_source: opts.keySource ?? "unknown",
|
|
23095
|
+
source_key_derivation: await readSourceKeyParams(opts.storage),
|
|
23096
|
+
namespaces: [...new Set(entries.map((entry) => entry.namespace))].sort(),
|
|
23097
|
+
total_keys: entries.length,
|
|
23098
|
+
contains_reserved_namespaces: false,
|
|
23099
|
+
entries
|
|
23100
|
+
};
|
|
23101
|
+
}
|
|
23102
|
+
function exportPublicIdentity(identity, masterKey) {
|
|
23103
|
+
const body = {
|
|
23104
|
+
format: "SANCTUARY_IDENTITY_BUNDLE_V1",
|
|
23105
|
+
publicKey: identity.public_key,
|
|
23106
|
+
did: identity.did,
|
|
23107
|
+
identity_id: identity.identity_id,
|
|
23108
|
+
label: identity.label,
|
|
23109
|
+
key_type: identity.key_type,
|
|
23110
|
+
key_protection: identity.key_protection,
|
|
23111
|
+
rotation_history: identity.rotation_history ?? [],
|
|
23112
|
+
exported_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
23113
|
+
};
|
|
23114
|
+
const signature = sign(
|
|
23115
|
+
canonicalizeToBytes(body),
|
|
23116
|
+
identity.encrypted_private_key,
|
|
23117
|
+
derivePurposeKey(masterKey, "identity-encryption")
|
|
23118
|
+
);
|
|
23119
|
+
return {
|
|
23120
|
+
bundle: body,
|
|
23121
|
+
signature: toBase64url(signature),
|
|
23122
|
+
signed_by: identity.did
|
|
23123
|
+
};
|
|
23124
|
+
}
|
|
23125
|
+
function redactedPolicy(policy) {
|
|
23126
|
+
return {
|
|
23127
|
+
...policy,
|
|
23128
|
+
approval_channel: {
|
|
23129
|
+
type: policy.approval_channel.type,
|
|
23130
|
+
timeout_seconds: policy.approval_channel.timeout_seconds,
|
|
23131
|
+
webhook_url: policy.approval_channel.webhook_url
|
|
23132
|
+
}
|
|
23133
|
+
};
|
|
23134
|
+
}
|
|
23135
|
+
function exportPolicySet(policy, config) {
|
|
23136
|
+
const cfg = config ?? defaultConfig();
|
|
23137
|
+
return {
|
|
23138
|
+
format: "SANCTUARY_EXIT_POLICY_SET_V1",
|
|
23139
|
+
exported_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
23140
|
+
principal_policy: redactedPolicy(policy),
|
|
23141
|
+
config_summary: {
|
|
23142
|
+
version: cfg.version,
|
|
23143
|
+
state: cfg.state,
|
|
23144
|
+
execution: cfg.execution,
|
|
23145
|
+
disclosure: cfg.disclosure,
|
|
23146
|
+
reputation: cfg.reputation,
|
|
23147
|
+
privacy_filter: cfg.privacy_filter
|
|
23148
|
+
}
|
|
23149
|
+
};
|
|
23150
|
+
}
|
|
23151
|
+
async function exportAuditReceipts(auditLog) {
|
|
23152
|
+
await auditLog.flush();
|
|
23153
|
+
const result = await auditLog.query({ limit: 1e5 });
|
|
23154
|
+
return {
|
|
23155
|
+
format: "SANCTUARY_AUDIT_RECEIPTS_V1",
|
|
23156
|
+
exported_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
23157
|
+
total: result.total,
|
|
23158
|
+
individual_entry_signatures: false,
|
|
23159
|
+
entries: result.entries
|
|
23160
|
+
};
|
|
23161
|
+
}
|
|
23162
|
+
async function exportCommitments(storage, masterKey) {
|
|
23163
|
+
const encryptionKey = derivePurposeKey(masterKey, "l3-commitments");
|
|
23164
|
+
const publicCommitments = [];
|
|
23165
|
+
let unreadable = 0;
|
|
23166
|
+
for (const meta of await storage.list("_commitments")) {
|
|
23167
|
+
const raw = await storage.read("_commitments", meta.key);
|
|
23168
|
+
if (!raw) continue;
|
|
23169
|
+
try {
|
|
23170
|
+
const encrypted = JSON.parse(bytesToString(raw));
|
|
23171
|
+
const decrypted = decrypt(encrypted, encryptionKey);
|
|
23172
|
+
const parsed = JSON.parse(bytesToString(decrypted));
|
|
23173
|
+
publicCommitments.push({
|
|
23174
|
+
commitment_id: meta.key,
|
|
23175
|
+
commitment: parsed.commitment,
|
|
23176
|
+
committed_at: parsed.committed_at,
|
|
23177
|
+
revealed: parsed.revealed,
|
|
23178
|
+
revealed_at: parsed.revealed_at
|
|
23179
|
+
});
|
|
23180
|
+
} catch {
|
|
23181
|
+
unreadable++;
|
|
23182
|
+
}
|
|
23183
|
+
}
|
|
23184
|
+
return {
|
|
23185
|
+
format: "SANCTUARY_EXIT_COMMITMENTS_V1",
|
|
23186
|
+
exported_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
23187
|
+
public_commitments: publicCommitments,
|
|
23188
|
+
unreadable_count: unreadable,
|
|
23189
|
+
redacted_fields: ["value", "blinding_factor"]
|
|
23190
|
+
};
|
|
23191
|
+
}
|
|
23192
|
+
async function exportPlaceholderVaultMetadata(storage, masterKey) {
|
|
23193
|
+
const encryptionKey = derivePurposeKey(masterKey, "l2-privacy-placeholders");
|
|
23194
|
+
const entries = [];
|
|
23195
|
+
let unreadable = 0;
|
|
23196
|
+
for (const meta of await storage.list(PRIVACY_PLACEHOLDER_NAMESPACE)) {
|
|
23197
|
+
const raw = await storage.read(PRIVACY_PLACEHOLDER_NAMESPACE, meta.key);
|
|
23198
|
+
if (!raw) continue;
|
|
23199
|
+
try {
|
|
23200
|
+
const encrypted = JSON.parse(bytesToString(raw));
|
|
23201
|
+
const decrypted = decrypt(encrypted, encryptionKey);
|
|
23202
|
+
const parsed = JSON.parse(bytesToString(decrypted));
|
|
23203
|
+
const safe = {
|
|
23204
|
+
key: meta.key,
|
|
23205
|
+
version: parsed.version,
|
|
23206
|
+
kind: parsed.kind ?? (meta.key.endsWith("__index") ? "index" : "metadata"),
|
|
23207
|
+
scope: parsed.scope,
|
|
23208
|
+
class: parsed.class,
|
|
23209
|
+
placeholder: parsed.placeholder,
|
|
23210
|
+
alias: parsed.alias,
|
|
23211
|
+
raw_hash: parsed.raw_hash,
|
|
23212
|
+
counters: parsed.counters,
|
|
23213
|
+
next: parsed.next,
|
|
23214
|
+
created_at: parsed.created_at
|
|
23215
|
+
};
|
|
23216
|
+
entries.push(
|
|
23217
|
+
Object.fromEntries(
|
|
23218
|
+
Object.entries(safe).filter(([, value]) => value !== void 0)
|
|
23219
|
+
)
|
|
23220
|
+
);
|
|
23221
|
+
} catch {
|
|
23222
|
+
unreadable++;
|
|
23223
|
+
}
|
|
23224
|
+
}
|
|
23225
|
+
return {
|
|
23226
|
+
format: "SANCTUARY_PLACEHOLDER_VAULT_METADATA_V1",
|
|
23227
|
+
exported_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
23228
|
+
entries,
|
|
23229
|
+
unreadable_count: unreadable,
|
|
23230
|
+
redacted_fields: ["raw_value", "raw_path"]
|
|
23231
|
+
};
|
|
23232
|
+
}
|
|
23233
|
+
async function exportExitBundle(opts) {
|
|
23234
|
+
const bundleDir = resolve(opts.bundleDir);
|
|
23235
|
+
await mkdir(bundleDir, { recursive: true, mode: 448 });
|
|
23236
|
+
await mkdir(join(bundleDir, ARTIFACT_DIR), { recursive: true, mode: 448 });
|
|
23237
|
+
const identity = opts.identityManager.getDefault();
|
|
23238
|
+
if (!identity) {
|
|
23239
|
+
throw new Error("Cannot export exit bundle: no default identity exists.");
|
|
23240
|
+
}
|
|
23241
|
+
const exportApprovalAuditId = opts.exportApprovalAuditId ?? `exit-export-${Date.now()}`;
|
|
23242
|
+
opts.auditLog.append("l1", "exit_bundle_export", identity.identity_id, {
|
|
23243
|
+
approval_id: exportApprovalAuditId,
|
|
23244
|
+
manifest_version: EXIT_BUNDLE_MANIFEST_VERSION
|
|
23245
|
+
});
|
|
23246
|
+
const reputationStore = opts.reputationStore ?? new ReputationStore(opts.storage, opts.masterKey);
|
|
23247
|
+
const identityEncryptionKey = derivePurposeKey(opts.masterKey, "identity-encryption");
|
|
23248
|
+
const artifacts = [];
|
|
23249
|
+
artifacts.push(
|
|
23250
|
+
await writeJsonArtifact(
|
|
23251
|
+
bundleDir,
|
|
23252
|
+
`${ARTIFACT_DIR}/public_identity.json`,
|
|
23253
|
+
exportPublicIdentity(identity, opts.masterKey),
|
|
23254
|
+
"public_identity"
|
|
23255
|
+
)
|
|
23256
|
+
);
|
|
23257
|
+
artifacts.push(
|
|
23258
|
+
await writeJsonArtifact(
|
|
23259
|
+
bundleDir,
|
|
23260
|
+
`${ARTIFACT_DIR}/encrypted_state.json`,
|
|
23261
|
+
await exportEncryptedState(opts),
|
|
23262
|
+
"encrypted_state"
|
|
23263
|
+
)
|
|
23264
|
+
);
|
|
23265
|
+
artifacts.push(
|
|
23266
|
+
await writeJsonArtifact(
|
|
23267
|
+
bundleDir,
|
|
23268
|
+
`${ARTIFACT_DIR}/policy_set.json`,
|
|
23269
|
+
exportPolicySet(opts.policy, opts.config),
|
|
23270
|
+
"policy_set"
|
|
23271
|
+
)
|
|
23272
|
+
);
|
|
23273
|
+
artifacts.push(
|
|
23274
|
+
await writeJsonArtifact(
|
|
23275
|
+
bundleDir,
|
|
23276
|
+
`${ARTIFACT_DIR}/audit_receipts.json`,
|
|
23277
|
+
await exportAuditReceipts(opts.auditLog),
|
|
23278
|
+
"audit_receipts"
|
|
23279
|
+
)
|
|
23280
|
+
);
|
|
23281
|
+
artifacts.push(
|
|
23282
|
+
await writeJsonArtifact(
|
|
23283
|
+
bundleDir,
|
|
23284
|
+
`${ARTIFACT_DIR}/reputation_bundle.json`,
|
|
23285
|
+
await reputationStore.exportBundle(identity, identityEncryptionKey),
|
|
23286
|
+
"reputation_bundle"
|
|
23287
|
+
)
|
|
23288
|
+
);
|
|
23289
|
+
artifacts.push(
|
|
23290
|
+
await writeJsonArtifact(
|
|
23291
|
+
bundleDir,
|
|
23292
|
+
`${ARTIFACT_DIR}/commitments.json`,
|
|
23293
|
+
await exportCommitments(opts.storage, opts.masterKey),
|
|
23294
|
+
"commitments"
|
|
23295
|
+
)
|
|
23296
|
+
);
|
|
23297
|
+
artifacts.push(
|
|
23298
|
+
await writeJsonArtifact(
|
|
23299
|
+
bundleDir,
|
|
23300
|
+
`${ARTIFACT_DIR}/placeholder_vault_metadata.json`,
|
|
23301
|
+
await exportPlaceholderVaultMetadata(opts.storage, opts.masterKey),
|
|
23302
|
+
"placeholder_vault_metadata"
|
|
23303
|
+
)
|
|
23304
|
+
);
|
|
23305
|
+
const body = {
|
|
23306
|
+
manifest_version: EXIT_BUNDLE_MANIFEST_VERSION,
|
|
23307
|
+
exported_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
23308
|
+
identity_binding: {
|
|
23309
|
+
identity_id: identity.identity_id,
|
|
23310
|
+
fortress_id: identity.did,
|
|
23311
|
+
fortress_master_pubkey: identity.public_key,
|
|
23312
|
+
did: identity.did
|
|
23313
|
+
},
|
|
23314
|
+
source_sanctuary_version: opts.config?.version ?? SANCTUARY_VERSION,
|
|
23315
|
+
artifacts,
|
|
23316
|
+
artifacts_aggregate_hash: sha256Hex3(
|
|
23317
|
+
stringToBytes(canonicalize2(artifacts))
|
|
23318
|
+
),
|
|
23319
|
+
artifacts_aggregate_hash_alg: "sha256",
|
|
23320
|
+
export_approval_audit_id: exportApprovalAuditId,
|
|
23321
|
+
signature_scheme: SIGNATURE_SCHEME_V1
|
|
23322
|
+
};
|
|
23323
|
+
const signature = sign(
|
|
23324
|
+
canonicalizeToBytes(body),
|
|
23325
|
+
identity.encrypted_private_key,
|
|
23326
|
+
identityEncryptionKey
|
|
23327
|
+
);
|
|
23328
|
+
const manifest = {
|
|
23329
|
+
body,
|
|
23330
|
+
signature: toBase64url(signature)
|
|
23331
|
+
};
|
|
23332
|
+
const manifestBytes = jsonBytes(manifest);
|
|
23333
|
+
await writeFile(join(bundleDir, "manifest.json"), manifestBytes, { mode: 384 });
|
|
23334
|
+
await opts.auditLog.flush();
|
|
23335
|
+
return {
|
|
23336
|
+
bundle_dir: bundleDir,
|
|
23337
|
+
manifest,
|
|
23338
|
+
manifest_hash: sha256Hex3(manifestBytes),
|
|
23339
|
+
artifact_count: artifacts.length,
|
|
23340
|
+
unsupported_artifacts: [
|
|
23341
|
+
"audit_receipts: legacy L2 audit entries are manifest-pinned but not individually signed"
|
|
23342
|
+
]
|
|
23343
|
+
};
|
|
23344
|
+
}
|
|
23345
|
+
function publicKeysFromIdentityArtifact(identityArtifact) {
|
|
23346
|
+
const pubkey = fromBase64url(identityArtifact.bundle.publicKey);
|
|
23347
|
+
return {
|
|
23348
|
+
byIdentityId: /* @__PURE__ */ new Map([[identityArtifact.bundle.identity_id, pubkey]]),
|
|
23349
|
+
byDid: /* @__PURE__ */ new Map([[identityArtifact.bundle.did, pubkey]])
|
|
23350
|
+
};
|
|
23351
|
+
}
|
|
23352
|
+
async function conflictReport(storage, identityArtifact, encryptedState, reputationBundle, manifest) {
|
|
23353
|
+
const stateConflicts = [];
|
|
23354
|
+
for (const item of encryptedState?.entries ?? []) {
|
|
23355
|
+
if (await storage.exists(item.namespace, item.key)) {
|
|
23356
|
+
stateConflicts.push({ namespace: item.namespace, key: item.key });
|
|
23357
|
+
}
|
|
23358
|
+
}
|
|
23359
|
+
const reputationConflicts = [];
|
|
23360
|
+
for (const attestation of reputationBundle?.attestations ?? []) {
|
|
23361
|
+
if (await storage.exists("_reputation", attestation.attestation_id)) {
|
|
23362
|
+
reputationConflicts.push(attestation.attestation_id);
|
|
23363
|
+
}
|
|
23364
|
+
}
|
|
23365
|
+
const importId = importIdForManifest(manifest);
|
|
23366
|
+
return {
|
|
23367
|
+
public_identity_exists: identityArtifact ? await storage.exists(
|
|
23368
|
+
EXIT_PUBLIC_IDENTITIES_NAMESPACE,
|
|
23369
|
+
identityArtifact.bundle.identity_id
|
|
23370
|
+
) : false,
|
|
23371
|
+
state_conflicts: stateConflicts,
|
|
23372
|
+
reputation_conflicts: reputationConflicts,
|
|
23373
|
+
policy_set_exists: await storage.exists(EXIT_POLICY_SETS_NAMESPACE, importId),
|
|
23374
|
+
audit_receipts_exist: await storage.exists(EXIT_AUDIT_RECEIPTS_NAMESPACE, importId)
|
|
23375
|
+
};
|
|
23376
|
+
}
|
|
23377
|
+
function importIdForManifest(manifest) {
|
|
23378
|
+
return `${manifest.body.identity_binding.identity_id}-${manifest.body.exported_at.replace(/[^0-9a-zA-Z_.-]/g, "_")}`;
|
|
23379
|
+
}
|
|
23380
|
+
async function resolveSourceMasterKey(encryptedState, opts) {
|
|
23381
|
+
if (!encryptedState || encryptedState.entries.length === 0) return null;
|
|
23382
|
+
if (opts.sourceMasterKey) return opts.sourceMasterKey;
|
|
23383
|
+
if (opts.sourcePassphrase && encryptedState.source_key_derivation) {
|
|
23384
|
+
return (await deriveMasterKey(
|
|
23385
|
+
opts.sourcePassphrase,
|
|
23386
|
+
encryptedState.source_key_derivation
|
|
23387
|
+
)).key;
|
|
23388
|
+
}
|
|
23389
|
+
if (opts.sourceRecoveryKey) {
|
|
23390
|
+
const key = fromBase64url(opts.sourceRecoveryKey);
|
|
23391
|
+
if (key.length !== 32) {
|
|
23392
|
+
throw new Error("Source recovery key must decode to 32 bytes.");
|
|
23393
|
+
}
|
|
23394
|
+
return key;
|
|
23395
|
+
}
|
|
23396
|
+
return null;
|
|
23397
|
+
}
|
|
23398
|
+
async function rekeyState(encryptedState, opts, sourceMasterKey, publicKeysByIdentityId) {
|
|
23399
|
+
const destinationSigner = opts.destinationSignerIdentityId ? opts.identityManager.get(opts.destinationSignerIdentityId) : opts.identityManager.getDefault();
|
|
23400
|
+
if (!destinationSigner) {
|
|
23401
|
+
return {
|
|
23402
|
+
status: "skipped_no_destination_signer",
|
|
23403
|
+
imported_keys: 0,
|
|
23404
|
+
skipped_keys: encryptedState.entries.length,
|
|
23405
|
+
skipped_invalid_sig: 0,
|
|
23406
|
+
skipped_unknown_kid: 0,
|
|
23407
|
+
conflicts: 0
|
|
23408
|
+
};
|
|
23409
|
+
}
|
|
23410
|
+
const stateStore = new StateStore(opts.storage, opts.masterKey);
|
|
23411
|
+
const identityEncryptionKey = derivePurposeKey(opts.masterKey, "identity-encryption");
|
|
23412
|
+
let imported = 0;
|
|
23413
|
+
let skipped = 0;
|
|
23414
|
+
let skippedInvalidSig = 0;
|
|
23415
|
+
let skippedUnknownKid = 0;
|
|
23416
|
+
let conflicts = 0;
|
|
23417
|
+
for (const item of encryptedState.entries) {
|
|
23418
|
+
if (isReservedNamespace(item.namespace)) {
|
|
23419
|
+
skipped++;
|
|
23420
|
+
continue;
|
|
23421
|
+
}
|
|
23422
|
+
const signerPubkey = publicKeysByIdentityId.get(item.entry.kid);
|
|
23423
|
+
if (!signerPubkey) {
|
|
23424
|
+
skippedUnknownKid++;
|
|
23425
|
+
skipped++;
|
|
23426
|
+
continue;
|
|
23427
|
+
}
|
|
23428
|
+
const sourceSigValid = verify(
|
|
23429
|
+
fromBase64url(item.entry.payload.ct),
|
|
23430
|
+
fromBase64url(item.entry.sig),
|
|
23431
|
+
signerPubkey
|
|
23432
|
+
);
|
|
23433
|
+
if (!sourceSigValid) {
|
|
23434
|
+
skippedInvalidSig++;
|
|
23435
|
+
skipped++;
|
|
23436
|
+
continue;
|
|
23437
|
+
}
|
|
23438
|
+
const exists = await opts.storage.exists(item.namespace, item.key);
|
|
23439
|
+
if (exists) {
|
|
23440
|
+
conflicts++;
|
|
23441
|
+
const resolution = opts.conflictResolution ?? "skip";
|
|
23442
|
+
if (resolution === "skip") {
|
|
23443
|
+
skipped++;
|
|
23444
|
+
continue;
|
|
23445
|
+
}
|
|
23446
|
+
if (resolution === "version") {
|
|
23447
|
+
const raw = await opts.storage.read(item.namespace, item.key);
|
|
23448
|
+
if (raw) {
|
|
23449
|
+
try {
|
|
23450
|
+
const existing = JSON.parse(bytesToString(raw));
|
|
23451
|
+
if (item.entry.ver <= existing.ver) {
|
|
23452
|
+
skipped++;
|
|
23453
|
+
continue;
|
|
23454
|
+
}
|
|
23455
|
+
} catch {
|
|
23456
|
+
}
|
|
23457
|
+
}
|
|
23458
|
+
}
|
|
23459
|
+
}
|
|
23460
|
+
try {
|
|
23461
|
+
const plaintext = decrypt(
|
|
23462
|
+
item.entry.payload,
|
|
23463
|
+
deriveNamespaceKey(sourceMasterKey, item.namespace)
|
|
23464
|
+
);
|
|
23465
|
+
if (hashToString(plaintext) !== item.entry.integrity_hash) {
|
|
23466
|
+
skippedInvalidSig++;
|
|
23467
|
+
skipped++;
|
|
23468
|
+
continue;
|
|
23469
|
+
}
|
|
23470
|
+
await stateStore.write(
|
|
23471
|
+
item.namespace,
|
|
23472
|
+
item.key,
|
|
23473
|
+
bytesToString(plaintext),
|
|
23474
|
+
destinationSigner.identity_id,
|
|
23475
|
+
destinationSigner.encrypted_private_key,
|
|
23476
|
+
identityEncryptionKey,
|
|
23477
|
+
{
|
|
23478
|
+
content_type: item.entry.metadata.content_type,
|
|
23479
|
+
ttl_seconds: item.entry.metadata.ttl_seconds,
|
|
23480
|
+
tags: [
|
|
23481
|
+
...item.entry.metadata.tags ?? [],
|
|
23482
|
+
"exit-import",
|
|
23483
|
+
`source:${item.entry.kid}`
|
|
23484
|
+
]
|
|
23485
|
+
}
|
|
23486
|
+
);
|
|
23487
|
+
imported++;
|
|
23488
|
+
} catch {
|
|
23489
|
+
skippedInvalidSig++;
|
|
23490
|
+
skipped++;
|
|
23491
|
+
}
|
|
23492
|
+
}
|
|
23493
|
+
return {
|
|
23494
|
+
status: "rekeyed",
|
|
23495
|
+
imported_keys: imported,
|
|
23496
|
+
skipped_keys: skipped,
|
|
23497
|
+
skipped_invalid_sig: skippedInvalidSig,
|
|
23498
|
+
skipped_unknown_kid: skippedUnknownKid,
|
|
23499
|
+
conflicts
|
|
23500
|
+
};
|
|
23501
|
+
}
|
|
23502
|
+
async function stageArtifact(storage, namespace, key, value) {
|
|
23503
|
+
await storage.write(namespace, key, jsonBytes(value));
|
|
23504
|
+
}
|
|
23505
|
+
async function importExitBundle(opts) {
|
|
23506
|
+
const verification = await verifyExitBundle(opts.bundleDir);
|
|
23507
|
+
if (!verification.passed) {
|
|
23508
|
+
return {
|
|
23509
|
+
verified: false,
|
|
23510
|
+
activated: false,
|
|
23511
|
+
conflicts: {
|
|
23512
|
+
public_identity_exists: false,
|
|
23513
|
+
state_conflicts: [],
|
|
23514
|
+
reputation_conflicts: [],
|
|
23515
|
+
policy_set_exists: false,
|
|
23516
|
+
audit_receipts_exist: false
|
|
23517
|
+
},
|
|
23518
|
+
state: {
|
|
23519
|
+
status: "not_requested",
|
|
23520
|
+
imported_keys: 0,
|
|
23521
|
+
skipped_keys: 0,
|
|
23522
|
+
skipped_invalid_sig: 0,
|
|
23523
|
+
skipped_unknown_kid: 0,
|
|
23524
|
+
conflicts: 0
|
|
23525
|
+
},
|
|
23526
|
+
reputation: {
|
|
23527
|
+
imported_attestations: 0,
|
|
23528
|
+
invalid_attestations: 0,
|
|
23529
|
+
unverifiable_attestations: verification.reputation?.unverifiable_attestations ?? 0
|
|
23530
|
+
},
|
|
23531
|
+
staged_artifacts: [],
|
|
23532
|
+
warnings: verification.warnings,
|
|
23533
|
+
unsupported_artifacts: verification.unsupported_artifacts
|
|
23534
|
+
};
|
|
23535
|
+
}
|
|
23536
|
+
const manifest = await readManifest(opts.bundleDir);
|
|
23537
|
+
const identityArtifact = await loadExitArtifact(
|
|
23538
|
+
opts.bundleDir,
|
|
23539
|
+
manifest,
|
|
23540
|
+
"public_identity"
|
|
23541
|
+
);
|
|
23542
|
+
const encryptedState = await loadExitArtifact(
|
|
23543
|
+
opts.bundleDir,
|
|
23544
|
+
manifest,
|
|
23545
|
+
"encrypted_state"
|
|
23546
|
+
);
|
|
23547
|
+
const policySet = await loadExitArtifact(
|
|
23548
|
+
opts.bundleDir,
|
|
23549
|
+
manifest,
|
|
23550
|
+
"policy_set"
|
|
23551
|
+
);
|
|
23552
|
+
const auditReceipts = await loadExitArtifact(
|
|
23553
|
+
opts.bundleDir,
|
|
23554
|
+
manifest,
|
|
23555
|
+
"audit_receipts"
|
|
23556
|
+
);
|
|
23557
|
+
const reputationArtifact = await loadExitArtifact(
|
|
23558
|
+
opts.bundleDir,
|
|
23559
|
+
manifest,
|
|
23560
|
+
"reputation_bundle"
|
|
23561
|
+
);
|
|
23562
|
+
const commitments = await loadExitArtifact(
|
|
23563
|
+
opts.bundleDir,
|
|
23564
|
+
manifest,
|
|
23565
|
+
"commitments"
|
|
23566
|
+
);
|
|
23567
|
+
const placeholderMetadata = await loadExitArtifact(
|
|
23568
|
+
opts.bundleDir,
|
|
23569
|
+
manifest,
|
|
23570
|
+
"placeholder_vault_metadata"
|
|
23571
|
+
);
|
|
23572
|
+
const conflicts = await conflictReport(
|
|
23573
|
+
opts.storage,
|
|
23574
|
+
identityArtifact?.json ?? null,
|
|
23575
|
+
encryptedState?.json ?? null,
|
|
23576
|
+
reputationArtifact?.json ?? null,
|
|
23577
|
+
manifest
|
|
23578
|
+
);
|
|
23579
|
+
if (!opts.activate) {
|
|
23580
|
+
return {
|
|
23581
|
+
verified: true,
|
|
23582
|
+
activated: false,
|
|
23583
|
+
conflicts,
|
|
23584
|
+
state: {
|
|
23585
|
+
status: "not_requested",
|
|
23586
|
+
imported_keys: 0,
|
|
23587
|
+
skipped_keys: 0,
|
|
23588
|
+
skipped_invalid_sig: 0,
|
|
23589
|
+
skipped_unknown_kid: 0,
|
|
23590
|
+
conflicts: conflicts.state_conflicts.length
|
|
23591
|
+
},
|
|
23592
|
+
reputation: {
|
|
23593
|
+
imported_attestations: 0,
|
|
23594
|
+
invalid_attestations: 0,
|
|
23595
|
+
unverifiable_attestations: verification.reputation?.unverifiable_attestations ?? 0
|
|
23596
|
+
},
|
|
23597
|
+
staged_artifacts: [],
|
|
23598
|
+
warnings: verification.warnings,
|
|
23599
|
+
unsupported_artifacts: verification.unsupported_artifacts
|
|
23600
|
+
};
|
|
23601
|
+
}
|
|
23602
|
+
const importId = importIdForManifest(manifest);
|
|
23603
|
+
const stagedArtifacts = [];
|
|
23604
|
+
if (identityArtifact) {
|
|
23605
|
+
await stageArtifact(
|
|
23606
|
+
opts.storage,
|
|
23607
|
+
EXIT_PUBLIC_IDENTITIES_NAMESPACE,
|
|
23608
|
+
identityArtifact.json.bundle.identity_id,
|
|
23609
|
+
identityArtifact.json
|
|
23610
|
+
);
|
|
23611
|
+
stagedArtifacts.push("public_identity");
|
|
23612
|
+
}
|
|
23613
|
+
if (policySet) {
|
|
23614
|
+
await stageArtifact(opts.storage, EXIT_POLICY_SETS_NAMESPACE, importId, policySet.json);
|
|
23615
|
+
stagedArtifacts.push("policy_set");
|
|
23616
|
+
}
|
|
23617
|
+
if (auditReceipts) {
|
|
23618
|
+
await stageArtifact(
|
|
23619
|
+
opts.storage,
|
|
23620
|
+
EXIT_AUDIT_RECEIPTS_NAMESPACE,
|
|
23621
|
+
importId,
|
|
23622
|
+
auditReceipts.json
|
|
23623
|
+
);
|
|
23624
|
+
stagedArtifacts.push("audit_receipts");
|
|
23625
|
+
}
|
|
23626
|
+
if (commitments) {
|
|
23627
|
+
await stageArtifact(opts.storage, EXIT_COMMITMENTS_NAMESPACE, importId, commitments.json);
|
|
23628
|
+
stagedArtifacts.push("commitments");
|
|
23629
|
+
}
|
|
23630
|
+
if (placeholderMetadata) {
|
|
23631
|
+
await stageArtifact(
|
|
23632
|
+
opts.storage,
|
|
23633
|
+
EXIT_PLACEHOLDER_METADATA_NAMESPACE,
|
|
23634
|
+
importId,
|
|
23635
|
+
placeholderMetadata.json
|
|
23636
|
+
);
|
|
23637
|
+
stagedArtifacts.push("placeholder_vault_metadata");
|
|
23638
|
+
}
|
|
23639
|
+
await stageArtifact(opts.storage, EXIT_IMPORT_NAMESPACE, importId, {
|
|
23640
|
+
manifest: manifest.body,
|
|
23641
|
+
verified_at: verification.verified_at,
|
|
23642
|
+
activated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
23643
|
+
});
|
|
23644
|
+
const publicKeys = identityArtifact ? publicKeysFromIdentityArtifact(identityArtifact.json) : { byIdentityId: /* @__PURE__ */ new Map(), byDid: /* @__PURE__ */ new Map() };
|
|
23645
|
+
let reputationResult = {
|
|
23646
|
+
imported_attestations: 0,
|
|
23647
|
+
invalid_attestations: 0,
|
|
23648
|
+
unverifiable_attestations: verification.reputation?.unverifiable_attestations ?? 0
|
|
23649
|
+
};
|
|
23650
|
+
if (reputationArtifact) {
|
|
23651
|
+
const reputationStore = opts.reputationStore ?? new ReputationStore(opts.storage, opts.masterKey);
|
|
23652
|
+
const imported = await reputationStore.importBundle(
|
|
23653
|
+
reputationArtifact.json,
|
|
23654
|
+
true,
|
|
23655
|
+
publicKeys.byDid
|
|
23656
|
+
);
|
|
23657
|
+
reputationResult = {
|
|
23658
|
+
imported_attestations: imported.imported,
|
|
23659
|
+
invalid_attestations: imported.invalid,
|
|
23660
|
+
unverifiable_attestations: verification.reputation?.unverifiable_attestations ?? 0
|
|
23661
|
+
};
|
|
23662
|
+
stagedArtifacts.push("reputation_bundle");
|
|
23663
|
+
}
|
|
23664
|
+
const sourceMasterKey = await resolveSourceMasterKey(
|
|
23665
|
+
encryptedState?.json ?? null,
|
|
23666
|
+
opts
|
|
23667
|
+
);
|
|
23668
|
+
const stateResult = encryptedState && encryptedState.json.entries.length > 0 ? sourceMasterKey ? await rekeyState(
|
|
23669
|
+
encryptedState.json,
|
|
23670
|
+
opts,
|
|
23671
|
+
sourceMasterKey,
|
|
23672
|
+
publicKeys.byIdentityId
|
|
23673
|
+
) : {
|
|
23674
|
+
status: "staged_requires_source_key",
|
|
23675
|
+
imported_keys: 0,
|
|
23676
|
+
skipped_keys: encryptedState.json.entries.length,
|
|
23677
|
+
skipped_invalid_sig: 0,
|
|
23678
|
+
skipped_unknown_kid: 0,
|
|
23679
|
+
conflicts: conflicts.state_conflicts.length
|
|
23680
|
+
} : {
|
|
23681
|
+
status: "not_requested",
|
|
23682
|
+
imported_keys: 0,
|
|
23683
|
+
skipped_keys: 0,
|
|
23684
|
+
skipped_invalid_sig: 0,
|
|
23685
|
+
skipped_unknown_kid: 0,
|
|
23686
|
+
conflicts: 0
|
|
23687
|
+
};
|
|
23688
|
+
opts.auditLog.append("l1", "exit_bundle_import_activate", manifest.body.identity_binding.identity_id, {
|
|
23689
|
+
import_id: importId,
|
|
23690
|
+
manifest_version: manifest.body.manifest_version,
|
|
23691
|
+
state_status: stateResult.status,
|
|
23692
|
+
state_imported_keys: stateResult.imported_keys,
|
|
23693
|
+
reputation_imported_attestations: reputationResult.imported_attestations
|
|
23694
|
+
});
|
|
23695
|
+
await opts.auditLog.flush();
|
|
23696
|
+
return {
|
|
23697
|
+
verified: true,
|
|
23698
|
+
activated: true,
|
|
23699
|
+
conflicts,
|
|
23700
|
+
state: stateResult,
|
|
23701
|
+
reputation: reputationResult,
|
|
23702
|
+
staged_artifacts: stagedArtifacts,
|
|
23703
|
+
warnings: verification.warnings,
|
|
23704
|
+
unsupported_artifacts: verification.unsupported_artifacts
|
|
23705
|
+
};
|
|
23706
|
+
}
|
|
23707
|
+
function exitBundleManifestShape() {
|
|
23708
|
+
return {
|
|
23709
|
+
manifest_version: EXIT_BUNDLE_MANIFEST_VERSION,
|
|
23710
|
+
artifacts: [...EXIT_BUNDLE_ARTIFACT_KINDS],
|
|
23711
|
+
hash_alg: "sha256",
|
|
23712
|
+
signature_scheme: SIGNATURE_SCHEME_V1,
|
|
23713
|
+
required_top_level_file: "manifest.json",
|
|
23714
|
+
artifact_paths: [
|
|
23715
|
+
"artifacts/public_identity.json",
|
|
23716
|
+
"artifacts/encrypted_state.json",
|
|
23717
|
+
"artifacts/policy_set.json",
|
|
23718
|
+
"artifacts/audit_receipts.json",
|
|
23719
|
+
"artifacts/reputation_bundle.json",
|
|
23720
|
+
"artifacts/commitments.json",
|
|
23721
|
+
"artifacts/placeholder_vault_metadata.json"
|
|
23722
|
+
]
|
|
23723
|
+
};
|
|
23724
|
+
}
|
|
23725
|
+
init_encoding();
|
|
23726
|
+
function write(stream, text) {
|
|
23727
|
+
stream.write(text);
|
|
23728
|
+
}
|
|
23729
|
+
function flagValue(argv, name) {
|
|
23730
|
+
const index = argv.indexOf(name);
|
|
23731
|
+
if (index === -1) return void 0;
|
|
23732
|
+
return argv[index + 1];
|
|
23733
|
+
}
|
|
23734
|
+
function hasFlag(argv, name) {
|
|
23735
|
+
return argv.includes(name);
|
|
23736
|
+
}
|
|
23737
|
+
function repeatedFlagValues(argv, name) {
|
|
23738
|
+
const values = [];
|
|
23739
|
+
for (let i = 0; i < argv.length; i++) {
|
|
23740
|
+
if (argv[i] === name && argv[i + 1]) values.push(argv[++i]);
|
|
23741
|
+
}
|
|
23742
|
+
return values;
|
|
23743
|
+
}
|
|
23744
|
+
async function confirmTier1(prompt, assumeYes, stdin, err) {
|
|
23745
|
+
if (assumeYes) return true;
|
|
23746
|
+
const readline = await import('readline/promises');
|
|
23747
|
+
const rl = readline.createInterface({
|
|
23748
|
+
input: stdin,
|
|
23749
|
+
output: err
|
|
23750
|
+
});
|
|
23751
|
+
const answer = await rl.question(`${prompt} [y/N] `);
|
|
23752
|
+
rl.close();
|
|
23753
|
+
return /^y(es)?$/i.test(answer.trim());
|
|
23754
|
+
}
|
|
23755
|
+
async function openExitContext(argv, env) {
|
|
23756
|
+
const passphrase = flagValue(argv, "--passphrase") ?? env.SANCTUARY_PASSPHRASE;
|
|
23757
|
+
const recoveryKey = env.SANCTUARY_RECOVERY_KEY;
|
|
23758
|
+
const config = await loadConfig();
|
|
23759
|
+
await mkdir(config.storage_path, { recursive: true, mode: 448 });
|
|
23760
|
+
const stateStoragePath = join(config.storage_path, "state");
|
|
23761
|
+
const storage = new FilesystemStorage(stateStoragePath);
|
|
23762
|
+
let masterKey;
|
|
23763
|
+
let keySource = "unknown";
|
|
23764
|
+
if (passphrase) {
|
|
23765
|
+
let existingParams;
|
|
23766
|
+
const raw = await storage.read("_meta", "key-params");
|
|
23767
|
+
if (raw) existingParams = JSON.parse(bytesToString(raw));
|
|
23768
|
+
const derived = await deriveMasterKey(passphrase, existingParams);
|
|
23769
|
+
masterKey = derived.key;
|
|
23770
|
+
if (!existingParams) {
|
|
23771
|
+
await storage.write(
|
|
23772
|
+
"_meta",
|
|
23773
|
+
"key-params",
|
|
23774
|
+
stringToBytes(JSON.stringify(derived.params))
|
|
23775
|
+
);
|
|
23776
|
+
}
|
|
23777
|
+
keySource = "passphrase";
|
|
23778
|
+
} else if (recoveryKey) {
|
|
23779
|
+
masterKey = fromBase64url(recoveryKey);
|
|
23780
|
+
if (masterKey.length !== 32) {
|
|
23781
|
+
throw new Error("SANCTUARY_RECOVERY_KEY must decode to 32 bytes.");
|
|
23782
|
+
}
|
|
23783
|
+
keySource = "recovery-key";
|
|
23784
|
+
} else {
|
|
23785
|
+
throw new Error(
|
|
23786
|
+
"sanctuary exit requires SANCTUARY_PASSPHRASE, --passphrase, or SANCTUARY_RECOVERY_KEY."
|
|
23787
|
+
);
|
|
23788
|
+
}
|
|
23789
|
+
const auditLog = new AuditLog(storage, masterKey);
|
|
23790
|
+
const identityManager = new IdentityManager(storage, masterKey);
|
|
23791
|
+
await identityManager.load();
|
|
23792
|
+
const reputationStore = new ReputationStore(storage, masterKey);
|
|
23793
|
+
return {
|
|
23794
|
+
storagePath: config.storage_path,
|
|
23795
|
+
stateStoragePath,
|
|
23796
|
+
storage,
|
|
23797
|
+
masterKey,
|
|
23798
|
+
auditLog,
|
|
23799
|
+
identityManager,
|
|
23800
|
+
reputationStore,
|
|
23801
|
+
keySource
|
|
23802
|
+
};
|
|
23803
|
+
}
|
|
23804
|
+
function printUsage(out) {
|
|
23805
|
+
write(out, `
|
|
23806
|
+
Usage: sanctuary exit <command> [options]
|
|
23807
|
+
|
|
23808
|
+
Commands:
|
|
23809
|
+
export --out <dir> Create a SANCTUARY_EXIT_BUNDLE_V1 directory
|
|
23810
|
+
verify <dir> Verify manifest, audit receipts, and reputation bundle
|
|
23811
|
+
import <dir> [--activate] Verify, report conflicts, and optionally activate
|
|
23812
|
+
manifest-shape Print the v1.1 manifest shape
|
|
23813
|
+
|
|
23814
|
+
Options:
|
|
23815
|
+
--passphrase <value> Current destination/source passphrase
|
|
23816
|
+
--source-passphrase <value> Source passphrase for state re-key on import
|
|
23817
|
+
--source-recovery-key <value> Source recovery key for state re-key on import
|
|
23818
|
+
--destination-identity-id <id> Destination signer for re-keyed state
|
|
23819
|
+
--state-namespace <name> Export a namespace; repeatable
|
|
23820
|
+
--conflict <skip|overwrite|version>
|
|
23821
|
+
--json
|
|
23822
|
+
--yes, -y Explicit non-interactive Tier 1 approval
|
|
23823
|
+
--help, -h
|
|
23824
|
+
`);
|
|
23825
|
+
}
|
|
23826
|
+
async function runExitCommand(args) {
|
|
23827
|
+
const argv = args.argv;
|
|
23828
|
+
const out = args.out ?? process.stdout;
|
|
23829
|
+
const err = args.err ?? process.stderr;
|
|
23830
|
+
const stdin = args.stdin ?? process.stdin;
|
|
23831
|
+
const env = args.env ?? process.env;
|
|
23832
|
+
if (argv.length === 0 || hasFlag(argv, "--help") || hasFlag(argv, "-h")) {
|
|
23833
|
+
printUsage(out);
|
|
23834
|
+
return 0;
|
|
23835
|
+
}
|
|
23836
|
+
const command = argv[0];
|
|
23837
|
+
const json = hasFlag(argv, "--json");
|
|
23838
|
+
try {
|
|
23839
|
+
if (command === "manifest-shape") {
|
|
23840
|
+
write(out, JSON.stringify(exitBundleManifestShape(), null, 2) + "\n");
|
|
23841
|
+
return 0;
|
|
23842
|
+
}
|
|
23843
|
+
if (command === "verify") {
|
|
23844
|
+
const dir = argv[1];
|
|
23845
|
+
if (!dir) {
|
|
23846
|
+
write(err, "Usage: sanctuary exit verify <dir>\n");
|
|
23847
|
+
return 2;
|
|
23848
|
+
}
|
|
23849
|
+
const result = await verifyExitBundle(dir);
|
|
23850
|
+
if (json) {
|
|
23851
|
+
write(out, JSON.stringify(result, null, 2) + "\n");
|
|
23852
|
+
} else {
|
|
23853
|
+
write(out, `manifest: ${result.passed ? "verified" : "failed"}
|
|
23854
|
+
`);
|
|
23855
|
+
write(out, `identity: ${result.manifest_summary.identity_id}
|
|
23856
|
+
`);
|
|
23857
|
+
write(out, `artifacts: ${result.manifest_summary.artifact_count}
|
|
23858
|
+
`);
|
|
23859
|
+
if (result.reputation) {
|
|
23860
|
+
write(
|
|
23861
|
+
out,
|
|
23862
|
+
`reputation: ${result.reputation.verified_attestations}/${result.reputation.attestation_count} attestations verified
|
|
23863
|
+
`
|
|
23864
|
+
);
|
|
23865
|
+
}
|
|
23866
|
+
for (const warning of result.warnings) write(out, `warning: ${warning}
|
|
23867
|
+
`);
|
|
23868
|
+
for (const item of result.unsupported_artifacts) {
|
|
23869
|
+
write(out, `unsupported: ${item}
|
|
23870
|
+
`);
|
|
23871
|
+
}
|
|
23872
|
+
}
|
|
23873
|
+
return result.passed ? 0 : 1;
|
|
23874
|
+
}
|
|
23875
|
+
if (command === "export") {
|
|
23876
|
+
const outDir = flagValue(argv, "--out");
|
|
23877
|
+
if (!outDir) {
|
|
23878
|
+
write(err, "Usage: sanctuary exit export --out <dir>\n");
|
|
23879
|
+
return 2;
|
|
23880
|
+
}
|
|
23881
|
+
const approved = await confirmTier1(
|
|
23882
|
+
"Tier 1 approval required: export complete Sanctuary exit bundle?",
|
|
23883
|
+
hasFlag(argv, "--yes") || hasFlag(argv, "-y"),
|
|
23884
|
+
stdin,
|
|
23885
|
+
err
|
|
23886
|
+
);
|
|
23887
|
+
if (!approved) {
|
|
23888
|
+
write(err, "Aborted.\n");
|
|
23889
|
+
return 1;
|
|
23890
|
+
}
|
|
23891
|
+
const config = await loadConfig();
|
|
23892
|
+
const ctx = await openExitContext(argv, env);
|
|
23893
|
+
const policy = await loadPrincipalPolicy(ctx.storagePath);
|
|
23894
|
+
const result = await exportExitBundle({
|
|
23895
|
+
bundleDir: outDir,
|
|
23896
|
+
storage: ctx.storage,
|
|
23897
|
+
masterKey: ctx.masterKey,
|
|
23898
|
+
identityManager: ctx.identityManager,
|
|
23899
|
+
auditLog: ctx.auditLog,
|
|
23900
|
+
reputationStore: ctx.reputationStore,
|
|
23901
|
+
policy,
|
|
23902
|
+
config,
|
|
23903
|
+
stateStoragePath: ctx.stateStoragePath,
|
|
23904
|
+
stateNamespaces: repeatedFlagValues(argv, "--state-namespace"),
|
|
23905
|
+
keySource: ctx.keySource
|
|
23906
|
+
});
|
|
23907
|
+
if (json) write(out, JSON.stringify(result, null, 2) + "\n");
|
|
23908
|
+
else {
|
|
23909
|
+
write(out, `exported: ${result.bundle_dir}
|
|
23910
|
+
`);
|
|
23911
|
+
write(out, `manifest_hash: ${result.manifest_hash}
|
|
23912
|
+
`);
|
|
23913
|
+
for (const item of result.unsupported_artifacts) {
|
|
23914
|
+
write(out, `unsupported: ${item}
|
|
23915
|
+
`);
|
|
23916
|
+
}
|
|
23917
|
+
}
|
|
23918
|
+
return 0;
|
|
23919
|
+
}
|
|
23920
|
+
if (command === "import") {
|
|
23921
|
+
const dir = argv[1];
|
|
23922
|
+
if (!dir) {
|
|
23923
|
+
write(err, "Usage: sanctuary exit import <dir> [--activate]\n");
|
|
23924
|
+
return 2;
|
|
23925
|
+
}
|
|
23926
|
+
const activate = hasFlag(argv, "--activate");
|
|
23927
|
+
if (activate) {
|
|
23928
|
+
const approved = await confirmTier1(
|
|
23929
|
+
"Tier 1 approval required: activate verified imported exit bundle?",
|
|
23930
|
+
hasFlag(argv, "--yes") || hasFlag(argv, "-y"),
|
|
23931
|
+
stdin,
|
|
23932
|
+
err
|
|
23933
|
+
);
|
|
23934
|
+
if (!approved) {
|
|
23935
|
+
write(err, "Aborted.\n");
|
|
23936
|
+
return 1;
|
|
23937
|
+
}
|
|
23938
|
+
}
|
|
23939
|
+
const ctx = await openExitContext(argv, env);
|
|
23940
|
+
const conflict = flagValue(argv, "--conflict") ?? "skip";
|
|
23941
|
+
if (!["skip", "overwrite", "version"].includes(conflict)) {
|
|
23942
|
+
write(err, "--conflict must be skip, overwrite, or version\n");
|
|
23943
|
+
return 2;
|
|
23944
|
+
}
|
|
23945
|
+
const result = await importExitBundle({
|
|
23946
|
+
bundleDir: dir,
|
|
23947
|
+
storage: ctx.storage,
|
|
23948
|
+
masterKey: ctx.masterKey,
|
|
23949
|
+
identityManager: ctx.identityManager,
|
|
23950
|
+
auditLog: ctx.auditLog,
|
|
23951
|
+
reputationStore: ctx.reputationStore,
|
|
23952
|
+
activate,
|
|
23953
|
+
conflictResolution: conflict,
|
|
23954
|
+
sourcePassphrase: flagValue(argv, "--source-passphrase"),
|
|
23955
|
+
sourceRecoveryKey: flagValue(argv, "--source-recovery-key"),
|
|
23956
|
+
destinationSignerIdentityId: flagValue(argv, "--destination-identity-id")
|
|
23957
|
+
});
|
|
23958
|
+
if (json) write(out, JSON.stringify(result, null, 2) + "\n");
|
|
23959
|
+
else {
|
|
23960
|
+
write(out, `verified: ${result.verified}
|
|
23961
|
+
`);
|
|
23962
|
+
write(out, `activated: ${result.activated}
|
|
23963
|
+
`);
|
|
23964
|
+
write(out, `state_conflicts: ${result.conflicts.state_conflicts.length}
|
|
23965
|
+
`);
|
|
23966
|
+
write(out, `reputation_conflicts: ${result.conflicts.reputation_conflicts.length}
|
|
23967
|
+
`);
|
|
23968
|
+
write(out, `state_status: ${result.state.status}
|
|
23969
|
+
`);
|
|
23970
|
+
write(out, `state_imported_keys: ${result.state.imported_keys}
|
|
23971
|
+
`);
|
|
23972
|
+
write(out, `reputation_imported_attestations: ${result.reputation.imported_attestations}
|
|
23973
|
+
`);
|
|
23974
|
+
for (const warning of result.warnings) write(out, `warning: ${warning}
|
|
23975
|
+
`);
|
|
23976
|
+
for (const item of result.unsupported_artifacts) {
|
|
23977
|
+
write(out, `unsupported: ${item}
|
|
23978
|
+
`);
|
|
23979
|
+
}
|
|
23980
|
+
}
|
|
23981
|
+
return result.verified ? 0 : 1;
|
|
23982
|
+
}
|
|
23983
|
+
write(err, `Unknown exit command: ${command}
|
|
23984
|
+
`);
|
|
23985
|
+
return 2;
|
|
23986
|
+
} catch (error) {
|
|
23987
|
+
write(err, error instanceof Error ? `${error.message}
|
|
23988
|
+
` : `${String(error)}
|
|
23989
|
+
`);
|
|
23990
|
+
return 1;
|
|
23991
|
+
}
|
|
23992
|
+
}
|
|
23993
|
+
|
|
23994
|
+
// src/dashboard/aggregator.ts
|
|
23995
|
+
var L4_DEGRADATION_IMPACT = {
|
|
23996
|
+
critical: 40,
|
|
23997
|
+
warning: 25,
|
|
23998
|
+
info: 10
|
|
23999
|
+
};
|
|
24000
|
+
function computeL4LayerScore(degradations, status) {
|
|
24001
|
+
if (status === "compromised") return 0;
|
|
24002
|
+
let score = 100;
|
|
24003
|
+
for (const deg of degradations) {
|
|
24004
|
+
score -= L4_DEGRADATION_IMPACT[deg.severity] ?? 10;
|
|
24005
|
+
}
|
|
24006
|
+
score = Math.max(0, score);
|
|
24007
|
+
if (degradations.length === 0 && score > 50) {
|
|
24008
|
+
score = Math.min(100, score + 5);
|
|
24009
|
+
}
|
|
24010
|
+
return Math.round(score);
|
|
24011
|
+
}
|
|
24012
|
+
var MAX_ACTIVITY = 50;
|
|
24013
|
+
var MAX_AUDIT = 50;
|
|
24014
|
+
function fingerprintDID(did) {
|
|
24015
|
+
const raw = did.replace(/^did:[a-z0-9]+:/i, "");
|
|
24016
|
+
if (raw.length <= 12) return raw;
|
|
24017
|
+
return `${raw.slice(0, 6)}\u2026${raw.slice(-6)}`;
|
|
24018
|
+
}
|
|
24019
|
+
function countInjectionsToday(audit) {
|
|
24020
|
+
const startOfDay = /* @__PURE__ */ new Date();
|
|
24021
|
+
startOfDay.setHours(0, 0, 0, 0);
|
|
24022
|
+
const cutoff = startOfDay.getTime();
|
|
24023
|
+
return audit.filter((e) => {
|
|
24024
|
+
const ts = new Date(e.timestamp).getTime();
|
|
24025
|
+
if (isNaN(ts) || ts < cutoff) return false;
|
|
24026
|
+
const op = (e.operation ?? "").toLowerCase();
|
|
24027
|
+
return op.includes("injection") || op.includes("blocked");
|
|
24028
|
+
}).length;
|
|
24029
|
+
}
|
|
24030
|
+
var PROOF_CREATION_OPS = /* @__PURE__ */ new Set([
|
|
24031
|
+
"zk_prove",
|
|
24032
|
+
"zk_range_prove",
|
|
24033
|
+
"proof_commitment"
|
|
24034
|
+
]);
|
|
24035
|
+
function countProofsToday(audit) {
|
|
24036
|
+
const startOfDay = /* @__PURE__ */ new Date();
|
|
24037
|
+
startOfDay.setHours(0, 0, 0, 0);
|
|
24038
|
+
const cutoff = startOfDay.getTime();
|
|
24039
|
+
return audit.filter((e) => {
|
|
24040
|
+
if (e.layer !== "l3") return false;
|
|
24041
|
+
if (!PROOF_CREATION_OPS.has(e.operation)) return false;
|
|
24042
|
+
const ts = new Date(e.timestamp).getTime();
|
|
24043
|
+
return !isNaN(ts) && ts >= cutoff;
|
|
24044
|
+
}).length;
|
|
24045
|
+
}
|
|
24046
|
+
function buildAgent(sources) {
|
|
24047
|
+
if (!sources.identityManager) {
|
|
24048
|
+
return {
|
|
24049
|
+
display_name: "Unclaimed agent",
|
|
24050
|
+
did: null,
|
|
24051
|
+
did_fingerprint: null,
|
|
24052
|
+
identity_count: 0,
|
|
24053
|
+
primary_identity_id: null
|
|
24054
|
+
};
|
|
24055
|
+
}
|
|
24056
|
+
const primary = sources.identityManager.getDefault();
|
|
24057
|
+
const identities = sources.identityManager.list();
|
|
24058
|
+
if (!primary) {
|
|
24059
|
+
return {
|
|
24060
|
+
display_name: "Unclaimed agent",
|
|
24061
|
+
did: null,
|
|
24062
|
+
did_fingerprint: null,
|
|
24063
|
+
identity_count: identities.length,
|
|
24064
|
+
primary_identity_id: null
|
|
24065
|
+
};
|
|
24066
|
+
}
|
|
24067
|
+
return {
|
|
24068
|
+
display_name: primary.label || "Sovereign agent",
|
|
24069
|
+
did: primary.did,
|
|
24070
|
+
did_fingerprint: fingerprintDID(primary.did),
|
|
24071
|
+
identity_count: identities.length,
|
|
24072
|
+
primary_identity_id: primary.identity_id
|
|
24073
|
+
};
|
|
24074
|
+
}
|
|
24075
|
+
function buildL1(sources, audit) {
|
|
24076
|
+
const hasIdentity = !!sources.identityManager?.getDefault();
|
|
24077
|
+
const state = hasIdentity ? "full" : "degraded";
|
|
24078
|
+
return {
|
|
24079
|
+
label: "L1 Cognitive",
|
|
24080
|
+
state,
|
|
24081
|
+
headline: hasIdentity ? "State encrypted at rest" : "No sovereign identity \u2014 run sanctuary_bootstrap",
|
|
24082
|
+
encryption: "AES-256-GCM + HKDF per namespace",
|
|
24083
|
+
injection_blocked_today: countInjectionsToday(audit),
|
|
24084
|
+
memory_attest_ready: hasIdentity
|
|
24085
|
+
};
|
|
24086
|
+
}
|
|
24087
|
+
function buildL2(sources) {
|
|
24088
|
+
const teeAvailable = sources.teeAvailable ?? false;
|
|
24089
|
+
const state = teeAvailable ? "full" : "degraded";
|
|
21483
24090
|
return {
|
|
21484
24091
|
label: "L2 Operational",
|
|
21485
24092
|
state,
|
|
@@ -21613,6 +24220,36 @@ function buildUpstreamServers(sources) {
|
|
|
21613
24220
|
return entry;
|
|
21614
24221
|
});
|
|
21615
24222
|
}
|
|
24223
|
+
function buildPrivacySummary(audit) {
|
|
24224
|
+
const classes = {};
|
|
24225
|
+
let filteredEvents = 0;
|
|
24226
|
+
let filteredSpans = 0;
|
|
24227
|
+
let lastFilteredAt = null;
|
|
24228
|
+
for (const entry of audit) {
|
|
24229
|
+
const details = entry.details ?? {};
|
|
24230
|
+
const rawFindings = details.privacy_findings;
|
|
24231
|
+
const findings = typeof rawFindings === "number" && Number.isFinite(rawFindings) ? Math.max(0, rawFindings) : 0;
|
|
24232
|
+
if (findings <= 0) continue;
|
|
24233
|
+
filteredEvents++;
|
|
24234
|
+
filteredSpans += findings;
|
|
24235
|
+
if (!lastFilteredAt || new Date(entry.timestamp).getTime() > new Date(lastFilteredAt).getTime()) {
|
|
24236
|
+
lastFilteredAt = entry.timestamp;
|
|
24237
|
+
}
|
|
24238
|
+
const rawClasses = details.privacy_classes;
|
|
24239
|
+
if (Array.isArray(rawClasses)) {
|
|
24240
|
+
for (const rawClass of rawClasses) {
|
|
24241
|
+
const key = String(rawClass);
|
|
24242
|
+
classes[key] = (classes[key] ?? 0) + 1;
|
|
24243
|
+
}
|
|
24244
|
+
}
|
|
24245
|
+
}
|
|
24246
|
+
return {
|
|
24247
|
+
filtered_events: filteredEvents,
|
|
24248
|
+
filtered_spans: filteredSpans,
|
|
24249
|
+
classes,
|
|
24250
|
+
last_filtered_at: lastFilteredAt
|
|
24251
|
+
};
|
|
24252
|
+
}
|
|
21616
24253
|
async function getProtectionSnapshot(sources) {
|
|
21617
24254
|
let audit = [];
|
|
21618
24255
|
if (sources.auditLog) {
|
|
@@ -21630,6 +24267,7 @@ async function getProtectionSnapshot(sources) {
|
|
|
21630
24267
|
const l4 = buildL4(sources);
|
|
21631
24268
|
const activity = (sources.activity ?? []).slice(0, MAX_ACTIVITY);
|
|
21632
24269
|
const pending_approvals = sources.pendingApprovals ?? [];
|
|
24270
|
+
const privacy = buildPrivacySummary(audit);
|
|
21633
24271
|
const upstream_servers = buildUpstreamServers(sources);
|
|
21634
24272
|
return {
|
|
21635
24273
|
overall: computeOverall(l1, l2, l3, l4),
|
|
@@ -21638,6 +24276,7 @@ async function getProtectionSnapshot(sources) {
|
|
|
21638
24276
|
activity,
|
|
21639
24277
|
pending_approvals,
|
|
21640
24278
|
audit: audit.slice(-MAX_AUDIT).reverse(),
|
|
24279
|
+
privacy,
|
|
21641
24280
|
upstream_servers,
|
|
21642
24281
|
mode: sources.mode,
|
|
21643
24282
|
server_version: sources.server_version,
|
|
@@ -21751,7 +24390,7 @@ function l4EvidenceBlock(l4) {
|
|
|
21751
24390
|
}
|
|
21752
24391
|
function renderDashboardHTML(options) {
|
|
21753
24392
|
const { snapshot } = options;
|
|
21754
|
-
const { overall, agent, layers, activity, pending_approvals, audit, upstream_servers } = snapshot;
|
|
24393
|
+
const { overall, agent, layers, activity, pending_approvals, audit, privacy, upstream_servers } = snapshot;
|
|
21755
24394
|
const activityRows = activity.length === 0 ? `<tr class="empty"><td colspan="5">Waiting for tool calls\u2026</td></tr>` : activity.map((entry) => {
|
|
21756
24395
|
const time = new Date(entry.timestamp).toLocaleTimeString();
|
|
21757
24396
|
return `<tr class="result-${escHtml(entry.result)}">
|
|
@@ -21784,6 +24423,8 @@ function renderDashboardHTML(options) {
|
|
|
21784
24423
|
<span class="mono">${escHtml(s.name)}</span>
|
|
21785
24424
|
<span class="server-meta">${escHtml(s.state)} \xB7 ${escHtml(s.tool_count)} tool${s.tool_count === 1 ? "" : "s"}</span>
|
|
21786
24425
|
</li>`).join("");
|
|
24426
|
+
const privacyClasses = Object.entries(privacy.classes).sort((a, b) => b[1] - a[1]).map(([name, count]) => `<span class="privacy-chip">${escHtml(name)} ${escHtml(count)}</span>`).join("");
|
|
24427
|
+
const privacyLast = privacy.last_filtered_at ? new Date(privacy.last_filtered_at).toLocaleString() : "No filtering events yet";
|
|
21787
24428
|
const initialSnapshot = JSON.stringify(snapshot).replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
21788
24429
|
return `<!DOCTYPE html>
|
|
21789
24430
|
<html lang="en">
|
|
@@ -22061,6 +24702,13 @@ button { font: inherit; cursor: pointer; }
|
|
|
22061
24702
|
.panel { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }
|
|
22062
24703
|
.panel-head { display: flex; justify-content: space-between; align-items: center; padding: 12px 18px; border-bottom: 1px solid var(--border); }
|
|
22063
24704
|
.panel-head h3 { font-size: 12px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink-dim); }
|
|
24705
|
+
.privacy-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
|
|
24706
|
+
.privacy-metric { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 14px 16px; }
|
|
24707
|
+
.privacy-metric dt { color: var(--ink-mute); font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 6px; }
|
|
24708
|
+
.privacy-metric dd { color: var(--ink); font-size: 22px; font-weight: 700; }
|
|
24709
|
+
.privacy-metric dd.small { font-size: 13px; font-weight: 500; color: var(--ink-dim); line-height: 1.35; overflow-wrap: anywhere; }
|
|
24710
|
+
.privacy-classes { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
|
|
24711
|
+
.privacy-chip { border: 1px solid var(--border); color: var(--ink-dim); border-radius: 999px; padding: 4px 8px; font-size: 12px; background: rgba(255,255,255,0.03); }
|
|
22064
24712
|
.filter-row { display: flex; gap: 6px; }
|
|
22065
24713
|
.filter-row button {
|
|
22066
24714
|
padding: 4px 10px;
|
|
@@ -22158,6 +24806,27 @@ details.audit-details .audit-filters { display: flex; gap: 6px; padding: 0 18px
|
|
|
22158
24806
|
<ul class="server-list" id="server-list">${serverRows}</ul>
|
|
22159
24807
|
</section>
|
|
22160
24808
|
|
|
24809
|
+
<section class="section">
|
|
24810
|
+
<h2>Privacy boundary <span class="count" id="privacy-count">${privacy.filtered_spans}</span></h2>
|
|
24811
|
+
<dl class="privacy-grid">
|
|
24812
|
+
<div class="privacy-metric">
|
|
24813
|
+
<dt>Filtered spans</dt>
|
|
24814
|
+
<dd id="privacy-spans">${escHtml(privacy.filtered_spans)}</dd>
|
|
24815
|
+
</div>
|
|
24816
|
+
<div class="privacy-metric">
|
|
24817
|
+
<dt>Filtered events</dt>
|
|
24818
|
+
<dd id="privacy-events">${escHtml(privacy.filtered_events)}</dd>
|
|
24819
|
+
</div>
|
|
24820
|
+
<div class="privacy-metric">
|
|
24821
|
+
<dt>Last filter</dt>
|
|
24822
|
+
<dd class="small" id="privacy-last">${escHtml(privacyLast)}</dd>
|
|
24823
|
+
</div>
|
|
24824
|
+
</dl>
|
|
24825
|
+
<div class="privacy-classes" id="privacy-classes">
|
|
24826
|
+
${privacyClasses || `<span class="privacy-chip">No classes recorded</span>`}
|
|
24827
|
+
</div>
|
|
24828
|
+
</section>
|
|
24829
|
+
|
|
22161
24830
|
<section class="section">
|
|
22162
24831
|
<h2>Live activity <span class="count" id="activity-count">${activity.length}</span></h2>
|
|
22163
24832
|
<div class="panel">
|
|
@@ -22285,6 +24954,23 @@ details.audit-details .audit-filters { display: flex; gap: 6px; padding: 0 18px
|
|
|
22285
24954
|
)).join("");
|
|
22286
24955
|
}
|
|
22287
24956
|
|
|
24957
|
+
function renderPrivacy(summary) {
|
|
24958
|
+
const count = document.getElementById("privacy-count");
|
|
24959
|
+
const spans = document.getElementById("privacy-spans");
|
|
24960
|
+
const events = document.getElementById("privacy-events");
|
|
24961
|
+
const last = document.getElementById("privacy-last");
|
|
24962
|
+
const classes = document.getElementById("privacy-classes");
|
|
24963
|
+
if (!summary || !spans || !events || !last || !classes) return;
|
|
24964
|
+
if (count) count.textContent = String(summary.filtered_spans || 0);
|
|
24965
|
+
spans.textContent = String(summary.filtered_spans || 0);
|
|
24966
|
+
events.textContent = String(summary.filtered_events || 0);
|
|
24967
|
+
last.textContent = summary.last_filtered_at ? new Date(summary.last_filtered_at).toLocaleString() : "No filtering events yet";
|
|
24968
|
+
const rows = Object.entries(summary.classes || {}).sort((a, b) => b[1] - a[1]);
|
|
24969
|
+
classes.innerHTML = rows.length
|
|
24970
|
+
? rows.map(([name, n]) => '<span class="privacy-chip">' + esc(name) + ' ' + esc(n) + '</span>').join("")
|
|
24971
|
+
: '<span class="privacy-chip">No classes recorded</span>';
|
|
24972
|
+
}
|
|
24973
|
+
|
|
22288
24974
|
function renderAll(snap) {
|
|
22289
24975
|
snapshot = snap;
|
|
22290
24976
|
renderShield(snap.overall.light, snap.overall.headline);
|
|
@@ -22294,6 +24980,7 @@ details.audit-details .audit-filters { display: flex; gap: 6px; padding: 0 18px
|
|
|
22294
24980
|
document.getElementById("agent-did").textContent = snap.agent.did_fingerprint || "unclaimed";
|
|
22295
24981
|
renderActivity(snap.activity);
|
|
22296
24982
|
renderApprovals(snap.pending_approvals);
|
|
24983
|
+
renderPrivacy(snap.privacy);
|
|
22297
24984
|
renderAudit(snap.audit, currentAuditFilter());
|
|
22298
24985
|
}
|
|
22299
24986
|
|
|
@@ -22761,88 +25448,6 @@ function applyChannelTemplate(id, params) {
|
|
|
22761
25448
|
return entry.factory(params);
|
|
22762
25449
|
}
|
|
22763
25450
|
|
|
22764
|
-
// src/mesh/errors.ts
|
|
22765
|
-
var MeshError = class extends Error {
|
|
22766
|
-
constructor(message) {
|
|
22767
|
-
super(message);
|
|
22768
|
-
this.name = "MeshError";
|
|
22769
|
-
}
|
|
22770
|
-
};
|
|
22771
|
-
var MeshEnvelopeError = class extends MeshError {
|
|
22772
|
-
constructor(message) {
|
|
22773
|
-
super(message);
|
|
22774
|
-
this.name = "MeshEnvelopeError";
|
|
22775
|
-
}
|
|
22776
|
-
};
|
|
22777
|
-
var MeshReservedExtensionKeyError = class extends MeshEnvelopeError {
|
|
22778
|
-
constructor(key) {
|
|
22779
|
-
super(
|
|
22780
|
-
`v0.1 emitters MUST NOT populate reserved extension_envelope key: ${key}`
|
|
22781
|
-
);
|
|
22782
|
-
this.name = "MeshReservedExtensionKeyError";
|
|
22783
|
-
}
|
|
22784
|
-
};
|
|
22785
|
-
var MeshReservedEventTypeError = class extends MeshEnvelopeError {
|
|
22786
|
-
constructor(eventType) {
|
|
22787
|
-
super(
|
|
22788
|
-
`v0.1 emitters MUST NOT emit reserved-namespace event_type: ${eventType}`
|
|
22789
|
-
);
|
|
22790
|
-
this.name = "MeshReservedEventTypeError";
|
|
22791
|
-
}
|
|
22792
|
-
};
|
|
22793
|
-
|
|
22794
|
-
// src/mesh/canonical-json.ts
|
|
22795
|
-
var MeshCanonicalJsonError = class extends MeshError {
|
|
22796
|
-
constructor(message) {
|
|
22797
|
-
super(message);
|
|
22798
|
-
this.name = "MeshCanonicalJsonError";
|
|
22799
|
-
}
|
|
22800
|
-
};
|
|
22801
|
-
function canonicalize2(value) {
|
|
22802
|
-
if (value === void 0) {
|
|
22803
|
-
throw new MeshCanonicalJsonError(
|
|
22804
|
-
"canonicalize(): top-level undefined is not serializable"
|
|
22805
|
-
);
|
|
22806
|
-
}
|
|
22807
|
-
return encode(value);
|
|
22808
|
-
}
|
|
22809
|
-
function encode(value) {
|
|
22810
|
-
if (value === null) return "null";
|
|
22811
|
-
if (typeof value === "boolean") return value ? "true" : "false";
|
|
22812
|
-
if (typeof value === "number") {
|
|
22813
|
-
if (!Number.isFinite(value)) {
|
|
22814
|
-
throw new MeshCanonicalJsonError(
|
|
22815
|
-
`canonicalize(): non-finite number (${String(value)}) is not serializable`
|
|
22816
|
-
);
|
|
22817
|
-
}
|
|
22818
|
-
return JSON.stringify(value);
|
|
22819
|
-
}
|
|
22820
|
-
if (typeof value === "string") return JSON.stringify(value);
|
|
22821
|
-
if (Array.isArray(value)) return encodeArray(value);
|
|
22822
|
-
if (typeof value === "object") return encodeObject(value);
|
|
22823
|
-
throw new MeshCanonicalJsonError(
|
|
22824
|
-
`canonicalize(): unsupported type ${typeof value}`
|
|
22825
|
-
);
|
|
22826
|
-
}
|
|
22827
|
-
function encodeArray(arr) {
|
|
22828
|
-
const parts = [];
|
|
22829
|
-
for (const item of arr) {
|
|
22830
|
-
parts.push(item === void 0 ? "null" : encode(item));
|
|
22831
|
-
}
|
|
22832
|
-
return "[" + parts.join(",") + "]";
|
|
22833
|
-
}
|
|
22834
|
-
function encodeObject(obj) {
|
|
22835
|
-
const keys = Object.keys(obj).filter((k) => obj[k] !== void 0).sort();
|
|
22836
|
-
const parts = [];
|
|
22837
|
-
for (const k of keys) {
|
|
22838
|
-
parts.push(JSON.stringify(k) + ":" + encode(obj[k]));
|
|
22839
|
-
}
|
|
22840
|
-
return "{" + parts.join(",") + "}";
|
|
22841
|
-
}
|
|
22842
|
-
function canonicalizeToBytes(value) {
|
|
22843
|
-
return new TextEncoder().encode(canonicalize2(value));
|
|
22844
|
-
}
|
|
22845
|
-
|
|
22846
25451
|
// src/policy-engine/canonical-policy.ts
|
|
22847
25452
|
init_encoding();
|
|
22848
25453
|
|
|
@@ -23355,8 +25960,8 @@ var EXTRAS_FILE_NAME = "agents-extra.json";
|
|
|
23355
25960
|
async function isTenantDir(path) {
|
|
23356
25961
|
const [hasState, hasProfile, hasFallback] = await Promise.all([
|
|
23357
25962
|
dirExists(join(path, "state")),
|
|
23358
|
-
|
|
23359
|
-
|
|
25963
|
+
fileExists3(join(path, "cocoon-profile.json")),
|
|
25964
|
+
fileExists3(join(path, "passphrase.enc"))
|
|
23360
25965
|
]);
|
|
23361
25966
|
const initialized = hasState;
|
|
23362
25967
|
let passphraseStatus;
|
|
@@ -23373,7 +25978,7 @@ async function dirExists(path) {
|
|
|
23373
25978
|
return false;
|
|
23374
25979
|
}
|
|
23375
25980
|
}
|
|
23376
|
-
async function
|
|
25981
|
+
async function fileExists3(path) {
|
|
23377
25982
|
try {
|
|
23378
25983
|
const s = await stat(path);
|
|
23379
25984
|
return s.isFile();
|
|
@@ -23753,11 +26358,11 @@ async function startDashboardServer(options) {
|
|
|
23753
26358
|
}
|
|
23754
26359
|
}
|
|
23755
26360
|
});
|
|
23756
|
-
await new Promise((
|
|
26361
|
+
await new Promise((resolve4, reject) => {
|
|
23757
26362
|
server.once("error", reject);
|
|
23758
26363
|
server.listen(port, host, () => {
|
|
23759
26364
|
server.off("error", reject);
|
|
23760
|
-
|
|
26365
|
+
resolve4();
|
|
23761
26366
|
});
|
|
23762
26367
|
});
|
|
23763
26368
|
const actualPort = (() => {
|
|
@@ -23770,12 +26375,14 @@ async function startDashboardServer(options) {
|
|
|
23770
26375
|
url,
|
|
23771
26376
|
port: actualPort,
|
|
23772
26377
|
host,
|
|
23773
|
-
stop: () => new Promise((
|
|
23774
|
-
server.close((err) => err ? reject(err) :
|
|
26378
|
+
stop: () => new Promise((resolve4, reject) => {
|
|
26379
|
+
server.close((err) => err ? reject(err) : resolve4());
|
|
23775
26380
|
}),
|
|
23776
26381
|
publish,
|
|
23777
26382
|
publishActivity: (entry) => publish({ type: "activity", data: entry }),
|
|
23778
|
-
publishApproval: (approval) => publish({ type: "approval", data: approval })
|
|
26383
|
+
publishApproval: (approval) => publish({ type: "approval", data: approval }),
|
|
26384
|
+
publishInbox: (item) => publish({ type: "inbox", data: item }),
|
|
26385
|
+
publishAgentStatus: (snapshot) => publish({ type: "agent_status", data: snapshot })
|
|
23779
26386
|
};
|
|
23780
26387
|
}
|
|
23781
26388
|
|
|
@@ -23910,6 +26517,20 @@ async function createSanctuaryServer(options) {
|
|
|
23910
26517
|
}
|
|
23911
26518
|
}
|
|
23912
26519
|
const auditLog = new AuditLog(storage, masterKey);
|
|
26520
|
+
try {
|
|
26521
|
+
await consumeResetHistoryMarker({
|
|
26522
|
+
storagePath: config.storage_path,
|
|
26523
|
+
auditLog
|
|
26524
|
+
});
|
|
26525
|
+
} catch (err) {
|
|
26526
|
+
if (err instanceof ResetHistoryMalformedError) {
|
|
26527
|
+
throw new Error(
|
|
26528
|
+
`Sanctuary: ${err.message}
|
|
26529
|
+
Refusing to start the cocoon while the reset-history marker is unreadable.`
|
|
26530
|
+
);
|
|
26531
|
+
}
|
|
26532
|
+
throw err;
|
|
26533
|
+
}
|
|
23913
26534
|
const stateStore = new StateStore(storage, masterKey);
|
|
23914
26535
|
const { tools: l1Tools, identityManager } = createL1Tools(
|
|
23915
26536
|
stateStore,
|
|
@@ -24183,7 +26804,9 @@ async function createSanctuaryServer(options) {
|
|
|
24183
26804
|
);
|
|
24184
26805
|
const { tools: auditTools } = createAuditTools(config);
|
|
24185
26806
|
const { tools: siemTools } = createSIEMTools(auditLog);
|
|
24186
|
-
const { tools: contextGateTools, enforcer: contextGateEnforcer } = createContextGateTools(storage, masterKey, auditLog
|
|
26807
|
+
const { tools: contextGateTools, enforcer: contextGateEnforcer } = createContextGateTools(storage, masterKey, auditLog, {
|
|
26808
|
+
privacyFilter: config.privacy_filter
|
|
26809
|
+
});
|
|
24187
26810
|
const hardeningTools = createL2HardeningTools(config.storage_path, auditLog);
|
|
24188
26811
|
const profileStore = new SovereigntyProfileStore(storage, masterKey);
|
|
24189
26812
|
await profileStore.load();
|
|
@@ -24372,7 +26995,7 @@ async function createSanctuaryServer(options) {
|
|
|
24372
26995
|
clientManager.configure(enabledServers).catch((err) => {
|
|
24373
26996
|
console.error(`[Sanctuary] Failed to configure upstream servers: ${err instanceof Error ? err.message : "unknown error"}`);
|
|
24374
26997
|
});
|
|
24375
|
-
await new Promise((
|
|
26998
|
+
await new Promise((resolve4) => setTimeout(resolve4, 2e3));
|
|
24376
26999
|
const proxiedTools = proxyRouter.getProxiedTools();
|
|
24377
27000
|
if (proxiedTools.length > 0) {
|
|
24378
27001
|
allTools.push(...proxiedTools);
|
|
@@ -24414,7 +27037,7 @@ async function createSanctuaryServer(options) {
|
|
|
24414
27037
|
if (recoveryKey) {
|
|
24415
27038
|
console.error(
|
|
24416
27039
|
`\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
|
|
24417
|
-
\u2551 SANCTUARY: First Run
|
|
27040
|
+
\u2551 SANCTUARY: First Run, Recovery Key Generated \u2551
|
|
24418
27041
|
\u2551 \u2551
|
|
24419
27042
|
\u2551 Recovery Key: ${recoveryKey.slice(0, 20)}... \u2551
|
|
24420
27043
|
\u2551 \u2551
|
|
@@ -24433,6 +27056,6 @@ async function createSanctuaryServer(options) {
|
|
|
24433
27056
|
};
|
|
24434
27057
|
}
|
|
24435
27058
|
|
|
24436
|
-
export { ATTESTATION_VERSION, ApprovalGate, AuditLog, AutoApproveChannel, BaselineTracker, TEMPLATES as CONTEXT_GATE_TEMPLATES, CallbackApprovalChannel, ClientManager, CommitmentStore, ContextGateEnforcer, ContextGatePolicyStore, DashboardApprovalChannel, FederationRegistry, FilesystemStorage, HERO_COPY, InMemoryModelProvenanceStore, InjectionDetector, MODEL_PRESETS, MemoryStorage, PolicyStore, ProxyRouter, ReputationStore, SovereigntyProfileStore, StateStore, StderrApprovalChannel, TIER_WEIGHTS, WebhookApprovalChannel, canonicalize, classifyField, completeHandshake, computeWeightedScore, createBridgeCommitment, createDefaultProfile, createPedersenCommitment, createProofOfKnowledge, createRangeProof, createSanctuaryServer, evaluateField, filterContext, generateAttestation, generateSHR, generateSystemPrompt, getProtectionSnapshot, getTemplate, initiateHandshake, listTemplateIds, loadConfig, loadPrincipalPolicy, recommendPolicy, renderDashboardHTML, resolveTier, respondToHandshake, signPayload, startDashboard, startDashboardServer, tierDistribution, verifyAttestation, verifyBridgeCommitment, verifyCompletion, verifyPedersenCommitment, verifyProofOfKnowledge, verifyRangeProof, verifySHR, verifySignature };
|
|
27059
|
+
export { ATTESTATION_VERSION, ApprovalGate, AuditLog, AutoApproveChannel, BaselineTracker, TEMPLATES as CONTEXT_GATE_TEMPLATES, CallbackApprovalChannel, ClientManager, CommitmentStore, ContextGateEnforcer, ContextGatePolicyStore, DashboardApprovalChannel, FederationRegistry, FilesystemStorage, HERO_COPY, InMemoryModelProvenanceStore, InjectionDetector, MODEL_PRESETS, MemoryStorage, PolicyStore, ProxyRouter, ReputationStore, SovereigntyProfileStore, StateStore, StderrApprovalChannel, TIER_WEIGHTS, WebhookApprovalChannel, canonicalize, classifyField, completeHandshake, computeWeightedScore, createBridgeCommitment, createDefaultProfile, createPedersenCommitment, createProofOfKnowledge, createRangeProof, createSanctuaryServer, evaluateField, exitBundleManifestShape, exportExitBundle, filterContext, generateAttestation, generateSHR, generateSystemPrompt, getProtectionSnapshot, getTemplate, importExitBundle, initiateHandshake, listTemplateIds, loadConfig, loadExitArtifact, loadPrincipalPolicy, readManifest, recommendPolicy, renderDashboardHTML, resolveTier, respondToHandshake, runExitCommand, signPayload, startDashboard, startDashboardServer, tierDistribution, verifyAttestation, verifyBridgeCommitment, verifyCompletion, verifyExitBundle, verifyPedersenCommitment, verifyProofOfKnowledge, verifyRangeProof, verifySHR, verifySignature };
|
|
24437
27060
|
//# sourceMappingURL=index.js.map
|
|
24438
27061
|
//# sourceMappingURL=index.js.map
|