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