@sanctuary-framework/mcp-server 0.3.0 → 0.4.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 +3110 -131
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +3112 -134
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +3033 -118
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +320 -16
- package/dist/index.d.ts +320 -16
- package/dist/index.js +3027 -121
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -7,6 +7,7 @@ var stdio_js = require('@modelcontextprotocol/sdk/server/stdio.js');
|
|
|
7
7
|
var promises = require('fs/promises');
|
|
8
8
|
var path = require('path');
|
|
9
9
|
var os = require('os');
|
|
10
|
+
var module$1 = require('module');
|
|
10
11
|
var crypto = require('crypto');
|
|
11
12
|
var aes_js = require('@noble/ciphers/aes.js');
|
|
12
13
|
var ed25519 = require('@noble/curves/ed25519');
|
|
@@ -17,7 +18,9 @@ var types_js = require('@modelcontextprotocol/sdk/types.js');
|
|
|
17
18
|
var http = require('http');
|
|
18
19
|
var https = require('https');
|
|
19
20
|
var fs = require('fs');
|
|
21
|
+
var child_process = require('child_process');
|
|
20
22
|
|
|
23
|
+
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
21
24
|
var __defProp = Object.defineProperty;
|
|
22
25
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
23
26
|
var __esm = (fn, res) => function __init() {
|
|
@@ -206,9 +209,11 @@ var init_hashing = __esm({
|
|
|
206
209
|
init_encoding();
|
|
207
210
|
}
|
|
208
211
|
});
|
|
212
|
+
var require2 = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href)));
|
|
213
|
+
var { version: PKG_VERSION } = require2("../package.json");
|
|
209
214
|
function defaultConfig() {
|
|
210
215
|
return {
|
|
211
|
-
version:
|
|
216
|
+
version: PKG_VERSION,
|
|
212
217
|
storage_path: path.join(os.homedir(), ".sanctuary"),
|
|
213
218
|
state: {
|
|
214
219
|
encryption: "aes-256-gcm",
|
|
@@ -300,8 +305,13 @@ async function loadConfig(configPath) {
|
|
|
300
305
|
try {
|
|
301
306
|
const raw = await promises.readFile(path$1, "utf-8");
|
|
302
307
|
const fileConfig = JSON.parse(raw);
|
|
303
|
-
|
|
304
|
-
|
|
308
|
+
const merged = deepMerge(config, fileConfig);
|
|
309
|
+
validateConfig(merged);
|
|
310
|
+
return merged;
|
|
311
|
+
} catch (err) {
|
|
312
|
+
if (err instanceof Error && err.message.includes("unimplemented features")) {
|
|
313
|
+
throw err;
|
|
314
|
+
}
|
|
305
315
|
return config;
|
|
306
316
|
}
|
|
307
317
|
}
|
|
@@ -309,6 +319,45 @@ async function saveConfig(config, configPath) {
|
|
|
309
319
|
const path$1 = path.join(config.storage_path, "sanctuary.json");
|
|
310
320
|
await promises.writeFile(path$1, JSON.stringify(config, null, 2), { mode: 384 });
|
|
311
321
|
}
|
|
322
|
+
function validateConfig(config) {
|
|
323
|
+
const errors = [];
|
|
324
|
+
const implementedKeyProtection = /* @__PURE__ */ new Set(["passphrase", "none"]);
|
|
325
|
+
if (!implementedKeyProtection.has(config.state.key_protection)) {
|
|
326
|
+
errors.push(
|
|
327
|
+
`Unimplemented config value: state.key_protection = "${config.state.key_protection}". Only ${[...implementedKeyProtection].map((v) => `"${v}"`).join(", ")} are currently implemented. Using an unimplemented key protection mode would silently degrade security.`
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
const implementedEnvironment = /* @__PURE__ */ new Set(["local-process", "docker"]);
|
|
331
|
+
if (!implementedEnvironment.has(config.execution.environment)) {
|
|
332
|
+
errors.push(
|
|
333
|
+
`Unimplemented config value: execution.environment = "${config.execution.environment}". Only ${[...implementedEnvironment].map((v) => `"${v}"`).join(", ")} are currently implemented. Using an unimplemented environment would silently degrade security.`
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
const implementedProofSystem = /* @__PURE__ */ new Set(["commitment-only"]);
|
|
337
|
+
if (!implementedProofSystem.has(config.disclosure.proof_system)) {
|
|
338
|
+
errors.push(
|
|
339
|
+
`Unimplemented config value: disclosure.proof_system = "${config.disclosure.proof_system}". Only ${[...implementedProofSystem].map((v) => `"${v}"`).join(", ")} is currently implemented. Using an unimplemented proof system would silently degrade security.`
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
const implementedDisclosurePolicy = /* @__PURE__ */ new Set(["minimum-necessary"]);
|
|
343
|
+
if (!implementedDisclosurePolicy.has(config.disclosure.default_policy)) {
|
|
344
|
+
errors.push(
|
|
345
|
+
`Unimplemented config value: disclosure.default_policy = "${config.disclosure.default_policy}". Only ${[...implementedDisclosurePolicy].map((v) => `"${v}"`).join(", ")} is currently implemented. Using an unimplemented disclosure policy would silently skip disclosure controls.`
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
const implementedReputationMode = /* @__PURE__ */ new Set(["self-custodied"]);
|
|
349
|
+
if (!implementedReputationMode.has(config.reputation.mode)) {
|
|
350
|
+
errors.push(
|
|
351
|
+
`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.`
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
if (errors.length > 0) {
|
|
355
|
+
throw new Error(
|
|
356
|
+
`Sanctuary configuration references unimplemented features:
|
|
357
|
+
${errors.join("\n")}`
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
312
361
|
function deepMerge(base, override) {
|
|
313
362
|
const result = { ...base };
|
|
314
363
|
for (const [key, value] of Object.entries(override)) {
|
|
@@ -652,7 +701,11 @@ var RESERVED_NAMESPACE_PREFIXES = [
|
|
|
652
701
|
"_commitments",
|
|
653
702
|
"_reputation",
|
|
654
703
|
"_escrow",
|
|
655
|
-
"_guarantees"
|
|
704
|
+
"_guarantees",
|
|
705
|
+
"_bridge",
|
|
706
|
+
"_federation",
|
|
707
|
+
"_handshake",
|
|
708
|
+
"_shr"
|
|
656
709
|
];
|
|
657
710
|
var StateStore = class {
|
|
658
711
|
storage;
|
|
@@ -919,12 +972,14 @@ var StateStore = class {
|
|
|
919
972
|
/**
|
|
920
973
|
* Import a previously exported state bundle.
|
|
921
974
|
*/
|
|
922
|
-
async import(bundleBase64, conflictResolution = "skip") {
|
|
975
|
+
async import(bundleBase64, conflictResolution = "skip", publicKeyResolver) {
|
|
923
976
|
const bundleBytes = fromBase64url(bundleBase64);
|
|
924
977
|
const bundleJson = bytesToString(bundleBytes);
|
|
925
978
|
const bundle = JSON.parse(bundleJson);
|
|
926
979
|
let importedKeys = 0;
|
|
927
980
|
let skippedKeys = 0;
|
|
981
|
+
let skippedInvalidSig = 0;
|
|
982
|
+
let skippedUnknownKid = 0;
|
|
928
983
|
let conflicts = 0;
|
|
929
984
|
const namespaces = [];
|
|
930
985
|
for (const [ns, entries] of Object.entries(
|
|
@@ -938,6 +993,26 @@ var StateStore = class {
|
|
|
938
993
|
}
|
|
939
994
|
namespaces.push(ns);
|
|
940
995
|
for (const { key, entry } of entries) {
|
|
996
|
+
const signerPublicKey = publicKeyResolver(entry.kid);
|
|
997
|
+
if (!signerPublicKey) {
|
|
998
|
+
skippedUnknownKid++;
|
|
999
|
+
skippedKeys++;
|
|
1000
|
+
continue;
|
|
1001
|
+
}
|
|
1002
|
+
try {
|
|
1003
|
+
const ciphertextBytes = fromBase64url(entry.payload.ct);
|
|
1004
|
+
const signatureBytes = fromBase64url(entry.sig);
|
|
1005
|
+
const sigValid = verify(ciphertextBytes, signatureBytes, signerPublicKey);
|
|
1006
|
+
if (!sigValid) {
|
|
1007
|
+
skippedInvalidSig++;
|
|
1008
|
+
skippedKeys++;
|
|
1009
|
+
continue;
|
|
1010
|
+
}
|
|
1011
|
+
} catch {
|
|
1012
|
+
skippedInvalidSig++;
|
|
1013
|
+
skippedKeys++;
|
|
1014
|
+
continue;
|
|
1015
|
+
}
|
|
941
1016
|
const exists = await this.storage.exists(ns, key);
|
|
942
1017
|
if (exists) {
|
|
943
1018
|
conflicts++;
|
|
@@ -973,12 +1048,16 @@ var StateStore = class {
|
|
|
973
1048
|
return {
|
|
974
1049
|
imported_keys: importedKeys,
|
|
975
1050
|
skipped_keys: skippedKeys,
|
|
1051
|
+
skipped_invalid_sig: skippedInvalidSig,
|
|
1052
|
+
skipped_unknown_kid: skippedUnknownKid,
|
|
976
1053
|
conflicts,
|
|
977
1054
|
namespaces,
|
|
978
1055
|
imported_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
979
1056
|
};
|
|
980
1057
|
}
|
|
981
1058
|
};
|
|
1059
|
+
var require3 = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href)));
|
|
1060
|
+
var { version: PKG_VERSION2 } = require3("../package.json");
|
|
982
1061
|
var MAX_STRING_BYTES = 1048576;
|
|
983
1062
|
var MAX_BUNDLE_BYTES = 5242880;
|
|
984
1063
|
var BUNDLE_FIELDS = /* @__PURE__ */ new Set(["bundle"]);
|
|
@@ -1061,7 +1140,7 @@ function createServer(tools, options) {
|
|
|
1061
1140
|
const server = new index_js.Server(
|
|
1062
1141
|
{
|
|
1063
1142
|
name: "sanctuary-mcp-server",
|
|
1064
|
-
version:
|
|
1143
|
+
version: PKG_VERSION2
|
|
1065
1144
|
},
|
|
1066
1145
|
{
|
|
1067
1146
|
capabilities: {
|
|
@@ -1161,7 +1240,11 @@ var RESERVED_NAMESPACE_PREFIXES2 = [
|
|
|
1161
1240
|
"_commitments",
|
|
1162
1241
|
"_reputation",
|
|
1163
1242
|
"_escrow",
|
|
1164
|
-
"_guarantees"
|
|
1243
|
+
"_guarantees",
|
|
1244
|
+
"_bridge",
|
|
1245
|
+
"_federation",
|
|
1246
|
+
"_handshake",
|
|
1247
|
+
"_shr"
|
|
1165
1248
|
];
|
|
1166
1249
|
function getReservedNamespaceViolation(namespace) {
|
|
1167
1250
|
for (const prefix of RESERVED_NAMESPACE_PREFIXES2) {
|
|
@@ -1498,6 +1581,13 @@ function createL1Tools(stateStore, storage, masterKey, keyProtection, auditLog)
|
|
|
1498
1581
|
required: ["namespace", "key"]
|
|
1499
1582
|
},
|
|
1500
1583
|
handler: async (args) => {
|
|
1584
|
+
const reservedViolation = getReservedNamespaceViolation(args.namespace);
|
|
1585
|
+
if (reservedViolation) {
|
|
1586
|
+
return toolResult({
|
|
1587
|
+
error: "namespace_reserved",
|
|
1588
|
+
message: `Namespace "${args.namespace}" is reserved for internal use (prefix: ${reservedViolation}). Cannot read from reserved namespaces.`
|
|
1589
|
+
});
|
|
1590
|
+
}
|
|
1501
1591
|
const result = await stateStore.read(
|
|
1502
1592
|
args.namespace,
|
|
1503
1593
|
args.key,
|
|
@@ -1534,6 +1624,13 @@ function createL1Tools(stateStore, storage, masterKey, keyProtection, auditLog)
|
|
|
1534
1624
|
required: ["namespace"]
|
|
1535
1625
|
},
|
|
1536
1626
|
handler: async (args) => {
|
|
1627
|
+
const reservedViolation = getReservedNamespaceViolation(args.namespace);
|
|
1628
|
+
if (reservedViolation) {
|
|
1629
|
+
return toolResult({
|
|
1630
|
+
error: "namespace_reserved",
|
|
1631
|
+
message: `Namespace "${args.namespace}" is reserved for internal use (prefix: ${reservedViolation}). Cannot list reserved namespaces.`
|
|
1632
|
+
});
|
|
1633
|
+
}
|
|
1537
1634
|
const result = await stateStore.list(
|
|
1538
1635
|
args.namespace,
|
|
1539
1636
|
args.prefix,
|
|
@@ -1612,9 +1709,15 @@ function createL1Tools(stateStore, storage, masterKey, keyProtection, auditLog)
|
|
|
1612
1709
|
required: ["bundle"]
|
|
1613
1710
|
},
|
|
1614
1711
|
handler: async (args) => {
|
|
1712
|
+
const publicKeyResolver = (kid) => {
|
|
1713
|
+
const identity = identityMgr.get(kid);
|
|
1714
|
+
if (!identity) return null;
|
|
1715
|
+
return fromBase64url(identity.public_key);
|
|
1716
|
+
};
|
|
1615
1717
|
const result = await stateStore.import(
|
|
1616
1718
|
args.bundle,
|
|
1617
|
-
args.conflict_resolution ?? "skip"
|
|
1719
|
+
args.conflict_resolution ?? "skip",
|
|
1720
|
+
publicKeyResolver
|
|
1618
1721
|
);
|
|
1619
1722
|
auditLog?.append("l1", "state_import", "principal", {
|
|
1620
1723
|
imported_keys: result.imported_keys
|
|
@@ -2059,7 +2162,7 @@ function createRangeProof(value, blindingFactor, commitment, min, max) {
|
|
|
2059
2162
|
bitProofs.push(bitProof);
|
|
2060
2163
|
}
|
|
2061
2164
|
const sumBlinding = bitBlindings.reduce(
|
|
2062
|
-
(acc, bi, i) => mod(acc + mod(BigInt(1 << i)) * bi),
|
|
2165
|
+
(acc, bi, i) => mod(acc + mod(BigInt(1) << BigInt(i)) * bi),
|
|
2063
2166
|
0n
|
|
2064
2167
|
);
|
|
2065
2168
|
const blindingDiff = mod(b - sumBlinding);
|
|
@@ -2101,7 +2204,7 @@ function verifyRangeProof(proof) {
|
|
|
2101
2204
|
let reconstructed = ed25519.RistrettoPoint.ZERO;
|
|
2102
2205
|
for (let i = 0; i < numBits; i++) {
|
|
2103
2206
|
const C_i = ed25519.RistrettoPoint.fromHex(fromBase64url(proof.bit_commitments[i]));
|
|
2104
|
-
const weight = mod(BigInt(1 << i));
|
|
2207
|
+
const weight = mod(BigInt(1) << BigInt(i));
|
|
2105
2208
|
reconstructed = reconstructed.add(safeMultiply(C_i, weight));
|
|
2106
2209
|
}
|
|
2107
2210
|
const diff = C.subtract(safeMultiply(G, mod(BigInt(proof.min)))).subtract(reconstructed);
|
|
@@ -3156,7 +3259,9 @@ function createL4Tools(storage, masterKey, identityManager, auditLog, handshakeR
|
|
|
3156
3259
|
contexts: summary.contexts
|
|
3157
3260
|
});
|
|
3158
3261
|
return toolResult({
|
|
3159
|
-
summary
|
|
3262
|
+
summary,
|
|
3263
|
+
// SEC-ADD-03: Tag response as containing counterparty-generated attestation data
|
|
3264
|
+
_content_trust: "external"
|
|
3160
3265
|
});
|
|
3161
3266
|
}
|
|
3162
3267
|
},
|
|
@@ -3477,24 +3582,27 @@ var DEFAULT_TIER2 = {
|
|
|
3477
3582
|
};
|
|
3478
3583
|
var DEFAULT_CHANNEL = {
|
|
3479
3584
|
type: "stderr",
|
|
3480
|
-
timeout_seconds: 300
|
|
3481
|
-
|
|
3585
|
+
timeout_seconds: 300
|
|
3586
|
+
// SEC-002: auto_deny is not configurable. Timeout always denies.
|
|
3587
|
+
// Field omitted intentionally — all channels hardcode deny on timeout.
|
|
3482
3588
|
};
|
|
3483
3589
|
var DEFAULT_POLICY = {
|
|
3484
3590
|
version: 1,
|
|
3485
3591
|
tier1_always_approve: [
|
|
3486
3592
|
"state_export",
|
|
3487
3593
|
"state_import",
|
|
3594
|
+
"state_delete",
|
|
3488
3595
|
"identity_rotate",
|
|
3489
3596
|
"reputation_import",
|
|
3490
|
-
"
|
|
3597
|
+
"reputation_export",
|
|
3598
|
+
"bootstrap_provide_guarantee",
|
|
3599
|
+
"decommission_certificate"
|
|
3491
3600
|
],
|
|
3492
3601
|
tier2_anomaly: DEFAULT_TIER2,
|
|
3493
3602
|
tier3_always_allow: [
|
|
3494
3603
|
"state_read",
|
|
3495
3604
|
"state_write",
|
|
3496
3605
|
"state_list",
|
|
3497
|
-
"state_delete",
|
|
3498
3606
|
"identity_create",
|
|
3499
3607
|
"identity_list",
|
|
3500
3608
|
"identity_sign",
|
|
@@ -3505,7 +3613,6 @@ var DEFAULT_POLICY = {
|
|
|
3505
3613
|
"disclosure_evaluate",
|
|
3506
3614
|
"reputation_record",
|
|
3507
3615
|
"reputation_query",
|
|
3508
|
-
"reputation_export",
|
|
3509
3616
|
"bootstrap_create_escrow",
|
|
3510
3617
|
"exec_attest",
|
|
3511
3618
|
"monitor_health",
|
|
@@ -3527,7 +3634,14 @@ var DEFAULT_POLICY = {
|
|
|
3527
3634
|
"zk_prove",
|
|
3528
3635
|
"zk_verify",
|
|
3529
3636
|
"zk_range_prove",
|
|
3530
|
-
"zk_range_verify"
|
|
3637
|
+
"zk_range_verify",
|
|
3638
|
+
"context_gate_set_policy",
|
|
3639
|
+
"context_gate_apply_template",
|
|
3640
|
+
"context_gate_recommend",
|
|
3641
|
+
"context_gate_filter",
|
|
3642
|
+
"context_gate_list_policies",
|
|
3643
|
+
"l2_hardening_status",
|
|
3644
|
+
"l2_verify_isolation"
|
|
3531
3645
|
],
|
|
3532
3646
|
approval_channel: DEFAULT_CHANNEL
|
|
3533
3647
|
};
|
|
@@ -3602,10 +3716,14 @@ function validatePolicy(raw) {
|
|
|
3602
3716
|
...raw.tier2_anomaly ?? {}
|
|
3603
3717
|
},
|
|
3604
3718
|
tier3_always_allow: raw.tier3_always_allow ?? DEFAULT_POLICY.tier3_always_allow,
|
|
3605
|
-
approval_channel: {
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3719
|
+
approval_channel: (() => {
|
|
3720
|
+
const merged = {
|
|
3721
|
+
...DEFAULT_CHANNEL,
|
|
3722
|
+
...raw.approval_channel ?? {}
|
|
3723
|
+
};
|
|
3724
|
+
delete merged.auto_deny;
|
|
3725
|
+
return merged;
|
|
3726
|
+
})()
|
|
3609
3727
|
};
|
|
3610
3728
|
}
|
|
3611
3729
|
function generateDefaultPolicyYaml() {
|
|
@@ -3622,8 +3740,10 @@ version: 1
|
|
|
3622
3740
|
tier1_always_approve:
|
|
3623
3741
|
- state_export
|
|
3624
3742
|
- state_import
|
|
3743
|
+
- state_delete
|
|
3625
3744
|
- identity_rotate
|
|
3626
3745
|
- reputation_import
|
|
3746
|
+
- reputation_export
|
|
3627
3747
|
- bootstrap_provide_guarantee
|
|
3628
3748
|
|
|
3629
3749
|
# \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
|
|
@@ -3643,7 +3763,6 @@ tier3_always_allow:
|
|
|
3643
3763
|
- state_read
|
|
3644
3764
|
- state_write
|
|
3645
3765
|
- state_list
|
|
3646
|
-
- state_delete
|
|
3647
3766
|
- identity_create
|
|
3648
3767
|
- identity_list
|
|
3649
3768
|
- identity_sign
|
|
@@ -3654,7 +3773,6 @@ tier3_always_allow:
|
|
|
3654
3773
|
- disclosure_evaluate
|
|
3655
3774
|
- reputation_record
|
|
3656
3775
|
- reputation_query
|
|
3657
|
-
- reputation_export
|
|
3658
3776
|
- bootstrap_create_escrow
|
|
3659
3777
|
- exec_attest
|
|
3660
3778
|
- monitor_health
|
|
@@ -3677,13 +3795,18 @@ tier3_always_allow:
|
|
|
3677
3795
|
- zk_verify
|
|
3678
3796
|
- zk_range_prove
|
|
3679
3797
|
- zk_range_verify
|
|
3798
|
+
- context_gate_set_policy
|
|
3799
|
+
- context_gate_apply_template
|
|
3800
|
+
- context_gate_recommend
|
|
3801
|
+
- context_gate_filter
|
|
3802
|
+
- context_gate_list_policies
|
|
3680
3803
|
|
|
3681
3804
|
# \u2500\u2500\u2500 Approval Channel \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
3682
3805
|
# How Sanctuary reaches you when approval is needed.
|
|
3806
|
+
# NOTE: Timeout always results in denial. This is not configurable (SEC-002).
|
|
3683
3807
|
approval_channel:
|
|
3684
3808
|
type: stderr
|
|
3685
3809
|
timeout_seconds: 300
|
|
3686
|
-
auto_deny: true
|
|
3687
3810
|
`;
|
|
3688
3811
|
}
|
|
3689
3812
|
async function loadPrincipalPolicy(storagePath) {
|
|
@@ -3860,27 +3983,16 @@ var BaselineTracker = class {
|
|
|
3860
3983
|
|
|
3861
3984
|
// src/principal-policy/approval-channel.ts
|
|
3862
3985
|
var StderrApprovalChannel = class {
|
|
3863
|
-
|
|
3864
|
-
constructor(config) {
|
|
3865
|
-
this.config = config;
|
|
3986
|
+
constructor(_config) {
|
|
3866
3987
|
}
|
|
3867
3988
|
async requestApproval(request) {
|
|
3868
3989
|
const prompt = this.formatPrompt(request);
|
|
3869
3990
|
process.stderr.write(prompt + "\n");
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
decided_by: "timeout"
|
|
3876
|
-
};
|
|
3877
|
-
} else {
|
|
3878
|
-
return {
|
|
3879
|
-
decision: "approve",
|
|
3880
|
-
decided_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3881
|
-
decided_by: "auto"
|
|
3882
|
-
};
|
|
3883
|
-
}
|
|
3991
|
+
return {
|
|
3992
|
+
decision: "deny",
|
|
3993
|
+
decided_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3994
|
+
decided_by: "stderr:non-interactive"
|
|
3995
|
+
};
|
|
3884
3996
|
}
|
|
3885
3997
|
formatPrompt(request) {
|
|
3886
3998
|
const tierLabel = request.tier === 1 ? "Tier 1 \u2014 always requires approval" : "Tier 2 \u2014 behavioral anomaly detected";
|
|
@@ -3888,7 +4000,7 @@ var StderrApprovalChannel = class {
|
|
|
3888
4000
|
return [
|
|
3889
4001
|
"",
|
|
3890
4002
|
"\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\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557",
|
|
3891
|
-
"\u2551 SANCTUARY:
|
|
4003
|
+
"\u2551 SANCTUARY: Operation Denied (non-interactive channel) \u2551",
|
|
3892
4004
|
"\u2560\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\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563",
|
|
3893
4005
|
`\u2551 Operation: ${request.operation.padEnd(50)}\u2551`,
|
|
3894
4006
|
`\u2551 ${tierLabel.padEnd(62)}\u2551`,
|
|
@@ -3899,7 +4011,8 @@ var StderrApprovalChannel = class {
|
|
|
3899
4011
|
(line) => `\u2551 ${line.padEnd(60)}\u2551`
|
|
3900
4012
|
),
|
|
3901
4013
|
"\u2551 \u2551",
|
|
3902
|
-
|
|
4014
|
+
"\u2551 Denied: stderr channel cannot accept input (SEC-016) \u2551",
|
|
4015
|
+
"\u2551 Use dashboard or webhook channel for interactive approval. \u2551",
|
|
3903
4016
|
"\u255A\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\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D",
|
|
3904
4017
|
""
|
|
3905
4018
|
].join("\n");
|
|
@@ -4203,20 +4316,38 @@ function generateDashboardHTML(options) {
|
|
|
4203
4316
|
<script>
|
|
4204
4317
|
(function() {
|
|
4205
4318
|
const TIMEOUT = ${options.timeoutSeconds};
|
|
4206
|
-
|
|
4319
|
+
// SEC-012: Auth token is passed via Authorization header only \u2014 never in URLs.
|
|
4320
|
+
// The token is provided by the server at generation time (embedded for initial auth).
|
|
4321
|
+
const AUTH_TOKEN = ${options.authToken ? JSON.stringify(options.authToken) : "null"};
|
|
4322
|
+
let SESSION_ID = null; // Short-lived session for SSE and URL-based requests
|
|
4207
4323
|
const pending = new Map();
|
|
4208
4324
|
let auditCount = 0;
|
|
4209
4325
|
|
|
4210
|
-
// Auth helpers
|
|
4326
|
+
// Auth helpers \u2014 SEC-012: token goes in header, session goes in URL
|
|
4211
4327
|
function authHeaders() {
|
|
4212
4328
|
const h = { 'Content-Type': 'application/json' };
|
|
4213
4329
|
if (AUTH_TOKEN) h['Authorization'] = 'Bearer ' + AUTH_TOKEN;
|
|
4214
4330
|
return h;
|
|
4215
4331
|
}
|
|
4216
|
-
function
|
|
4217
|
-
if (!
|
|
4332
|
+
function sessionQuery(url) {
|
|
4333
|
+
if (!SESSION_ID) return url;
|
|
4218
4334
|
const sep = url.includes('?') ? '&' : '?';
|
|
4219
|
-
return url + sep + '
|
|
4335
|
+
return url + sep + 'session=' + SESSION_ID;
|
|
4336
|
+
}
|
|
4337
|
+
|
|
4338
|
+
// SEC-012: Exchange the long-lived token for a short-lived session
|
|
4339
|
+
async function exchangeSession() {
|
|
4340
|
+
if (!AUTH_TOKEN) return;
|
|
4341
|
+
try {
|
|
4342
|
+
const resp = await fetch('/auth/session', { method: 'POST', headers: authHeaders() });
|
|
4343
|
+
if (resp.ok) {
|
|
4344
|
+
const data = await resp.json();
|
|
4345
|
+
SESSION_ID = data.session_id;
|
|
4346
|
+
// Refresh session before expiry (at 80% of TTL)
|
|
4347
|
+
const refreshMs = (data.expires_in_seconds || 300) * 800;
|
|
4348
|
+
setTimeout(async () => { await exchangeSession(); reconnectSSE(); }, refreshMs);
|
|
4349
|
+
}
|
|
4350
|
+
} catch(e) { /* will retry on next connect */ }
|
|
4220
4351
|
}
|
|
4221
4352
|
|
|
4222
4353
|
// Tab switching
|
|
@@ -4229,10 +4360,14 @@ function generateDashboardHTML(options) {
|
|
|
4229
4360
|
});
|
|
4230
4361
|
});
|
|
4231
4362
|
|
|
4232
|
-
// SSE Connection
|
|
4363
|
+
// SSE Connection \u2014 SEC-012: uses short-lived session token in URL, not auth token
|
|
4233
4364
|
let evtSource;
|
|
4365
|
+
function reconnectSSE() {
|
|
4366
|
+
if (evtSource) { evtSource.close(); }
|
|
4367
|
+
connect();
|
|
4368
|
+
}
|
|
4234
4369
|
function connect() {
|
|
4235
|
-
evtSource = new EventSource(
|
|
4370
|
+
evtSource = new EventSource(sessionQuery('/events'));
|
|
4236
4371
|
evtSource.onopen = () => {
|
|
4237
4372
|
document.getElementById('statusDot').classList.remove('disconnected');
|
|
4238
4373
|
document.getElementById('statusText').textContent = 'Connected';
|
|
@@ -4420,12 +4555,20 @@ function generateDashboardHTML(options) {
|
|
|
4420
4555
|
return d.innerHTML;
|
|
4421
4556
|
}
|
|
4422
4557
|
|
|
4423
|
-
// Init
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
if (
|
|
4427
|
-
if (
|
|
4428
|
-
|
|
4558
|
+
// Init \u2014 SEC-012: exchange token for session before connecting SSE
|
|
4559
|
+
(async function init() {
|
|
4560
|
+
await exchangeSession();
|
|
4561
|
+
// Clean token from URL if present (legacy bookmarks)
|
|
4562
|
+
if (window.location.search.includes('token=')) {
|
|
4563
|
+
const clean = window.location.pathname;
|
|
4564
|
+
window.history.replaceState({}, '', clean);
|
|
4565
|
+
}
|
|
4566
|
+
connect();
|
|
4567
|
+
fetch('/api/status', { headers: authHeaders() }).then(r => r.json()).then(data => {
|
|
4568
|
+
if (data.baseline) updateBaseline(data.baseline);
|
|
4569
|
+
if (data.policy) updatePolicy(data.policy);
|
|
4570
|
+
}).catch(() => {});
|
|
4571
|
+
})();
|
|
4429
4572
|
})();
|
|
4430
4573
|
</script>
|
|
4431
4574
|
</body>
|
|
@@ -4433,6 +4576,14 @@ function generateDashboardHTML(options) {
|
|
|
4433
4576
|
}
|
|
4434
4577
|
|
|
4435
4578
|
// src/principal-policy/dashboard.ts
|
|
4579
|
+
var require4 = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href)));
|
|
4580
|
+
var { version: PKG_VERSION3 } = require4("../../package.json");
|
|
4581
|
+
var SESSION_TTL_MS = 5 * 60 * 1e3;
|
|
4582
|
+
var MAX_SESSIONS = 1e3;
|
|
4583
|
+
var RATE_LIMIT_WINDOW_MS = 6e4;
|
|
4584
|
+
var RATE_LIMIT_GENERAL = 120;
|
|
4585
|
+
var RATE_LIMIT_DECISIONS = 20;
|
|
4586
|
+
var MAX_RATE_LIMIT_ENTRIES = 1e4;
|
|
4436
4587
|
var DashboardApprovalChannel = class {
|
|
4437
4588
|
config;
|
|
4438
4589
|
pending = /* @__PURE__ */ new Map();
|
|
@@ -4444,15 +4595,21 @@ var DashboardApprovalChannel = class {
|
|
|
4444
4595
|
dashboardHTML;
|
|
4445
4596
|
authToken;
|
|
4446
4597
|
useTLS;
|
|
4598
|
+
/** SEC-012: Short-lived session store. Sessions replace URL query tokens. */
|
|
4599
|
+
sessions = /* @__PURE__ */ new Map();
|
|
4600
|
+
sessionCleanupTimer = null;
|
|
4601
|
+
/** Rate limiting: per-IP request tracking */
|
|
4602
|
+
rateLimits = /* @__PURE__ */ new Map();
|
|
4447
4603
|
constructor(config) {
|
|
4448
4604
|
this.config = config;
|
|
4449
4605
|
this.authToken = config.auth_token;
|
|
4450
4606
|
this.useTLS = !!(config.tls?.cert_path && config.tls?.key_path);
|
|
4451
4607
|
this.dashboardHTML = generateDashboardHTML({
|
|
4452
4608
|
timeoutSeconds: config.timeout_seconds,
|
|
4453
|
-
serverVersion:
|
|
4609
|
+
serverVersion: PKG_VERSION3,
|
|
4454
4610
|
authToken: this.authToken
|
|
4455
4611
|
});
|
|
4612
|
+
this.sessionCleanupTimer = setInterval(() => this.cleanupSessions(), 6e4);
|
|
4456
4613
|
}
|
|
4457
4614
|
/**
|
|
4458
4615
|
* Inject dependencies after construction.
|
|
@@ -4482,13 +4639,14 @@ var DashboardApprovalChannel = class {
|
|
|
4482
4639
|
const baseUrl = `${protocol}://${this.config.host}:${this.config.port}`;
|
|
4483
4640
|
this.httpServer.listen(this.config.port, this.config.host, () => {
|
|
4484
4641
|
if (this.authToken) {
|
|
4642
|
+
const hint = this.authToken.slice(0, 4) + "..." + this.authToken.slice(-4);
|
|
4485
4643
|
process.stderr.write(
|
|
4486
4644
|
`
|
|
4487
|
-
Sanctuary Principal Dashboard: ${baseUrl}
|
|
4645
|
+
Sanctuary Principal Dashboard: ${baseUrl}
|
|
4488
4646
|
`
|
|
4489
4647
|
);
|
|
4490
4648
|
process.stderr.write(
|
|
4491
|
-
` Auth token: ${
|
|
4649
|
+
` Auth required (token: ${hint}). Use Authorization: Bearer <TOKEN> header.
|
|
4492
4650
|
|
|
4493
4651
|
`
|
|
4494
4652
|
);
|
|
@@ -4522,6 +4680,12 @@ var DashboardApprovalChannel = class {
|
|
|
4522
4680
|
client.end();
|
|
4523
4681
|
}
|
|
4524
4682
|
this.sseClients.clear();
|
|
4683
|
+
this.sessions.clear();
|
|
4684
|
+
if (this.sessionCleanupTimer) {
|
|
4685
|
+
clearInterval(this.sessionCleanupTimer);
|
|
4686
|
+
this.sessionCleanupTimer = null;
|
|
4687
|
+
}
|
|
4688
|
+
this.rateLimits.clear();
|
|
4525
4689
|
if (this.httpServer) {
|
|
4526
4690
|
return new Promise((resolve) => {
|
|
4527
4691
|
this.httpServer.close(() => resolve());
|
|
@@ -4542,7 +4706,8 @@ var DashboardApprovalChannel = class {
|
|
|
4542
4706
|
const timer = setTimeout(() => {
|
|
4543
4707
|
this.pending.delete(id);
|
|
4544
4708
|
const response = {
|
|
4545
|
-
|
|
4709
|
+
// SEC-002: Timeout ALWAYS denies. No configuration can change this.
|
|
4710
|
+
decision: "deny",
|
|
4546
4711
|
decided_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4547
4712
|
decided_by: "timeout"
|
|
4548
4713
|
};
|
|
@@ -4574,7 +4739,12 @@ var DashboardApprovalChannel = class {
|
|
|
4574
4739
|
// ── Authentication ──────────────────────────────────────────────────
|
|
4575
4740
|
/**
|
|
4576
4741
|
* Verify bearer token authentication.
|
|
4577
|
-
*
|
|
4742
|
+
*
|
|
4743
|
+
* SEC-012: The long-lived auth token is ONLY accepted via the Authorization
|
|
4744
|
+
* header — never in URL query strings. For SSE and page loads that cannot
|
|
4745
|
+
* set headers, a short-lived session token (obtained via POST /auth/session)
|
|
4746
|
+
* is accepted via ?session= query parameter.
|
|
4747
|
+
*
|
|
4578
4748
|
* Returns true if auth passes, false if blocked (response already sent).
|
|
4579
4749
|
*/
|
|
4580
4750
|
checkAuth(req, url, res) {
|
|
@@ -4586,19 +4756,126 @@ var DashboardApprovalChannel = class {
|
|
|
4586
4756
|
return true;
|
|
4587
4757
|
}
|
|
4588
4758
|
}
|
|
4589
|
-
const
|
|
4590
|
-
if (
|
|
4759
|
+
const sessionId = url.searchParams.get("session");
|
|
4760
|
+
if (sessionId && this.validateSession(sessionId)) {
|
|
4591
4761
|
return true;
|
|
4592
4762
|
}
|
|
4593
4763
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
4594
|
-
res.end(JSON.stringify({ error: "Unauthorized \u2014
|
|
4764
|
+
res.end(JSON.stringify({ error: "Unauthorized \u2014 use Authorization: Bearer header or a valid session" }));
|
|
4595
4765
|
return false;
|
|
4596
4766
|
}
|
|
4767
|
+
// ── Session Management (SEC-012) ──────────────────────────────────
|
|
4768
|
+
/**
|
|
4769
|
+
* Create a short-lived session by exchanging the long-lived auth token
|
|
4770
|
+
* (provided in the Authorization header) for a session ID.
|
|
4771
|
+
*/
|
|
4772
|
+
createSession() {
|
|
4773
|
+
if (this.sessions.size >= MAX_SESSIONS) {
|
|
4774
|
+
this.cleanupSessions();
|
|
4775
|
+
if (this.sessions.size >= MAX_SESSIONS) {
|
|
4776
|
+
const oldest = [...this.sessions.entries()].sort(
|
|
4777
|
+
(a, b) => a[1].created_at - b[1].created_at
|
|
4778
|
+
)[0];
|
|
4779
|
+
if (oldest) this.sessions.delete(oldest[0]);
|
|
4780
|
+
}
|
|
4781
|
+
}
|
|
4782
|
+
const id = crypto.randomBytes(32).toString("hex");
|
|
4783
|
+
const now = Date.now();
|
|
4784
|
+
this.sessions.set(id, {
|
|
4785
|
+
id,
|
|
4786
|
+
created_at: now,
|
|
4787
|
+
expires_at: now + SESSION_TTL_MS
|
|
4788
|
+
});
|
|
4789
|
+
return id;
|
|
4790
|
+
}
|
|
4791
|
+
/**
|
|
4792
|
+
* Validate a session ID — must exist and not be expired.
|
|
4793
|
+
*/
|
|
4794
|
+
validateSession(sessionId) {
|
|
4795
|
+
const session = this.sessions.get(sessionId);
|
|
4796
|
+
if (!session) return false;
|
|
4797
|
+
if (Date.now() > session.expires_at) {
|
|
4798
|
+
this.sessions.delete(sessionId);
|
|
4799
|
+
return false;
|
|
4800
|
+
}
|
|
4801
|
+
return true;
|
|
4802
|
+
}
|
|
4803
|
+
/**
|
|
4804
|
+
* Remove all expired sessions.
|
|
4805
|
+
*/
|
|
4806
|
+
cleanupSessions() {
|
|
4807
|
+
const now = Date.now();
|
|
4808
|
+
for (const [id, session] of this.sessions) {
|
|
4809
|
+
if (now > session.expires_at) {
|
|
4810
|
+
this.sessions.delete(id);
|
|
4811
|
+
}
|
|
4812
|
+
}
|
|
4813
|
+
}
|
|
4814
|
+
// ── Rate Limiting ─────────────────────────────────────────────────
|
|
4815
|
+
/**
|
|
4816
|
+
* Get the remote address from a request, normalizing IPv6-mapped IPv4.
|
|
4817
|
+
*/
|
|
4818
|
+
getRemoteAddr(req) {
|
|
4819
|
+
const addr = req.socket.remoteAddress ?? "unknown";
|
|
4820
|
+
return addr.startsWith("::ffff:") ? addr.slice(7) : addr;
|
|
4821
|
+
}
|
|
4822
|
+
/**
|
|
4823
|
+
* Check rate limit for a request. Returns true if allowed, false if rate-limited.
|
|
4824
|
+
* When rate-limited, sends a 429 response.
|
|
4825
|
+
*/
|
|
4826
|
+
checkRateLimit(req, res, type) {
|
|
4827
|
+
const addr = this.getRemoteAddr(req);
|
|
4828
|
+
const now = Date.now();
|
|
4829
|
+
const windowStart = now - RATE_LIMIT_WINDOW_MS;
|
|
4830
|
+
let entry = this.rateLimits.get(addr);
|
|
4831
|
+
if (!entry) {
|
|
4832
|
+
if (this.rateLimits.size >= MAX_RATE_LIMIT_ENTRIES) {
|
|
4833
|
+
this.pruneRateLimits(now);
|
|
4834
|
+
}
|
|
4835
|
+
entry = { general: [], decisions: [] };
|
|
4836
|
+
this.rateLimits.set(addr, entry);
|
|
4837
|
+
}
|
|
4838
|
+
entry.general = entry.general.filter((t) => t > windowStart);
|
|
4839
|
+
entry.decisions = entry.decisions.filter((t) => t > windowStart);
|
|
4840
|
+
const limit = type === "decisions" ? RATE_LIMIT_DECISIONS : RATE_LIMIT_GENERAL;
|
|
4841
|
+
const timestamps = entry[type];
|
|
4842
|
+
if (timestamps.length >= limit) {
|
|
4843
|
+
const retryAfter = Math.ceil((timestamps[0] + RATE_LIMIT_WINDOW_MS - now) / 1e3);
|
|
4844
|
+
res.writeHead(429, {
|
|
4845
|
+
"Content-Type": "application/json",
|
|
4846
|
+
"Retry-After": String(Math.max(1, retryAfter))
|
|
4847
|
+
});
|
|
4848
|
+
res.end(JSON.stringify({
|
|
4849
|
+
error: "Rate limit exceeded",
|
|
4850
|
+
retry_after_seconds: Math.max(1, retryAfter)
|
|
4851
|
+
}));
|
|
4852
|
+
return false;
|
|
4853
|
+
}
|
|
4854
|
+
timestamps.push(now);
|
|
4855
|
+
return true;
|
|
4856
|
+
}
|
|
4857
|
+
/**
|
|
4858
|
+
* Remove stale entries from the rate limit map.
|
|
4859
|
+
*/
|
|
4860
|
+
pruneRateLimits(now) {
|
|
4861
|
+
const windowStart = now - RATE_LIMIT_WINDOW_MS;
|
|
4862
|
+
for (const [addr, entry] of this.rateLimits) {
|
|
4863
|
+
const hasRecent = entry.general.some((t) => t > windowStart) || entry.decisions.some((t) => t > windowStart);
|
|
4864
|
+
if (!hasRecent) {
|
|
4865
|
+
this.rateLimits.delete(addr);
|
|
4866
|
+
}
|
|
4867
|
+
}
|
|
4868
|
+
}
|
|
4597
4869
|
// ── HTTP Request Handler ────────────────────────────────────────────
|
|
4598
4870
|
handleRequest(req, res) {
|
|
4599
4871
|
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
4600
4872
|
const method = req.method ?? "GET";
|
|
4601
|
-
|
|
4873
|
+
const origin = req.headers.origin;
|
|
4874
|
+
const protocol = this.useTLS ? "https" : "http";
|
|
4875
|
+
const selfOrigin = `${protocol}://${this.config.host}:${this.config.port}`;
|
|
4876
|
+
if (origin === selfOrigin) {
|
|
4877
|
+
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
4878
|
+
}
|
|
4602
4879
|
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
4603
4880
|
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
4604
4881
|
if (method === "OPTIONS") {
|
|
@@ -4607,7 +4884,12 @@ var DashboardApprovalChannel = class {
|
|
|
4607
4884
|
return;
|
|
4608
4885
|
}
|
|
4609
4886
|
if (!this.checkAuth(req, url, res)) return;
|
|
4887
|
+
if (!this.checkRateLimit(req, res, "general")) return;
|
|
4610
4888
|
try {
|
|
4889
|
+
if (method === "POST" && url.pathname === "/auth/session") {
|
|
4890
|
+
this.handleSessionExchange(req, res);
|
|
4891
|
+
return;
|
|
4892
|
+
}
|
|
4611
4893
|
if (method === "GET" && url.pathname === "/") {
|
|
4612
4894
|
this.serveDashboard(res);
|
|
4613
4895
|
} else if (method === "GET" && url.pathname === "/events") {
|
|
@@ -4619,9 +4901,11 @@ var DashboardApprovalChannel = class {
|
|
|
4619
4901
|
} else if (method === "GET" && url.pathname === "/api/audit-log") {
|
|
4620
4902
|
this.handleAuditLog(url, res);
|
|
4621
4903
|
} else if (method === "POST" && url.pathname.startsWith("/api/approve/")) {
|
|
4904
|
+
if (!this.checkRateLimit(req, res, "decisions")) return;
|
|
4622
4905
|
const id = url.pathname.slice("/api/approve/".length);
|
|
4623
4906
|
this.handleDecision(id, "approve", res);
|
|
4624
4907
|
} else if (method === "POST" && url.pathname.startsWith("/api/deny/")) {
|
|
4908
|
+
if (!this.checkRateLimit(req, res, "decisions")) return;
|
|
4625
4909
|
const id = url.pathname.slice("/api/deny/".length);
|
|
4626
4910
|
this.handleDecision(id, "deny", res);
|
|
4627
4911
|
} else {
|
|
@@ -4634,6 +4918,40 @@ var DashboardApprovalChannel = class {
|
|
|
4634
4918
|
}
|
|
4635
4919
|
}
|
|
4636
4920
|
// ── Route Handlers ──────────────────────────────────────────────────
|
|
4921
|
+
/**
|
|
4922
|
+
* SEC-012: Exchange a long-lived auth token (in Authorization header)
|
|
4923
|
+
* for a short-lived session ID. The session ID can be used in URL
|
|
4924
|
+
* query parameters without exposing the long-lived credential.
|
|
4925
|
+
*
|
|
4926
|
+
* This endpoint performs its OWN auth check (header-only) because it
|
|
4927
|
+
* must reject query-parameter tokens and is called before the
|
|
4928
|
+
* normal checkAuth flow.
|
|
4929
|
+
*/
|
|
4930
|
+
handleSessionExchange(req, res) {
|
|
4931
|
+
if (!this.authToken) {
|
|
4932
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4933
|
+
res.end(JSON.stringify({ session_id: "no-auth" }));
|
|
4934
|
+
return;
|
|
4935
|
+
}
|
|
4936
|
+
const authHeader = req.headers.authorization;
|
|
4937
|
+
if (!authHeader) {
|
|
4938
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
4939
|
+
res.end(JSON.stringify({ error: "Authorization header required" }));
|
|
4940
|
+
return;
|
|
4941
|
+
}
|
|
4942
|
+
const parts = authHeader.split(" ");
|
|
4943
|
+
if (parts.length !== 2 || parts[0] !== "Bearer" || parts[1] !== this.authToken) {
|
|
4944
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
4945
|
+
res.end(JSON.stringify({ error: "Invalid bearer token" }));
|
|
4946
|
+
return;
|
|
4947
|
+
}
|
|
4948
|
+
const sessionId = this.createSession();
|
|
4949
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4950
|
+
res.end(JSON.stringify({
|
|
4951
|
+
session_id: sessionId,
|
|
4952
|
+
expires_in_seconds: SESSION_TTL_MS / 1e3
|
|
4953
|
+
}));
|
|
4954
|
+
}
|
|
4637
4955
|
serveDashboard(res) {
|
|
4638
4956
|
res.writeHead(200, {
|
|
4639
4957
|
"Content-Type": "text/html; charset=utf-8",
|
|
@@ -4659,7 +4977,8 @@ var DashboardApprovalChannel = class {
|
|
|
4659
4977
|
approval_channel: {
|
|
4660
4978
|
type: this.policy.approval_channel.type,
|
|
4661
4979
|
timeout_seconds: this.policy.approval_channel.timeout_seconds,
|
|
4662
|
-
auto_deny:
|
|
4980
|
+
auto_deny: true
|
|
4981
|
+
// SEC-002: hardcoded, not configurable
|
|
4663
4982
|
}
|
|
4664
4983
|
};
|
|
4665
4984
|
}
|
|
@@ -4700,7 +5019,8 @@ data: ${JSON.stringify(initData)}
|
|
|
4700
5019
|
approval_channel: {
|
|
4701
5020
|
type: this.policy.approval_channel.type,
|
|
4702
5021
|
timeout_seconds: this.policy.approval_channel.timeout_seconds,
|
|
4703
|
-
auto_deny:
|
|
5022
|
+
auto_deny: true
|
|
5023
|
+
// SEC-002: hardcoded, not configurable
|
|
4704
5024
|
}
|
|
4705
5025
|
};
|
|
4706
5026
|
}
|
|
@@ -4873,7 +5193,8 @@ var WebhookApprovalChannel = class {
|
|
|
4873
5193
|
const timer = setTimeout(() => {
|
|
4874
5194
|
this.pending.delete(id);
|
|
4875
5195
|
const response = {
|
|
4876
|
-
|
|
5196
|
+
// SEC-002: Timeout ALWAYS denies. No configuration can change this.
|
|
5197
|
+
decision: "deny",
|
|
4877
5198
|
decided_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4878
5199
|
decided_by: "timeout"
|
|
4879
5200
|
};
|
|
@@ -5061,16 +5382,29 @@ var ApprovalGate = class {
|
|
|
5061
5382
|
if (anomaly) {
|
|
5062
5383
|
return this.requestApproval(operation, 2, anomaly.reason, anomaly.context);
|
|
5063
5384
|
}
|
|
5064
|
-
this.
|
|
5065
|
-
|
|
5066
|
-
|
|
5385
|
+
if (this.policy.tier3_always_allow.includes(operation)) {
|
|
5386
|
+
this.auditLog.append("l2", `gate_allow:${operation}`, "system", {
|
|
5387
|
+
tier: 3,
|
|
5388
|
+
operation
|
|
5389
|
+
});
|
|
5390
|
+
return {
|
|
5391
|
+
allowed: true,
|
|
5392
|
+
tier: 3,
|
|
5393
|
+
reason: "Operation allowed (Tier 3)",
|
|
5394
|
+
approval_required: false
|
|
5395
|
+
};
|
|
5396
|
+
}
|
|
5397
|
+
this.auditLog.append("l2", `gate_unclassified:${operation}`, "system", {
|
|
5398
|
+
tier: 1,
|
|
5399
|
+
operation,
|
|
5400
|
+
warning: "Operation is not classified in any policy tier \u2014 defaulting to Tier 1 (require approval)"
|
|
5067
5401
|
});
|
|
5068
|
-
return
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5402
|
+
return this.requestApproval(
|
|
5403
|
+
operation,
|
|
5404
|
+
1,
|
|
5405
|
+
`"${operation}" is not classified in any policy tier \u2014 requires approval (SEC-011 safe default)`,
|
|
5406
|
+
{ operation, unclassified: true }
|
|
5407
|
+
);
|
|
5074
5408
|
}
|
|
5075
5409
|
/**
|
|
5076
5410
|
* Detect Tier 2 behavioral anomalies.
|
|
@@ -5243,7 +5577,8 @@ function createPrincipalPolicyTools(policy, baseline, auditLog) {
|
|
|
5243
5577
|
approval_channel: {
|
|
5244
5578
|
type: policy.approval_channel.type,
|
|
5245
5579
|
timeout_seconds: policy.approval_channel.timeout_seconds,
|
|
5246
|
-
auto_deny:
|
|
5580
|
+
auto_deny: true
|
|
5581
|
+
// SEC-002: hardcoded, not configurable
|
|
5247
5582
|
}
|
|
5248
5583
|
};
|
|
5249
5584
|
if (includeDefaults) {
|
|
@@ -5313,14 +5648,14 @@ function generateSHR(identityId, opts) {
|
|
|
5313
5648
|
code: "PROCESS_ISOLATION_ONLY",
|
|
5314
5649
|
severity: "warning",
|
|
5315
5650
|
description: "Process-level isolation only (no TEE)",
|
|
5316
|
-
mitigation: "TEE support planned for
|
|
5651
|
+
mitigation: "TEE support planned for a future release"
|
|
5317
5652
|
});
|
|
5318
5653
|
degradations.push({
|
|
5319
5654
|
layer: "l2",
|
|
5320
5655
|
code: "SELF_REPORTED_ATTESTATION",
|
|
5321
5656
|
severity: "warning",
|
|
5322
5657
|
description: "Attestation is self-reported (no hardware root of trust)",
|
|
5323
|
-
mitigation: "TEE attestation planned for
|
|
5658
|
+
mitigation: "TEE attestation planned for a future release"
|
|
5324
5659
|
});
|
|
5325
5660
|
}
|
|
5326
5661
|
if (config.disclosure.proof_system === "commitment-only") {
|
|
@@ -5464,6 +5799,245 @@ function assessSovereigntyLevel(body) {
|
|
|
5464
5799
|
return "minimal";
|
|
5465
5800
|
}
|
|
5466
5801
|
|
|
5802
|
+
// src/shr/gateway-adapter.ts
|
|
5803
|
+
var LAYER_WEIGHTS = {
|
|
5804
|
+
l1: 100,
|
|
5805
|
+
l2: 100,
|
|
5806
|
+
l3: 100,
|
|
5807
|
+
l4: 100
|
|
5808
|
+
};
|
|
5809
|
+
var DEGRADATION_IMPACT = {
|
|
5810
|
+
critical: 40,
|
|
5811
|
+
warning: 25,
|
|
5812
|
+
info: 10
|
|
5813
|
+
};
|
|
5814
|
+
function transformSHRForGateway(shr) {
|
|
5815
|
+
const { body, signed_by, signature } = shr;
|
|
5816
|
+
const layerScores = calculateLayerScores(body);
|
|
5817
|
+
const overallScore = calculateOverallScore(layerScores);
|
|
5818
|
+
const trustLevel = determineTrustLevel(overallScore);
|
|
5819
|
+
const signals = extractAuthorizationSignals(body);
|
|
5820
|
+
const degradations = transformDegradations(body.degradations);
|
|
5821
|
+
const constraints = generateAuthorizationConstraints(body);
|
|
5822
|
+
return {
|
|
5823
|
+
shr_version: body.shr_version,
|
|
5824
|
+
agent_identity: signed_by,
|
|
5825
|
+
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5826
|
+
context_expires_at: body.expires_at,
|
|
5827
|
+
overall_score: overallScore,
|
|
5828
|
+
recommended_trust_level: trustLevel,
|
|
5829
|
+
layer_scores: {
|
|
5830
|
+
l1_cognitive: layerScores.l1,
|
|
5831
|
+
l2_operational: layerScores.l2,
|
|
5832
|
+
l3_disclosure: layerScores.l3,
|
|
5833
|
+
l4_reputation: layerScores.l4
|
|
5834
|
+
},
|
|
5835
|
+
layer_status: {
|
|
5836
|
+
l1_cognitive: body.layers.l1.status,
|
|
5837
|
+
l2_operational: body.layers.l2.status,
|
|
5838
|
+
l3_disclosure: body.layers.l3.status,
|
|
5839
|
+
l4_reputation: body.layers.l4.status
|
|
5840
|
+
},
|
|
5841
|
+
authorization_signals: signals,
|
|
5842
|
+
degradations,
|
|
5843
|
+
recommended_constraints: constraints,
|
|
5844
|
+
shr_signature: signature,
|
|
5845
|
+
shr_signed_by: signed_by
|
|
5846
|
+
};
|
|
5847
|
+
}
|
|
5848
|
+
function calculateLayerScores(body) {
|
|
5849
|
+
const layers = body.layers;
|
|
5850
|
+
const degradations = body.degradations;
|
|
5851
|
+
let l1Score = LAYER_WEIGHTS.l1;
|
|
5852
|
+
let l2Score = LAYER_WEIGHTS.l2;
|
|
5853
|
+
let l3Score = LAYER_WEIGHTS.l3;
|
|
5854
|
+
let l4Score = LAYER_WEIGHTS.l4;
|
|
5855
|
+
for (const deg of degradations) {
|
|
5856
|
+
const impact = DEGRADATION_IMPACT[deg.severity] || 10;
|
|
5857
|
+
if (deg.layer === "l1") {
|
|
5858
|
+
l1Score = Math.max(0, l1Score - impact);
|
|
5859
|
+
} else if (deg.layer === "l2") {
|
|
5860
|
+
l2Score = Math.max(0, l2Score - impact);
|
|
5861
|
+
} else if (deg.layer === "l3") {
|
|
5862
|
+
l3Score = Math.max(0, l3Score - impact);
|
|
5863
|
+
} else if (deg.layer === "l4") {
|
|
5864
|
+
l4Score = Math.max(0, l4Score - impact);
|
|
5865
|
+
}
|
|
5866
|
+
}
|
|
5867
|
+
if (layers.l1.status === "active" && l1Score > 50) l1Score = Math.min(100, l1Score + 5);
|
|
5868
|
+
if (layers.l2.status === "active" && l2Score > 50) l2Score = Math.min(100, l2Score + 5);
|
|
5869
|
+
if (layers.l3.status === "active" && l3Score > 50) l3Score = Math.min(100, l3Score + 5);
|
|
5870
|
+
if (layers.l4.status === "active" && l4Score > 50) l4Score = Math.min(100, l4Score + 5);
|
|
5871
|
+
if (layers.l1.status === "inactive") l1Score = 0;
|
|
5872
|
+
if (layers.l2.status === "inactive") l2Score = 0;
|
|
5873
|
+
if (layers.l3.status === "inactive") l3Score = 0;
|
|
5874
|
+
if (layers.l4.status === "inactive") l4Score = 0;
|
|
5875
|
+
return {
|
|
5876
|
+
l1: Math.round(l1Score),
|
|
5877
|
+
l2: Math.round(l2Score),
|
|
5878
|
+
l3: Math.round(l3Score),
|
|
5879
|
+
l4: Math.round(l4Score)
|
|
5880
|
+
};
|
|
5881
|
+
}
|
|
5882
|
+
function calculateOverallScore(layerScores) {
|
|
5883
|
+
const average = (layerScores.l1 + layerScores.l2 + layerScores.l3 + layerScores.l4) / 4;
|
|
5884
|
+
return Math.round(average);
|
|
5885
|
+
}
|
|
5886
|
+
function determineTrustLevel(score) {
|
|
5887
|
+
if (score >= 80) return "full";
|
|
5888
|
+
if (score >= 60) return "elevated";
|
|
5889
|
+
if (score >= 40) return "standard";
|
|
5890
|
+
return "restricted";
|
|
5891
|
+
}
|
|
5892
|
+
function extractAuthorizationSignals(body) {
|
|
5893
|
+
const l1 = body.layers.l1;
|
|
5894
|
+
const l3 = body.layers.l3;
|
|
5895
|
+
const l4 = body.layers.l4;
|
|
5896
|
+
return {
|
|
5897
|
+
approval_gate_active: body.capabilities.handshake,
|
|
5898
|
+
// Handshake implies human loop capability
|
|
5899
|
+
context_gating_active: body.capabilities.encrypted_channel,
|
|
5900
|
+
// Proxy for gating capability
|
|
5901
|
+
encryption_at_rest: l1.encryption !== "none" && l1.encryption !== "unencrypted",
|
|
5902
|
+
behavioral_baseline_active: false,
|
|
5903
|
+
// Would need explicit field in SHR v1.1
|
|
5904
|
+
identity_verified: l1.identity_type === "ed25519" || l1.identity_type !== "none",
|
|
5905
|
+
zero_knowledge_capable: l3.status === "active" && l3.proof_system !== "commitment-only",
|
|
5906
|
+
selective_disclosure_active: l3.selective_disclosure,
|
|
5907
|
+
reputation_portable: l4.reputation_portable,
|
|
5908
|
+
handshake_capable: body.capabilities.handshake
|
|
5909
|
+
};
|
|
5910
|
+
}
|
|
5911
|
+
function transformDegradations(degradations) {
|
|
5912
|
+
return degradations.map((deg) => {
|
|
5913
|
+
let authzImpact = "";
|
|
5914
|
+
if (deg.code === "NO_TEE") {
|
|
5915
|
+
authzImpact = "Restricted to read-only operations until TEE available";
|
|
5916
|
+
} else if (deg.code === "PROCESS_ISOLATION_ONLY") {
|
|
5917
|
+
authzImpact = "Requires additional identity verification";
|
|
5918
|
+
} else if (deg.code === "COMMITMENT_ONLY") {
|
|
5919
|
+
authzImpact = "Limited data sharing scope \u2014 no zero-knowledge proofs";
|
|
5920
|
+
} else if (deg.code === "NO_ZK_PROOFS") {
|
|
5921
|
+
authzImpact = "Cannot perform confidential disclosures";
|
|
5922
|
+
} else if (deg.code === "SELF_REPORTED_ATTESTATION") {
|
|
5923
|
+
authzImpact = "Attestation trust degraded \u2014 human verification recommended";
|
|
5924
|
+
} else if (deg.code === "NO_SELECTIVE_DISCLOSURE") {
|
|
5925
|
+
authzImpact = "Must share entire data context, cannot redact";
|
|
5926
|
+
} else if (deg.code === "BASIC_SYBIL_ONLY") {
|
|
5927
|
+
authzImpact = "Restrict to interactions with known agents only";
|
|
5928
|
+
} else {
|
|
5929
|
+
authzImpact = "Unknown authorization impact";
|
|
5930
|
+
}
|
|
5931
|
+
return {
|
|
5932
|
+
layer: deg.layer,
|
|
5933
|
+
code: deg.code,
|
|
5934
|
+
severity: deg.severity,
|
|
5935
|
+
description: deg.description,
|
|
5936
|
+
authorization_impact: authzImpact
|
|
5937
|
+
};
|
|
5938
|
+
});
|
|
5939
|
+
}
|
|
5940
|
+
function generateAuthorizationConstraints(body, _degradations) {
|
|
5941
|
+
const constraints = [];
|
|
5942
|
+
const layers = body.layers;
|
|
5943
|
+
if (layers.l1.status === "degraded" || layers.l1.key_custody !== "self") {
|
|
5944
|
+
constraints.push({
|
|
5945
|
+
type: "identity_verification_required",
|
|
5946
|
+
description: "Additional identity verification required for sensitive operations",
|
|
5947
|
+
rationale: "L1 is degraded or key custody is not self-managed",
|
|
5948
|
+
priority: "high"
|
|
5949
|
+
});
|
|
5950
|
+
}
|
|
5951
|
+
if (!layers.l1.state_portable) {
|
|
5952
|
+
constraints.push({
|
|
5953
|
+
type: "location_bound",
|
|
5954
|
+
description: "Agent state is not portable \u2014 restrict to home environment",
|
|
5955
|
+
rationale: "State cannot be safely migrated across boundaries",
|
|
5956
|
+
priority: "medium"
|
|
5957
|
+
});
|
|
5958
|
+
}
|
|
5959
|
+
if (layers.l2.status === "degraded" || layers.l2.isolation_type === "local-process") {
|
|
5960
|
+
constraints.push({
|
|
5961
|
+
type: "read_only",
|
|
5962
|
+
description: "Restrict to read-only operations until operational isolation improves",
|
|
5963
|
+
rationale: "L2 isolation is process-level only (no TEE)",
|
|
5964
|
+
priority: "high"
|
|
5965
|
+
});
|
|
5966
|
+
}
|
|
5967
|
+
if (!layers.l2.attestation_available) {
|
|
5968
|
+
constraints.push({
|
|
5969
|
+
type: "requires_approval",
|
|
5970
|
+
description: "Human approval required for writes and sensitive reads",
|
|
5971
|
+
rationale: "No attestation available \u2014 self-reported integrity only",
|
|
5972
|
+
priority: "high"
|
|
5973
|
+
});
|
|
5974
|
+
}
|
|
5975
|
+
if (layers.l3.status === "degraded" || !layers.l3.selective_disclosure) {
|
|
5976
|
+
constraints.push({
|
|
5977
|
+
type: "restricted_scope",
|
|
5978
|
+
description: "Limit data sharing to minimal required scope \u2014 no selective disclosure",
|
|
5979
|
+
rationale: "Agent cannot redact data or prove predicates without revealing all context",
|
|
5980
|
+
priority: "high"
|
|
5981
|
+
});
|
|
5982
|
+
}
|
|
5983
|
+
if (layers.l3.proof_system === "commitment-only") {
|
|
5984
|
+
constraints.push({
|
|
5985
|
+
type: "restricted_scope",
|
|
5986
|
+
description: "No zero-knowledge proofs available \u2014 entire state context may be visible",
|
|
5987
|
+
rationale: "Proof system is commitment-only (no ZK)",
|
|
5988
|
+
priority: "medium"
|
|
5989
|
+
});
|
|
5990
|
+
}
|
|
5991
|
+
if (layers.l4.status === "degraded") {
|
|
5992
|
+
constraints.push({
|
|
5993
|
+
type: "known_agents_only",
|
|
5994
|
+
description: "Restrict interactions to known, pre-approved agents",
|
|
5995
|
+
rationale: "Reputation layer is degraded",
|
|
5996
|
+
priority: "medium"
|
|
5997
|
+
});
|
|
5998
|
+
}
|
|
5999
|
+
if (!layers.l4.reputation_portable) {
|
|
6000
|
+
constraints.push({
|
|
6001
|
+
type: "location_bound",
|
|
6002
|
+
description: "Reputation is not portable \u2014 restrict to home environment",
|
|
6003
|
+
rationale: "Cannot present reputation to external parties",
|
|
6004
|
+
priority: "low"
|
|
6005
|
+
});
|
|
6006
|
+
}
|
|
6007
|
+
const layerScores = calculateLayerScores(body);
|
|
6008
|
+
const overallScore = calculateOverallScore(layerScores);
|
|
6009
|
+
if (overallScore < 40) {
|
|
6010
|
+
constraints.push({
|
|
6011
|
+
type: "restricted_scope",
|
|
6012
|
+
description: "Overall sovereignty score below threshold \u2014 restrict to non-sensitive operations",
|
|
6013
|
+
rationale: `Overall sovereignty score is ${overallScore}/100`,
|
|
6014
|
+
priority: "high"
|
|
6015
|
+
});
|
|
6016
|
+
}
|
|
6017
|
+
return constraints;
|
|
6018
|
+
}
|
|
6019
|
+
function transformSHRGeneric(shr) {
|
|
6020
|
+
const context = transformSHRForGateway(shr);
|
|
6021
|
+
return {
|
|
6022
|
+
agent_id: context.agent_identity,
|
|
6023
|
+
sovereignty_score: context.overall_score,
|
|
6024
|
+
trust_level: context.recommended_trust_level,
|
|
6025
|
+
layer_scores: {
|
|
6026
|
+
l1: context.layer_scores.l1_cognitive,
|
|
6027
|
+
l2: context.layer_scores.l2_operational,
|
|
6028
|
+
l3: context.layer_scores.l3_disclosure,
|
|
6029
|
+
l4: context.layer_scores.l4_reputation
|
|
6030
|
+
},
|
|
6031
|
+
capabilities: context.authorization_signals,
|
|
6032
|
+
constraints: context.recommended_constraints.map((c) => ({
|
|
6033
|
+
type: c.type,
|
|
6034
|
+
description: c.description
|
|
6035
|
+
})),
|
|
6036
|
+
expires_at: context.context_expires_at,
|
|
6037
|
+
signature: context.shr_signature
|
|
6038
|
+
};
|
|
6039
|
+
}
|
|
6040
|
+
|
|
5467
6041
|
// src/shr/tools.ts
|
|
5468
6042
|
function createSHRTools(config, identityManager, masterKey, auditLog) {
|
|
5469
6043
|
const generatorOpts = {
|
|
@@ -5526,6 +6100,53 @@ function createSHRTools(config, identityManager, masterKey, auditLog) {
|
|
|
5526
6100
|
);
|
|
5527
6101
|
return toolResult(result);
|
|
5528
6102
|
}
|
|
6103
|
+
},
|
|
6104
|
+
{
|
|
6105
|
+
name: "sanctuary/shr_gateway_export",
|
|
6106
|
+
description: "Export this instance's Sovereignty Health Report formatted for Ping Identity's Agent Gateway or other identity providers. Transforms the SHR into an authorization context with sovereignty scores, capability flags, and recommended access constraints.",
|
|
6107
|
+
inputSchema: {
|
|
6108
|
+
type: "object",
|
|
6109
|
+
properties: {
|
|
6110
|
+
format: {
|
|
6111
|
+
type: "string",
|
|
6112
|
+
enum: ["ping", "generic"],
|
|
6113
|
+
description: "Output format: 'ping' (Ping Identity Gateway format) or 'generic' (format-agnostic). Default: 'ping'."
|
|
6114
|
+
},
|
|
6115
|
+
identity_id: {
|
|
6116
|
+
type: "string",
|
|
6117
|
+
description: "Identity to sign the SHR with. Defaults to primary identity."
|
|
6118
|
+
},
|
|
6119
|
+
validity_minutes: {
|
|
6120
|
+
type: "number",
|
|
6121
|
+
description: "How long the SHR is valid (minutes). Default: 60."
|
|
6122
|
+
}
|
|
6123
|
+
}
|
|
6124
|
+
},
|
|
6125
|
+
handler: async (args) => {
|
|
6126
|
+
const format = args.format || "ping";
|
|
6127
|
+
const validityMs = args.validity_minutes ? args.validity_minutes * 60 * 1e3 : void 0;
|
|
6128
|
+
const shrResult = generateSHR(args.identity_id, {
|
|
6129
|
+
...generatorOpts,
|
|
6130
|
+
validityMs
|
|
6131
|
+
});
|
|
6132
|
+
if (typeof shrResult === "string") {
|
|
6133
|
+
return toolResult({ error: shrResult });
|
|
6134
|
+
}
|
|
6135
|
+
let context;
|
|
6136
|
+
if (format === "generic") {
|
|
6137
|
+
context = transformSHRGeneric(shrResult);
|
|
6138
|
+
} else {
|
|
6139
|
+
context = transformSHRForGateway(shrResult);
|
|
6140
|
+
}
|
|
6141
|
+
auditLog.append(
|
|
6142
|
+
"l2",
|
|
6143
|
+
"shr_gateway_export",
|
|
6144
|
+
shrResult.body.instance_id,
|
|
6145
|
+
void 0,
|
|
6146
|
+
"success"
|
|
6147
|
+
);
|
|
6148
|
+
return toolResult(context);
|
|
6149
|
+
}
|
|
5529
6150
|
}
|
|
5530
6151
|
];
|
|
5531
6152
|
return { tools };
|
|
@@ -5774,7 +6395,9 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog) {
|
|
|
5774
6395
|
return toolResult({
|
|
5775
6396
|
session_id: result.session.session_id,
|
|
5776
6397
|
response: result.response,
|
|
5777
|
-
instructions: "Send the 'response' object back to the initiator. When you receive their completion, pass it to sanctuary/handshake_status with this session_id."
|
|
6398
|
+
instructions: "Send the 'response' object back to the initiator. When you receive their completion, pass it to sanctuary/handshake_status with this session_id.",
|
|
6399
|
+
// SEC-ADD-03: Tag response — contains SHR data that will be sent to counterparty
|
|
6400
|
+
_content_trust: "external"
|
|
5778
6401
|
});
|
|
5779
6402
|
}
|
|
5780
6403
|
},
|
|
@@ -5827,7 +6450,9 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog) {
|
|
|
5827
6450
|
return toolResult({
|
|
5828
6451
|
completion: result.completion,
|
|
5829
6452
|
result: result.result,
|
|
5830
|
-
instructions: "Send the 'completion' object to the responder so they can verify the handshake. The 'result' object contains the verified counterparty status and trust tier."
|
|
6453
|
+
instructions: "Send the 'completion' object to the responder so they can verify the handshake. The 'result' object contains the verified counterparty status and trust tier.",
|
|
6454
|
+
// SEC-ADD-03: Tag response as containing counterparty-controlled SHR data
|
|
6455
|
+
_content_trust: "external"
|
|
5831
6456
|
});
|
|
5832
6457
|
}
|
|
5833
6458
|
},
|
|
@@ -6252,7 +6877,21 @@ function canonicalize(outcome) {
|
|
|
6252
6877
|
return stringToBytes(stableStringify(outcome));
|
|
6253
6878
|
}
|
|
6254
6879
|
function stableStringify(value) {
|
|
6255
|
-
if (value === null
|
|
6880
|
+
if (value === null) return "null";
|
|
6881
|
+
if (value === void 0) return "null";
|
|
6882
|
+
if (typeof value === "number") {
|
|
6883
|
+
if (!Number.isFinite(value)) {
|
|
6884
|
+
throw new Error(
|
|
6885
|
+
`Cannot canonicalize non-finite number: ${value}. NaN, Infinity, and -Infinity are not representable in JSON.`
|
|
6886
|
+
);
|
|
6887
|
+
}
|
|
6888
|
+
if (Object.is(value, -0)) {
|
|
6889
|
+
throw new Error(
|
|
6890
|
+
"Cannot canonicalize negative zero (-0). Use 0 instead for deterministic cross-language serialization."
|
|
6891
|
+
);
|
|
6892
|
+
}
|
|
6893
|
+
return JSON.stringify(value);
|
|
6894
|
+
}
|
|
6256
6895
|
if (typeof value !== "object") return JSON.stringify(value);
|
|
6257
6896
|
if (Array.isArray(value)) {
|
|
6258
6897
|
return "[" + value.map((v) => stableStringify(v)).join(",") + "]";
|
|
@@ -6280,11 +6919,12 @@ function createBridgeCommitment(outcome, identity, identityEncryptionKey, includ
|
|
|
6280
6919
|
bridge_commitment_id: commitmentId,
|
|
6281
6920
|
session_id: outcome.session_id,
|
|
6282
6921
|
sha256_commitment: sha2564.commitment,
|
|
6922
|
+
terms_hash: outcome.terms_hash,
|
|
6283
6923
|
committer_did: identity.did,
|
|
6284
6924
|
committed_at: now,
|
|
6285
6925
|
bridge_version: "sanctuary-concordia-bridge-v1"
|
|
6286
6926
|
};
|
|
6287
|
-
const payloadBytes = stringToBytes(
|
|
6927
|
+
const payloadBytes = stringToBytes(stableStringify(commitmentPayload));
|
|
6288
6928
|
const signature = sign(payloadBytes, identity.encrypted_private_key, identityEncryptionKey);
|
|
6289
6929
|
return {
|
|
6290
6930
|
bridge_commitment_id: commitmentId,
|
|
@@ -6310,11 +6950,12 @@ function verifyBridgeCommitment(commitment, outcome, committerPublicKey) {
|
|
|
6310
6950
|
bridge_commitment_id: commitment.bridge_commitment_id,
|
|
6311
6951
|
session_id: commitment.session_id,
|
|
6312
6952
|
sha256_commitment: commitment.sha256_commitment,
|
|
6953
|
+
terms_hash: outcome.terms_hash,
|
|
6313
6954
|
committer_did: commitment.committer_did,
|
|
6314
6955
|
committed_at: commitment.committed_at,
|
|
6315
6956
|
bridge_version: commitment.bridge_version
|
|
6316
6957
|
};
|
|
6317
|
-
const payloadBytes = stringToBytes(
|
|
6958
|
+
const payloadBytes = stringToBytes(stableStringify(commitmentPayload));
|
|
6318
6959
|
const sigBytes = fromBase64url(commitment.signature);
|
|
6319
6960
|
const signatureValid = verify(payloadBytes, sigBytes, committerPublicKey);
|
|
6320
6961
|
const sessionIdMatch = commitment.session_id === outcome.session_id;
|
|
@@ -6541,7 +7182,9 @@ function createBridgeTools(storage, masterKey, identityManager, auditLog, handsh
|
|
|
6541
7182
|
return toolResult({
|
|
6542
7183
|
...result,
|
|
6543
7184
|
session_id: storedCommitment.session_id,
|
|
6544
|
-
committer_did: storedCommitment.committer_did
|
|
7185
|
+
committer_did: storedCommitment.committer_did,
|
|
7186
|
+
// SEC-ADD-03: Tag response as containing counterparty-controlled data
|
|
7187
|
+
_content_trust: "external"
|
|
6545
7188
|
});
|
|
6546
7189
|
}
|
|
6547
7190
|
},
|
|
@@ -6635,35 +7278,2253 @@ function createBridgeTools(storage, masterKey, identityManager, auditLog, handsh
|
|
|
6635
7278
|
];
|
|
6636
7279
|
return { tools };
|
|
6637
7280
|
}
|
|
6638
|
-
|
|
6639
|
-
|
|
6640
|
-
|
|
6641
|
-
|
|
6642
|
-
|
|
6643
|
-
|
|
6644
|
-
|
|
6645
|
-
|
|
6646
|
-
|
|
6647
|
-
|
|
6648
|
-
|
|
6649
|
-
|
|
6650
|
-
|
|
6651
|
-
|
|
6652
|
-
|
|
6653
|
-
|
|
6654
|
-
|
|
6655
|
-
|
|
6656
|
-
|
|
6657
|
-
|
|
6658
|
-
|
|
6659
|
-
|
|
6660
|
-
|
|
6661
|
-
|
|
6662
|
-
|
|
6663
|
-
|
|
6664
|
-
|
|
6665
|
-
|
|
6666
|
-
|
|
7281
|
+
function lenientJsonParse(raw) {
|
|
7282
|
+
let cleaned = raw.replace(/\/\/[^\n]*/g, "");
|
|
7283
|
+
cleaned = cleaned.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
7284
|
+
cleaned = cleaned.replace(/,\s*([\]}])/g, "$1");
|
|
7285
|
+
return JSON.parse(cleaned);
|
|
7286
|
+
}
|
|
7287
|
+
async function fileExists(path) {
|
|
7288
|
+
try {
|
|
7289
|
+
await promises.access(path);
|
|
7290
|
+
return true;
|
|
7291
|
+
} catch {
|
|
7292
|
+
return false;
|
|
7293
|
+
}
|
|
7294
|
+
}
|
|
7295
|
+
async function safeReadFile(path) {
|
|
7296
|
+
try {
|
|
7297
|
+
return await promises.readFile(path, "utf-8");
|
|
7298
|
+
} catch {
|
|
7299
|
+
return null;
|
|
7300
|
+
}
|
|
7301
|
+
}
|
|
7302
|
+
async function detectEnvironment(config, deepScan) {
|
|
7303
|
+
const fingerprint = {
|
|
7304
|
+
sanctuary_installed: true,
|
|
7305
|
+
// We're running inside Sanctuary
|
|
7306
|
+
sanctuary_version: config.version,
|
|
7307
|
+
openclaw_detected: false,
|
|
7308
|
+
openclaw_version: null,
|
|
7309
|
+
openclaw_config: null,
|
|
7310
|
+
node_version: process.version,
|
|
7311
|
+
platform: `${process.platform}-${process.arch}`
|
|
7312
|
+
};
|
|
7313
|
+
if (!deepScan) {
|
|
7314
|
+
return fingerprint;
|
|
7315
|
+
}
|
|
7316
|
+
const home = os.homedir();
|
|
7317
|
+
const openclawConfigPath = path.join(home, ".openclaw", "openclaw.json");
|
|
7318
|
+
const openclawEnvPath = path.join(home, ".openclaw", ".env");
|
|
7319
|
+
const openclawMemoryPath = path.join(home, ".openclaw", "workspace", "MEMORY.md");
|
|
7320
|
+
const openclawMemoryDir = path.join(home, ".openclaw", "workspace", "memory");
|
|
7321
|
+
const configExists = await fileExists(openclawConfigPath);
|
|
7322
|
+
const envExists = await fileExists(openclawEnvPath);
|
|
7323
|
+
const memoryExists = await fileExists(openclawMemoryPath);
|
|
7324
|
+
const memoryDirExists = await fileExists(openclawMemoryDir);
|
|
7325
|
+
if (configExists || memoryExists || memoryDirExists) {
|
|
7326
|
+
fingerprint.openclaw_detected = true;
|
|
7327
|
+
fingerprint.openclaw_config = await auditOpenClawConfig(
|
|
7328
|
+
openclawConfigPath,
|
|
7329
|
+
openclawEnvPath,
|
|
7330
|
+
openclawMemoryPath,
|
|
7331
|
+
configExists,
|
|
7332
|
+
envExists,
|
|
7333
|
+
memoryExists
|
|
7334
|
+
);
|
|
7335
|
+
}
|
|
7336
|
+
return fingerprint;
|
|
7337
|
+
}
|
|
7338
|
+
async function auditOpenClawConfig(configPath, envPath, _memoryPath, configExists, envExists, memoryExists) {
|
|
7339
|
+
const audit = {
|
|
7340
|
+
config_path: configExists ? configPath : null,
|
|
7341
|
+
require_approval_enabled: false,
|
|
7342
|
+
sandbox_policy_active: false,
|
|
7343
|
+
sandbox_allow_list: [],
|
|
7344
|
+
sandbox_deny_list: [],
|
|
7345
|
+
memory_encrypted: false,
|
|
7346
|
+
// Stock OpenClaw never encrypts memory
|
|
7347
|
+
env_file_exposed: false,
|
|
7348
|
+
gateway_token_set: false,
|
|
7349
|
+
dm_pairing_enabled: false,
|
|
7350
|
+
mcp_bridge_active: false
|
|
7351
|
+
};
|
|
7352
|
+
if (configExists) {
|
|
7353
|
+
const raw = await safeReadFile(configPath);
|
|
7354
|
+
if (raw) {
|
|
7355
|
+
try {
|
|
7356
|
+
const parsed = lenientJsonParse(raw);
|
|
7357
|
+
const hooks = parsed.hooks;
|
|
7358
|
+
if (hooks) {
|
|
7359
|
+
const beforeToolCall = hooks.before_tool_call;
|
|
7360
|
+
if (beforeToolCall) {
|
|
7361
|
+
const hookStr = JSON.stringify(beforeToolCall);
|
|
7362
|
+
audit.require_approval_enabled = hookStr.includes("requireApproval");
|
|
7363
|
+
}
|
|
7364
|
+
}
|
|
7365
|
+
const tools = parsed.tools;
|
|
7366
|
+
if (tools) {
|
|
7367
|
+
const sandbox = tools.sandbox;
|
|
7368
|
+
if (sandbox) {
|
|
7369
|
+
const sandboxTools = sandbox.tools;
|
|
7370
|
+
if (sandboxTools) {
|
|
7371
|
+
audit.sandbox_policy_active = true;
|
|
7372
|
+
if (Array.isArray(sandboxTools.allow)) {
|
|
7373
|
+
audit.sandbox_allow_list = sandboxTools.allow.filter(
|
|
7374
|
+
(item) => typeof item === "string"
|
|
7375
|
+
);
|
|
7376
|
+
}
|
|
7377
|
+
if (Array.isArray(sandboxTools.alsoAllow)) {
|
|
7378
|
+
audit.sandbox_allow_list = [
|
|
7379
|
+
...audit.sandbox_allow_list,
|
|
7380
|
+
...sandboxTools.alsoAllow.filter(
|
|
7381
|
+
(item) => typeof item === "string"
|
|
7382
|
+
)
|
|
7383
|
+
];
|
|
7384
|
+
}
|
|
7385
|
+
if (Array.isArray(sandboxTools.deny)) {
|
|
7386
|
+
audit.sandbox_deny_list = sandboxTools.deny.filter(
|
|
7387
|
+
(item) => typeof item === "string"
|
|
7388
|
+
);
|
|
7389
|
+
}
|
|
7390
|
+
}
|
|
7391
|
+
}
|
|
7392
|
+
}
|
|
7393
|
+
const mcpServers = parsed.mcpServers;
|
|
7394
|
+
if (mcpServers && Object.keys(mcpServers).length > 0) {
|
|
7395
|
+
audit.mcp_bridge_active = true;
|
|
7396
|
+
}
|
|
7397
|
+
} catch {
|
|
7398
|
+
}
|
|
7399
|
+
}
|
|
7400
|
+
}
|
|
7401
|
+
if (envExists) {
|
|
7402
|
+
const envContent = await safeReadFile(envPath);
|
|
7403
|
+
if (envContent) {
|
|
7404
|
+
const secretPatterns = [
|
|
7405
|
+
/[A-Z_]*API_KEY\s*=/,
|
|
7406
|
+
/[A-Z_]*TOKEN\s*=/,
|
|
7407
|
+
/[A-Z_]*SECRET\s*=/,
|
|
7408
|
+
/[A-Z_]*PASSWORD\s*=/,
|
|
7409
|
+
/[A-Z_]*PRIVATE_KEY\s*=/
|
|
7410
|
+
];
|
|
7411
|
+
audit.env_file_exposed = secretPatterns.some((p) => p.test(envContent));
|
|
7412
|
+
audit.gateway_token_set = /OPENCLAW_GATEWAY_TOKEN\s*=/.test(envContent);
|
|
7413
|
+
}
|
|
7414
|
+
}
|
|
7415
|
+
if (memoryExists) {
|
|
7416
|
+
audit.memory_encrypted = false;
|
|
7417
|
+
}
|
|
7418
|
+
return audit;
|
|
7419
|
+
}
|
|
7420
|
+
|
|
7421
|
+
// src/audit/analyzer.ts
|
|
7422
|
+
var L1_ENCRYPTION_AT_REST = 10;
|
|
7423
|
+
var L1_IDENTITY_CRYPTOGRAPHIC = 10;
|
|
7424
|
+
var L1_INTEGRITY_VERIFICATION = 8;
|
|
7425
|
+
var L1_STATE_PORTABLE = 7;
|
|
7426
|
+
var L2_THREE_TIER_GATE = 10;
|
|
7427
|
+
var L2_BINARY_GATE = 3;
|
|
7428
|
+
var L2_ANOMALY_DETECTION = 5;
|
|
7429
|
+
var L2_ENCRYPTED_AUDIT = 4;
|
|
7430
|
+
var L2_TOOL_SANDBOXING = 2;
|
|
7431
|
+
var L2_CONTEXT_GATING = 4;
|
|
7432
|
+
var L2_PROCESS_HARDENING = 5;
|
|
7433
|
+
var L3_COMMITMENT_SCHEME = 8;
|
|
7434
|
+
var L3_ZK_PROOFS = 7;
|
|
7435
|
+
var L3_DISCLOSURE_POLICIES = 5;
|
|
7436
|
+
var L4_PORTABLE_REPUTATION = 6;
|
|
7437
|
+
var L4_SIGNED_ATTESTATIONS = 6;
|
|
7438
|
+
var L4_SYBIL_DETECTION = 4;
|
|
7439
|
+
var L4_SOVEREIGNTY_GATED = 4;
|
|
7440
|
+
var SEVERITY_ORDER = {
|
|
7441
|
+
critical: 0,
|
|
7442
|
+
high: 1,
|
|
7443
|
+
medium: 2,
|
|
7444
|
+
low: 3
|
|
7445
|
+
};
|
|
7446
|
+
var INCIDENT_META_SEV1 = {
|
|
7447
|
+
id: "META-SEV1-2026",
|
|
7448
|
+
name: "Meta Sev 1: Unauthorized autonomous data exposure",
|
|
7449
|
+
date: "2026-03-18",
|
|
7450
|
+
description: "AI agent autonomously posted proprietary code, business strategies, and user datasets to an internal forum without human approval. Two-hour exposure window."
|
|
7451
|
+
};
|
|
7452
|
+
var INCIDENT_OPENCLAW_SANDBOX = {
|
|
7453
|
+
id: "OPENCLAW-CVE-2026",
|
|
7454
|
+
name: "OpenClaw sandbox escape via privilege inheritance",
|
|
7455
|
+
date: "2026-03-18",
|
|
7456
|
+
description: "Nine CVEs in four days. Child processes inherited sandbox.mode=off from parent, bypassing runtime confinement. 42,900+ internet-exposed instances, 15,200 vulnerable to RCE.",
|
|
7457
|
+
cves: [
|
|
7458
|
+
"CVE-2026-32048",
|
|
7459
|
+
"CVE-2026-32915",
|
|
7460
|
+
"CVE-2026-32918"
|
|
7461
|
+
]
|
|
7462
|
+
};
|
|
7463
|
+
var INCIDENT_CONTEXT_LEAKAGE = {
|
|
7464
|
+
id: "CONTEXT-LEAK-CLASS",
|
|
7465
|
+
name: "Context leakage: Full state exposure to inference providers",
|
|
7466
|
+
date: "2026-03",
|
|
7467
|
+
description: "Agents send full context \u2014 conversation history, memory, secrets, internal reasoning \u2014 to remote LLM providers on every inference call with no filtering mechanism."
|
|
7468
|
+
};
|
|
7469
|
+
var INCIDENT_CLAUDE_CODE_LEAK = {
|
|
7470
|
+
id: "CLAUDE-CODE-LEAK-2026",
|
|
7471
|
+
name: "Claude Code source leak: 512K lines exposed via npm source map",
|
|
7472
|
+
date: "2026-03-31",
|
|
7473
|
+
description: "Anthropic accidentally shipped a 59.8 MB source map in npm package v2.1.88, exposing the full Claude Code TypeScript source \u2014 1,900 files, internal model codenames, unreleased features, OAuth flows, and multi-agent coordination logic."
|
|
7474
|
+
};
|
|
7475
|
+
function analyzeSovereignty(env, config) {
|
|
7476
|
+
const l1 = assessL1(env, config);
|
|
7477
|
+
const l2 = assessL2(env);
|
|
7478
|
+
const l3 = assessL3(env);
|
|
7479
|
+
const l4 = assessL4(env);
|
|
7480
|
+
const l1Score = scoreL1(l1);
|
|
7481
|
+
const l2Score = scoreL2(l2);
|
|
7482
|
+
const l3Score = scoreL3(l3);
|
|
7483
|
+
const l4Score = scoreL4(l4);
|
|
7484
|
+
const overallScore = l1Score + l2Score + l3Score + l4Score;
|
|
7485
|
+
const sovereigntyLevel = overallScore >= 80 ? "full" : overallScore >= 50 ? "partial" : overallScore >= 20 ? "minimal" : "none";
|
|
7486
|
+
const gaps = generateGaps(env, l1, l2, l3, l4);
|
|
7487
|
+
gaps.sort((a, b) => SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity]);
|
|
7488
|
+
const recommendations = generateRecommendations(env, l1, l2, l3, l4);
|
|
7489
|
+
return {
|
|
7490
|
+
version: "1.0",
|
|
7491
|
+
audited_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7492
|
+
environment: env,
|
|
7493
|
+
layers: {
|
|
7494
|
+
l1_cognitive: l1,
|
|
7495
|
+
l2_operational: l2,
|
|
7496
|
+
l3_selective_disclosure: l3,
|
|
7497
|
+
l4_reputation: l4
|
|
7498
|
+
},
|
|
7499
|
+
overall_score: overallScore,
|
|
7500
|
+
sovereignty_level: sovereigntyLevel,
|
|
7501
|
+
gaps,
|
|
7502
|
+
recommendations
|
|
7503
|
+
};
|
|
7504
|
+
}
|
|
7505
|
+
function assessL1(env, config) {
|
|
7506
|
+
const findings = [];
|
|
7507
|
+
const sanctuaryActive = env.sanctuary_installed;
|
|
7508
|
+
const encryptionAtRest = sanctuaryActive;
|
|
7509
|
+
const keyCustody = sanctuaryActive ? "self" : "none";
|
|
7510
|
+
const integrityVerification = sanctuaryActive;
|
|
7511
|
+
const identityCryptographic = sanctuaryActive;
|
|
7512
|
+
const statePortable = sanctuaryActive;
|
|
7513
|
+
if (sanctuaryActive) {
|
|
7514
|
+
findings.push("AES-256-GCM encryption active for all state");
|
|
7515
|
+
findings.push(`Key derivation: ${config.state.key_derivation}`);
|
|
7516
|
+
findings.push(`Identity provider: ${config.state.identity_provider}`);
|
|
7517
|
+
findings.push("Merkle integrity verification enabled");
|
|
7518
|
+
findings.push("State export/import available");
|
|
7519
|
+
}
|
|
7520
|
+
if (env.openclaw_detected && env.openclaw_config) {
|
|
7521
|
+
if (!env.openclaw_config.memory_encrypted) {
|
|
7522
|
+
findings.push("OpenClaw agent memory (MEMORY.md, daily notes) stored in plaintext");
|
|
7523
|
+
}
|
|
7524
|
+
if (env.openclaw_config.env_file_exposed) {
|
|
7525
|
+
findings.push("OpenClaw .env file contains plaintext API keys/tokens");
|
|
7526
|
+
}
|
|
7527
|
+
}
|
|
7528
|
+
const status = encryptionAtRest && identityCryptographic ? "active" : encryptionAtRest || identityCryptographic ? "partial" : "inactive";
|
|
7529
|
+
return {
|
|
7530
|
+
status,
|
|
7531
|
+
encryption_at_rest: encryptionAtRest,
|
|
7532
|
+
key_custody: keyCustody,
|
|
7533
|
+
integrity_verification: integrityVerification,
|
|
7534
|
+
identity_cryptographic: identityCryptographic,
|
|
7535
|
+
state_portable: statePortable,
|
|
7536
|
+
findings
|
|
7537
|
+
};
|
|
7538
|
+
}
|
|
7539
|
+
function assessL2(env, _config) {
|
|
7540
|
+
const findings = [];
|
|
7541
|
+
const sanctuaryActive = env.sanctuary_installed;
|
|
7542
|
+
let approvalGate = "none";
|
|
7543
|
+
let behavioralAnomalyDetection = false;
|
|
7544
|
+
let auditTrailEncrypted = false;
|
|
7545
|
+
let auditTrailExists = false;
|
|
7546
|
+
let toolSandboxing = "none";
|
|
7547
|
+
let contextGating = false;
|
|
7548
|
+
let processIsolationHardening = "none";
|
|
7549
|
+
if (sanctuaryActive) {
|
|
7550
|
+
approvalGate = "three-tier";
|
|
7551
|
+
behavioralAnomalyDetection = true;
|
|
7552
|
+
auditTrailEncrypted = true;
|
|
7553
|
+
auditTrailExists = true;
|
|
7554
|
+
contextGating = true;
|
|
7555
|
+
findings.push("Three-tier Principal Policy gate active");
|
|
7556
|
+
findings.push("Behavioral anomaly detection (BaselineTracker) enabled");
|
|
7557
|
+
findings.push("Encrypted audit trail active");
|
|
7558
|
+
findings.push("Context gating available (sanctuary/context_gate_set_policy)");
|
|
7559
|
+
}
|
|
7560
|
+
if (env.openclaw_detected && env.openclaw_config) {
|
|
7561
|
+
if (env.openclaw_config.require_approval_enabled) {
|
|
7562
|
+
if (!sanctuaryActive) {
|
|
7563
|
+
approvalGate = "binary";
|
|
7564
|
+
}
|
|
7565
|
+
findings.push("OpenClaw requireApproval hook enabled (binary approve/deny)");
|
|
7566
|
+
}
|
|
7567
|
+
if (env.openclaw_config.sandbox_policy_active) {
|
|
7568
|
+
if (!sanctuaryActive) {
|
|
7569
|
+
toolSandboxing = "basic";
|
|
7570
|
+
}
|
|
7571
|
+
findings.push(
|
|
7572
|
+
`OpenClaw sandbox policy active (${env.openclaw_config.sandbox_allow_list.length} allowed, ${env.openclaw_config.sandbox_deny_list.length} denied)`
|
|
7573
|
+
);
|
|
7574
|
+
}
|
|
7575
|
+
}
|
|
7576
|
+
processIsolationHardening = "none";
|
|
7577
|
+
const status = approvalGate === "three-tier" && auditTrailEncrypted ? "active" : approvalGate !== "none" || auditTrailExists ? "partial" : "inactive";
|
|
7578
|
+
return {
|
|
7579
|
+
status,
|
|
7580
|
+
approval_gate: approvalGate,
|
|
7581
|
+
behavioral_anomaly_detection: behavioralAnomalyDetection,
|
|
7582
|
+
audit_trail_encrypted: auditTrailEncrypted,
|
|
7583
|
+
audit_trail_exists: auditTrailExists,
|
|
7584
|
+
tool_sandboxing: sanctuaryActive ? "policy-enforced" : toolSandboxing,
|
|
7585
|
+
context_gating: contextGating,
|
|
7586
|
+
process_isolation_hardening: processIsolationHardening,
|
|
7587
|
+
findings
|
|
7588
|
+
};
|
|
7589
|
+
}
|
|
7590
|
+
function assessL3(env, _config) {
|
|
7591
|
+
const findings = [];
|
|
7592
|
+
const sanctuaryActive = env.sanctuary_installed;
|
|
7593
|
+
let commitmentScheme = "none";
|
|
7594
|
+
let zkProofs = false;
|
|
7595
|
+
let selectiveDisclosurePolicy = false;
|
|
7596
|
+
if (sanctuaryActive) {
|
|
7597
|
+
commitmentScheme = "pedersen+sha256";
|
|
7598
|
+
zkProofs = true;
|
|
7599
|
+
selectiveDisclosurePolicy = true;
|
|
7600
|
+
findings.push("SHA-256 + Pedersen commitment schemes active");
|
|
7601
|
+
findings.push("Schnorr zero-knowledge proofs (Fiat-Shamir) enabled \u2014 genuine ZK proofs");
|
|
7602
|
+
findings.push("Range proofs (bit-decomposition + OR-proofs) enabled \u2014 genuine ZK proofs");
|
|
7603
|
+
findings.push("Selective disclosure policies configurable");
|
|
7604
|
+
findings.push("Non-interactive proofs with replay-resistant domain separation");
|
|
7605
|
+
}
|
|
7606
|
+
const status = commitmentScheme === "pedersen+sha256" && zkProofs ? "active" : commitmentScheme !== "none" ? "partial" : "inactive";
|
|
7607
|
+
return {
|
|
7608
|
+
status,
|
|
7609
|
+
commitment_scheme: commitmentScheme,
|
|
7610
|
+
zero_knowledge_proofs: zkProofs,
|
|
7611
|
+
selective_disclosure_policy: selectiveDisclosurePolicy,
|
|
7612
|
+
findings
|
|
7613
|
+
};
|
|
7614
|
+
}
|
|
7615
|
+
function assessL4(env, _config) {
|
|
7616
|
+
const findings = [];
|
|
7617
|
+
const sanctuaryActive = env.sanctuary_installed;
|
|
7618
|
+
const reputationPortable = sanctuaryActive;
|
|
7619
|
+
const reputationSigned = sanctuaryActive;
|
|
7620
|
+
const sybilDetection = sanctuaryActive;
|
|
7621
|
+
const sovereigntyGated = sanctuaryActive;
|
|
7622
|
+
if (sanctuaryActive) {
|
|
7623
|
+
findings.push("Signed EAS-compatible attestations active");
|
|
7624
|
+
findings.push("Reputation export/import available");
|
|
7625
|
+
findings.push("Sybil detection heuristics enabled");
|
|
7626
|
+
findings.push("Sovereignty-gated reputation tiers active");
|
|
7627
|
+
} else {
|
|
7628
|
+
findings.push("No portable reputation system detected");
|
|
7629
|
+
}
|
|
7630
|
+
const status = reputationPortable && reputationSigned && sovereigntyGated ? "active" : reputationPortable || reputationSigned ? "partial" : "inactive";
|
|
7631
|
+
return {
|
|
7632
|
+
status,
|
|
7633
|
+
reputation_portable: reputationPortable,
|
|
7634
|
+
reputation_signed: reputationSigned,
|
|
7635
|
+
reputation_sybil_detection: sybilDetection,
|
|
7636
|
+
sovereignty_gated_tiers: sovereigntyGated,
|
|
7637
|
+
findings
|
|
7638
|
+
};
|
|
7639
|
+
}
|
|
7640
|
+
function scoreL1(l1) {
|
|
7641
|
+
let score = 0;
|
|
7642
|
+
if (l1.encryption_at_rest) score += L1_ENCRYPTION_AT_REST;
|
|
7643
|
+
if (l1.identity_cryptographic) score += L1_IDENTITY_CRYPTOGRAPHIC;
|
|
7644
|
+
if (l1.integrity_verification) score += L1_INTEGRITY_VERIFICATION;
|
|
7645
|
+
if (l1.state_portable) score += L1_STATE_PORTABLE;
|
|
7646
|
+
return score;
|
|
7647
|
+
}
|
|
7648
|
+
function scoreL2(l2) {
|
|
7649
|
+
let score = 0;
|
|
7650
|
+
if (l2.approval_gate === "three-tier") score += L2_THREE_TIER_GATE;
|
|
7651
|
+
else if (l2.approval_gate === "binary") score += L2_BINARY_GATE;
|
|
7652
|
+
if (l2.behavioral_anomaly_detection) score += L2_ANOMALY_DETECTION;
|
|
7653
|
+
if (l2.audit_trail_encrypted) score += L2_ENCRYPTED_AUDIT;
|
|
7654
|
+
if (l2.tool_sandboxing === "policy-enforced") score += L2_TOOL_SANDBOXING;
|
|
7655
|
+
else if (l2.tool_sandboxing === "basic") score += 1;
|
|
7656
|
+
if (l2.context_gating) score += L2_CONTEXT_GATING;
|
|
7657
|
+
if (l2.process_isolation_hardening === "hardened") score += L2_PROCESS_HARDENING;
|
|
7658
|
+
else if (l2.process_isolation_hardening === "basic") score += 2;
|
|
7659
|
+
return score;
|
|
7660
|
+
}
|
|
7661
|
+
function scoreL3(l3) {
|
|
7662
|
+
let score = 0;
|
|
7663
|
+
if (l3.commitment_scheme === "pedersen+sha256") score += L3_COMMITMENT_SCHEME;
|
|
7664
|
+
else if (l3.commitment_scheme === "sha256-only") score += 4;
|
|
7665
|
+
if (l3.zero_knowledge_proofs) score += L3_ZK_PROOFS;
|
|
7666
|
+
if (l3.selective_disclosure_policy) score += L3_DISCLOSURE_POLICIES;
|
|
7667
|
+
return score;
|
|
7668
|
+
}
|
|
7669
|
+
function scoreL4(l4) {
|
|
7670
|
+
let score = 0;
|
|
7671
|
+
if (l4.reputation_portable) score += L4_PORTABLE_REPUTATION;
|
|
7672
|
+
if (l4.reputation_signed) score += L4_SIGNED_ATTESTATIONS;
|
|
7673
|
+
if (l4.reputation_sybil_detection) score += L4_SYBIL_DETECTION;
|
|
7674
|
+
if (l4.sovereignty_gated_tiers) score += L4_SOVEREIGNTY_GATED;
|
|
7675
|
+
return score;
|
|
7676
|
+
}
|
|
7677
|
+
function generateGaps(env, l1, l2, l3, l4) {
|
|
7678
|
+
const gaps = [];
|
|
7679
|
+
const oc = env.openclaw_config;
|
|
7680
|
+
if (oc && !oc.memory_encrypted) {
|
|
7681
|
+
gaps.push({
|
|
7682
|
+
id: "GAP-L1-001",
|
|
7683
|
+
layer: "L1",
|
|
7684
|
+
severity: "critical",
|
|
7685
|
+
title: "Agent memory stored in plaintext",
|
|
7686
|
+
description: "Your agent's memory (MEMORY.md, daily notes, SQLite index) is stored in plaintext at ~/.openclaw/workspace/. Any process with file access can read your agent's full context \u2014 preferences, decisions, conversation history.",
|
|
7687
|
+
openclaw_relevance: "Stock OpenClaw stores all agent memory in plaintext files. There is no built-in encryption for agent state.",
|
|
7688
|
+
sanctuary_solution: "Sanctuary encrypts all state at rest with AES-256-GCM using a key derived from Argon2id, making state opaque to any process that doesn't hold the master key. Use sanctuary/state_write to migrate sensitive state to the encrypted store.",
|
|
7689
|
+
incident_class: INCIDENT_META_SEV1
|
|
7690
|
+
});
|
|
7691
|
+
}
|
|
7692
|
+
if (oc && oc.env_file_exposed) {
|
|
7693
|
+
gaps.push({
|
|
7694
|
+
id: "GAP-L1-002",
|
|
7695
|
+
layer: "L1",
|
|
7696
|
+
severity: "critical",
|
|
7697
|
+
title: "Plaintext API keys in .env file",
|
|
7698
|
+
description: "Your .env file contains plaintext API keys and tokens. These secrets are readable by any process with filesystem access.",
|
|
7699
|
+
openclaw_relevance: "OpenClaw stores API keys (LLM providers, gateway tokens) in a plaintext .env file.",
|
|
7700
|
+
sanctuary_solution: "Sanctuary's encrypted state store can hold secrets under the same AES-256-GCM envelope as all other state, tied to your self-custodied identity. Use sanctuary/state_write with namespace 'secrets'."
|
|
7701
|
+
});
|
|
7702
|
+
}
|
|
7703
|
+
if (!l1.identity_cryptographic) {
|
|
7704
|
+
gaps.push({
|
|
7705
|
+
id: "GAP-L1-003",
|
|
7706
|
+
layer: "L1",
|
|
7707
|
+
severity: "critical",
|
|
7708
|
+
title: "No cryptographic agent identity",
|
|
7709
|
+
description: "Your agent has no cryptographic identity. It cannot prove it is who it claims to be to any counterparty, sign messages, or participate in sovereignty handshakes.",
|
|
7710
|
+
openclaw_relevance: env.openclaw_detected ? "OpenClaw has no cryptographic agent identity. Agent identity is implicit (tied to the process/session), not cryptographically verifiable." : null,
|
|
7711
|
+
sanctuary_solution: "Sanctuary provides Ed25519 self-custodied identity with key rotation and delegation. Use sanctuary/identity_create to establish your cryptographic identity."
|
|
7712
|
+
});
|
|
7713
|
+
}
|
|
7714
|
+
if (l2.approval_gate === "binary" && !l2.behavioral_anomaly_detection) {
|
|
7715
|
+
gaps.push({
|
|
7716
|
+
id: "GAP-L2-001",
|
|
7717
|
+
layer: "L2",
|
|
7718
|
+
severity: "high",
|
|
7719
|
+
title: "Binary approval gate (no anomaly detection)",
|
|
7720
|
+
description: "Your approval gate provides binary approve/deny gating without behavioral anomaly detection. Routine operations require the same manual approval as sensitive ones.",
|
|
7721
|
+
openclaw_relevance: env.openclaw_detected ? "OpenClaw's requireApproval hook provides binary approve/deny gating. Sanctuary's three-tier Principal Policy adds behavioral anomaly detection (auto-escalation when agent behavior deviates from baseline), encrypted audit trails, and graduated approval tiers \u2014 so routine operations auto-proceed while sensitive operations require explicit consent." : null,
|
|
7722
|
+
sanctuary_solution: "Sanctuary's three-tier Principal Policy gate auto-allows routine operations (Tier 3), escalates anomalous behavior (Tier 2), and always requires human approval for irreversible operations (Tier 1). Use sanctuary/principal_policy_view to inspect.",
|
|
7723
|
+
incident_class: INCIDENT_META_SEV1
|
|
7724
|
+
});
|
|
7725
|
+
} else if (l2.approval_gate === "none") {
|
|
7726
|
+
gaps.push({
|
|
7727
|
+
id: "GAP-L2-001",
|
|
7728
|
+
layer: "L2",
|
|
7729
|
+
severity: "critical",
|
|
7730
|
+
title: "No approval gate",
|
|
7731
|
+
description: "No approval gate is configured. All tool calls execute without oversight.",
|
|
7732
|
+
openclaw_relevance: null,
|
|
7733
|
+
sanctuary_solution: "Sanctuary's Principal Policy evaluates every tool call before execution. Enable it to get three-tier approval gating with behavioral anomaly detection.",
|
|
7734
|
+
incident_class: INCIDENT_META_SEV1
|
|
7735
|
+
});
|
|
7736
|
+
}
|
|
7737
|
+
if (l2.tool_sandboxing === "basic") {
|
|
7738
|
+
gaps.push({
|
|
7739
|
+
id: "GAP-L2-002",
|
|
7740
|
+
layer: "L2",
|
|
7741
|
+
severity: "medium",
|
|
7742
|
+
title: "Basic tool sandboxing (no cryptographic attestation)",
|
|
7743
|
+
description: "Your tool sandbox enforces allow/deny lists but provides no cryptographic attestation of execution context.",
|
|
7744
|
+
openclaw_relevance: env.openclaw_detected ? "OpenClaw's sandbox tool policy (tools.sandbox.tools) enforces allow/deny lists. Sanctuary adds cryptographic attestation of execution context \u2014 a verifiable proof that an operation ran within policy, not just that a policy was configured." : null,
|
|
7745
|
+
sanctuary_solution: "Sanctuary provides cryptographic execution attestation via sanctuary/exec_attest and policy-enforced sandboxing with encrypted audit trails.",
|
|
7746
|
+
incident_class: INCIDENT_OPENCLAW_SANDBOX
|
|
7747
|
+
});
|
|
7748
|
+
}
|
|
7749
|
+
if (!l2.context_gating) {
|
|
7750
|
+
gaps.push({
|
|
7751
|
+
id: "GAP-L2-003",
|
|
7752
|
+
layer: "L2",
|
|
7753
|
+
severity: "high",
|
|
7754
|
+
title: "No context gating for outbound inference calls",
|
|
7755
|
+
description: "Your agent sends its full context \u2014 conversation history, memory, preferences, internal reasoning \u2014 to remote LLM providers on every inference call. There is no mechanism to filter what leaves the sovereignty boundary. The provider sees everything the agent knows.",
|
|
7756
|
+
openclaw_relevance: env.openclaw_detected ? "OpenClaw sends full agent context (including MEMORY.md, tool results, and conversation history) to the configured LLM provider with every API call. There is no built-in context filtering." : null,
|
|
7757
|
+
sanctuary_solution: "Sanctuary's context gating (sanctuary/context_gate_set_policy + sanctuary/context_gate_filter) lets you define per-provider policies that control exactly what context flows outbound. Redact secrets, hash identifiers, and send only minimum-necessary context for each call.",
|
|
7758
|
+
incident_class: INCIDENT_CONTEXT_LEAKAGE
|
|
7759
|
+
});
|
|
7760
|
+
}
|
|
7761
|
+
if (!l2.audit_trail_exists) {
|
|
7762
|
+
gaps.push({
|
|
7763
|
+
id: "GAP-L2-004",
|
|
7764
|
+
layer: "L2",
|
|
7765
|
+
severity: "high",
|
|
7766
|
+
title: "No audit trail",
|
|
7767
|
+
description: "No audit trail exists for tool call history. There is no record of what operations were executed, when, or by whom.",
|
|
7768
|
+
openclaw_relevance: null,
|
|
7769
|
+
sanctuary_solution: "Sanctuary maintains an encrypted audit log of all operations, queryable via sanctuary/monitor_audit_log.",
|
|
7770
|
+
incident_class: INCIDENT_CLAUDE_CODE_LEAK
|
|
7771
|
+
});
|
|
7772
|
+
}
|
|
7773
|
+
if (l3.commitment_scheme === "none") {
|
|
7774
|
+
gaps.push({
|
|
7775
|
+
id: "GAP-L3-001",
|
|
7776
|
+
layer: "L3",
|
|
7777
|
+
severity: "high",
|
|
7778
|
+
title: "No selective disclosure capability",
|
|
7779
|
+
description: "Your agent has no cryptographic mechanism to prove facts about its state without revealing the state itself. Every disclosure is all-or-nothing: no commitments, no zero-knowledge proofs, no selective disclosure policies.",
|
|
7780
|
+
openclaw_relevance: env.openclaw_detected ? "OpenClaw has no selective disclosure mechanism. When your agent shares information, it shares everything or nothing \u2014 there is no way to prove a claim without revealing the underlying data." : null,
|
|
7781
|
+
sanctuary_solution: "Sanctuary's L3 provides SHA-256 + Pedersen commitments with genuine zero-knowledge proofs (Schnorr + range proofs via Fiat-Shamir transform). Your agent can prove it has a valid credential, sufficient reputation, or a completed transaction without exposing the underlying data. Use sanctuary/zk_commit and sanctuary/zk_prove.",
|
|
7782
|
+
incident_class: INCIDENT_META_SEV1
|
|
7783
|
+
});
|
|
7784
|
+
}
|
|
7785
|
+
if (!l4.reputation_portable) {
|
|
7786
|
+
gaps.push({
|
|
7787
|
+
id: "GAP-L4-001",
|
|
7788
|
+
layer: "L4",
|
|
7789
|
+
severity: "high",
|
|
7790
|
+
title: "No portable reputation",
|
|
7791
|
+
description: "Your agent's reputation is platform-locked. If you move to a different harness or platform, your track record doesn't follow.",
|
|
7792
|
+
openclaw_relevance: env.openclaw_detected ? "OpenClaw has no reputation system. Your agent's track record exists only in conversation history, which is not structured, signed, or portable." : null,
|
|
7793
|
+
sanctuary_solution: "Sanctuary's L4 provides signed EAS-compatible attestations that are self-custodied, portable, and cryptographically verifiable. Your reputation is yours, not your platform's. Use sanctuary/reputation_record to start building portable reputation."
|
|
7794
|
+
});
|
|
7795
|
+
}
|
|
7796
|
+
return gaps;
|
|
7797
|
+
}
|
|
7798
|
+
function generateRecommendations(env, l1, l2, l3, l4) {
|
|
7799
|
+
const recs = [];
|
|
7800
|
+
if (!l1.identity_cryptographic) {
|
|
7801
|
+
recs.push({
|
|
7802
|
+
priority: 1,
|
|
7803
|
+
action: "Create a cryptographic identity \u2014 your agent's foundation for all sovereignty operations",
|
|
7804
|
+
tool: "sanctuary/identity_create",
|
|
7805
|
+
effort: "immediate",
|
|
7806
|
+
impact: "critical"
|
|
7807
|
+
});
|
|
7808
|
+
}
|
|
7809
|
+
if (!l1.encryption_at_rest || env.openclaw_config && !env.openclaw_config.memory_encrypted) {
|
|
7810
|
+
recs.push({
|
|
7811
|
+
priority: 2,
|
|
7812
|
+
action: "Migrate plaintext agent state to Sanctuary's encrypted store",
|
|
7813
|
+
tool: "sanctuary/state_write",
|
|
7814
|
+
effort: "minutes",
|
|
7815
|
+
impact: "critical"
|
|
7816
|
+
});
|
|
7817
|
+
}
|
|
7818
|
+
recs.push({
|
|
7819
|
+
priority: 3,
|
|
7820
|
+
action: "Generate a Sovereignty Health Report to present to counterparties",
|
|
7821
|
+
tool: "sanctuary/shr_generate",
|
|
7822
|
+
effort: "immediate",
|
|
7823
|
+
impact: "high"
|
|
7824
|
+
});
|
|
7825
|
+
if (l2.approval_gate !== "three-tier") {
|
|
7826
|
+
recs.push({
|
|
7827
|
+
priority: 4,
|
|
7828
|
+
action: "Enable the three-tier Principal Policy gate for graduated approval",
|
|
7829
|
+
tool: "sanctuary/principal_policy_view",
|
|
7830
|
+
effort: "minutes",
|
|
7831
|
+
impact: "high"
|
|
7832
|
+
});
|
|
7833
|
+
}
|
|
7834
|
+
if (!l2.context_gating) {
|
|
7835
|
+
recs.push({
|
|
7836
|
+
priority: 5,
|
|
7837
|
+
action: "Configure context gating to control what flows to LLM providers",
|
|
7838
|
+
tool: "sanctuary/context_gate_set_policy",
|
|
7839
|
+
effort: "minutes",
|
|
7840
|
+
impact: "high"
|
|
7841
|
+
});
|
|
7842
|
+
}
|
|
7843
|
+
if (!l4.reputation_signed) {
|
|
7844
|
+
recs.push({
|
|
7845
|
+
priority: 6,
|
|
7846
|
+
action: "Start recording reputation attestations from completed interactions",
|
|
7847
|
+
tool: "sanctuary/reputation_record",
|
|
7848
|
+
effort: "minutes",
|
|
7849
|
+
impact: "medium"
|
|
7850
|
+
});
|
|
7851
|
+
}
|
|
7852
|
+
if (!l3.selective_disclosure_policy) {
|
|
7853
|
+
recs.push({
|
|
7854
|
+
priority: 7,
|
|
7855
|
+
action: "Configure selective disclosure policies for data sharing",
|
|
7856
|
+
tool: "sanctuary/disclosure_set_policy",
|
|
7857
|
+
effort: "hours",
|
|
7858
|
+
impact: "medium"
|
|
7859
|
+
});
|
|
7860
|
+
}
|
|
7861
|
+
return recs;
|
|
7862
|
+
}
|
|
7863
|
+
function formatAuditReport(result) {
|
|
7864
|
+
const { environment: env, layers, overall_score, sovereignty_level, gaps, recommendations } = result;
|
|
7865
|
+
const scoreBar = formatScoreBar(overall_score);
|
|
7866
|
+
const levelLabel = sovereignty_level.toUpperCase();
|
|
7867
|
+
let report = "";
|
|
7868
|
+
report += "\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\n";
|
|
7869
|
+
report += " SOVEREIGNTY AUDIT REPORT\n";
|
|
7870
|
+
report += ` Generated: ${result.audited_at}
|
|
7871
|
+
`;
|
|
7872
|
+
report += "\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\n";
|
|
7873
|
+
report += "\n";
|
|
7874
|
+
report += ` Overall Score: ${overall_score} / 100 ${scoreBar} ${levelLabel}
|
|
7875
|
+
`;
|
|
7876
|
+
report += "\n";
|
|
7877
|
+
report += " Environment:\n";
|
|
7878
|
+
report += ` \u2022 Sanctuary v${env.sanctuary_version ?? "?"} ${padDots("Sanctuary v" + (env.sanctuary_version ?? "?"))} ${env.sanctuary_installed ? "\u2713 installed" : "\u2717 not found"}
|
|
7879
|
+
`;
|
|
7880
|
+
if (env.openclaw_detected) {
|
|
7881
|
+
report += ` \u2022 OpenClaw ${padDots("OpenClaw")} \u2713 detected
|
|
7882
|
+
`;
|
|
7883
|
+
if (env.openclaw_config) {
|
|
7884
|
+
report += ` \u2022 OpenClaw requireApproval ${padDots("OpenClaw requireApproval")} ${env.openclaw_config.require_approval_enabled ? "\u2713 enabled" : "\u2717 disabled"}
|
|
7885
|
+
`;
|
|
7886
|
+
report += ` \u2022 OpenClaw sandbox policy ${padDots("OpenClaw sandbox policy")} ${env.openclaw_config.sandbox_policy_active ? "\u2713 active" : "\u2717 inactive"}
|
|
7887
|
+
`;
|
|
7888
|
+
}
|
|
7889
|
+
}
|
|
7890
|
+
report += "\n";
|
|
7891
|
+
const l1Score = scoreL1(layers.l1_cognitive);
|
|
7892
|
+
const l2Score = scoreL2(layers.l2_operational);
|
|
7893
|
+
const l3Score = scoreL3(layers.l3_selective_disclosure);
|
|
7894
|
+
const l4Score = scoreL4(layers.l4_reputation);
|
|
7895
|
+
report += " Layer Assessment:\n";
|
|
7896
|
+
report += " \u250C\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\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n";
|
|
7897
|
+
report += " \u2502 Layer \u2502 Status \u2502 Score \u2502\n";
|
|
7898
|
+
report += " \u251C\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\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n";
|
|
7899
|
+
report += ` \u2502 L1 Cognitive Sovereignty \u2502 ${padStatus(layers.l1_cognitive.status)} \u2502 ${padScore(l1Score, 35)} \u2502
|
|
7900
|
+
`;
|
|
7901
|
+
report += ` \u2502 L2 Operational Isolation \u2502 ${padStatus(layers.l2_operational.status)} \u2502 ${padScore(l2Score, 25)} \u2502
|
|
7902
|
+
`;
|
|
7903
|
+
if (layers.l2_operational.context_gating) {
|
|
7904
|
+
report += ` \u2502 \u2514 Context Gating \u2502 ACTIVE \u2502 \u2502
|
|
7905
|
+
`;
|
|
7906
|
+
}
|
|
7907
|
+
report += ` \u2502 L3 Selective Disclosure \u2502 ${padStatus(layers.l3_selective_disclosure.status)} \u2502 ${padScore(l3Score, 20)} \u2502
|
|
7908
|
+
`;
|
|
7909
|
+
report += ` \u2502 L4 Verifiable Reputation \u2502 ${padStatus(layers.l4_reputation.status)} \u2502 ${padScore(l4Score, 20)} \u2502
|
|
7910
|
+
`;
|
|
7911
|
+
report += " \u2514\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\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n";
|
|
7912
|
+
report += "\n";
|
|
7913
|
+
if (gaps.length > 0) {
|
|
7914
|
+
report += ` \u26A0 ${gaps.length} SOVEREIGNTY GAP${gaps.length !== 1 ? "S" : ""} FOUND
|
|
7915
|
+
`;
|
|
7916
|
+
report += "\n";
|
|
7917
|
+
for (const gap of gaps) {
|
|
7918
|
+
const severityLabel = `[${gap.severity.toUpperCase()}]`;
|
|
7919
|
+
report += ` ${severityLabel} ${gap.id}: ${gap.title}
|
|
7920
|
+
`;
|
|
7921
|
+
const descLines = wordWrap(gap.description, 66);
|
|
7922
|
+
for (const line of descLines) {
|
|
7923
|
+
report += ` ${line}
|
|
7924
|
+
`;
|
|
7925
|
+
}
|
|
7926
|
+
if (gap.incident_class) {
|
|
7927
|
+
const ic = gap.incident_class;
|
|
7928
|
+
const cveStr = ic.cves?.length ? ` (${ic.cves.join(", ")})` : "";
|
|
7929
|
+
report += ` \u2192 Incident precedent: ${ic.name}${cveStr} [${ic.date}]
|
|
7930
|
+
`;
|
|
7931
|
+
}
|
|
7932
|
+
report += ` \u2192 Fix: ${gap.sanctuary_solution.split(".")[0]}.
|
|
7933
|
+
`;
|
|
7934
|
+
if (gap.openclaw_relevance) {
|
|
7935
|
+
report += ` \u2192 OpenClaw context: ${gap.openclaw_relevance.split(".")[0]}.
|
|
7936
|
+
`;
|
|
7937
|
+
}
|
|
7938
|
+
report += "\n";
|
|
7939
|
+
}
|
|
7940
|
+
} else {
|
|
7941
|
+
report += " \u2713 NO SOVEREIGNTY GAPS FOUND\n";
|
|
7942
|
+
report += "\n";
|
|
7943
|
+
}
|
|
7944
|
+
if (recommendations.length > 0) {
|
|
7945
|
+
report += " RECOMMENDED NEXT STEPS (in order):\n";
|
|
7946
|
+
for (const rec of recommendations) {
|
|
7947
|
+
const effortLabel = rec.effort === "immediate" ? "immediate" : rec.effort === "minutes" ? "5 min" : "30 min";
|
|
7948
|
+
report += ` ${rec.priority}. [${effortLabel}] ${rec.action}`;
|
|
7949
|
+
if (rec.tool) {
|
|
7950
|
+
report += `: ${rec.tool}`;
|
|
7951
|
+
}
|
|
7952
|
+
report += "\n";
|
|
7953
|
+
}
|
|
7954
|
+
report += "\n";
|
|
7955
|
+
}
|
|
7956
|
+
report += "\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\n";
|
|
7957
|
+
return report;
|
|
7958
|
+
}
|
|
7959
|
+
function formatScoreBar(score) {
|
|
7960
|
+
const filled = Math.round(score / 10);
|
|
7961
|
+
return "[" + "\u25A0".repeat(filled) + "\u2591".repeat(10 - filled) + "]";
|
|
7962
|
+
}
|
|
7963
|
+
function padDots(label) {
|
|
7964
|
+
const totalWidth = 30;
|
|
7965
|
+
const dotsNeeded = Math.max(2, totalWidth - label.length - 4);
|
|
7966
|
+
return ".".repeat(dotsNeeded);
|
|
7967
|
+
}
|
|
7968
|
+
function padStatus(status) {
|
|
7969
|
+
const label = status.toUpperCase();
|
|
7970
|
+
return label + " ".repeat(Math.max(0, 8 - label.length));
|
|
7971
|
+
}
|
|
7972
|
+
function padScore(score, max) {
|
|
7973
|
+
const text = `${score}/${max}`;
|
|
7974
|
+
return " ".repeat(Math.max(0, 5 - text.length)) + text;
|
|
7975
|
+
}
|
|
7976
|
+
function wordWrap(text, maxWidth) {
|
|
7977
|
+
const words = text.split(" ");
|
|
7978
|
+
const lines = [];
|
|
7979
|
+
let current = "";
|
|
7980
|
+
for (const word of words) {
|
|
7981
|
+
if (current.length + word.length + 1 > maxWidth && current.length > 0) {
|
|
7982
|
+
lines.push(current);
|
|
7983
|
+
current = word;
|
|
7984
|
+
} else {
|
|
7985
|
+
current = current.length > 0 ? current + " " + word : word;
|
|
7986
|
+
}
|
|
7987
|
+
}
|
|
7988
|
+
if (current.length > 0) lines.push(current);
|
|
7989
|
+
return lines;
|
|
7990
|
+
}
|
|
7991
|
+
|
|
7992
|
+
// src/audit/tools.ts
|
|
7993
|
+
function createAuditTools(config) {
|
|
7994
|
+
const tools = [
|
|
7995
|
+
{
|
|
7996
|
+
name: "sanctuary/sovereignty_audit",
|
|
7997
|
+
description: "Audit your agent's sovereignty posture. Inspects the local environment for encryption, identity, approval gates, selective disclosure, and reputation \u2014 including OpenClaw-specific configurations. Returns a scored gap analysis with prioritized recommendations.",
|
|
7998
|
+
inputSchema: {
|
|
7999
|
+
type: "object",
|
|
8000
|
+
properties: {
|
|
8001
|
+
deep_scan: {
|
|
8002
|
+
type: "boolean",
|
|
8003
|
+
description: "If true (default), also scans for OpenClaw config, .env files, and memory files. Set to false for a Sanctuary-only assessment."
|
|
8004
|
+
}
|
|
8005
|
+
}
|
|
8006
|
+
},
|
|
8007
|
+
handler: async (args) => {
|
|
8008
|
+
const deepScan = args.deep_scan !== false;
|
|
8009
|
+
const env = await detectEnvironment(config, deepScan);
|
|
8010
|
+
const result = analyzeSovereignty(env, config);
|
|
8011
|
+
const report = formatAuditReport(result);
|
|
8012
|
+
return {
|
|
8013
|
+
content: [
|
|
8014
|
+
{ type: "text", text: report },
|
|
8015
|
+
{ type: "text", text: JSON.stringify(result, null, 2) }
|
|
8016
|
+
]
|
|
8017
|
+
};
|
|
8018
|
+
}
|
|
8019
|
+
}
|
|
8020
|
+
];
|
|
8021
|
+
return { tools };
|
|
8022
|
+
}
|
|
8023
|
+
|
|
8024
|
+
// src/l2-operational/context-gate.ts
|
|
8025
|
+
init_encoding();
|
|
8026
|
+
init_hashing();
|
|
8027
|
+
var MAX_CONTEXT_FIELDS = 1e3;
|
|
8028
|
+
var MAX_POLICY_RULES = 50;
|
|
8029
|
+
var MAX_PATTERNS_PER_ARRAY = 500;
|
|
8030
|
+
function evaluateField(policy, provider, field) {
|
|
8031
|
+
const exactRule = policy.rules.find((r) => r.provider === provider);
|
|
8032
|
+
const wildcardRule = policy.rules.find((r) => r.provider === "*");
|
|
8033
|
+
const matchedRule = exactRule ?? wildcardRule;
|
|
8034
|
+
if (!matchedRule) {
|
|
8035
|
+
return {
|
|
8036
|
+
field,
|
|
8037
|
+
action: policy.default_action === "deny" ? "deny" : "redact",
|
|
8038
|
+
reason: `No rule matches provider "${provider}"; applying default (${policy.default_action})`
|
|
8039
|
+
};
|
|
8040
|
+
}
|
|
8041
|
+
if (matchesPattern(field, matchedRule.redact)) {
|
|
8042
|
+
return {
|
|
8043
|
+
field,
|
|
8044
|
+
action: "redact",
|
|
8045
|
+
reason: `Field "${field}" is explicitly redacted for ${matchedRule.provider} provider`
|
|
8046
|
+
};
|
|
8047
|
+
}
|
|
8048
|
+
if (matchesPattern(field, matchedRule.hash)) {
|
|
8049
|
+
return {
|
|
8050
|
+
field,
|
|
8051
|
+
action: "hash",
|
|
8052
|
+
reason: `Field "${field}" is hashed for ${matchedRule.provider} provider`
|
|
8053
|
+
};
|
|
8054
|
+
}
|
|
8055
|
+
if (matchesPattern(field, matchedRule.summarize)) {
|
|
8056
|
+
return {
|
|
8057
|
+
field,
|
|
8058
|
+
action: "summarize",
|
|
8059
|
+
reason: `Field "${field}" should be summarized for ${matchedRule.provider} provider`
|
|
8060
|
+
};
|
|
8061
|
+
}
|
|
8062
|
+
if (matchesPattern(field, matchedRule.allow)) {
|
|
8063
|
+
return {
|
|
8064
|
+
field,
|
|
8065
|
+
action: "allow",
|
|
8066
|
+
reason: `Field "${field}" is allowed for ${matchedRule.provider} provider`
|
|
8067
|
+
};
|
|
8068
|
+
}
|
|
8069
|
+
return {
|
|
8070
|
+
field,
|
|
8071
|
+
action: policy.default_action === "deny" ? "deny" : "redact",
|
|
8072
|
+
reason: `Field "${field}" not addressed in ${matchedRule.provider} rule; applying default (${policy.default_action})`
|
|
8073
|
+
};
|
|
8074
|
+
}
|
|
8075
|
+
function filterContext(policy, provider, context) {
|
|
8076
|
+
const fields = Object.keys(context);
|
|
8077
|
+
if (fields.length > MAX_CONTEXT_FIELDS) {
|
|
8078
|
+
throw new Error(
|
|
8079
|
+
`Context object has ${fields.length} fields, exceeding limit of ${MAX_CONTEXT_FIELDS}`
|
|
8080
|
+
);
|
|
8081
|
+
}
|
|
8082
|
+
const decisions = [];
|
|
8083
|
+
let allowed = 0;
|
|
8084
|
+
let redacted = 0;
|
|
8085
|
+
let hashed = 0;
|
|
8086
|
+
let summarized = 0;
|
|
8087
|
+
let denied = 0;
|
|
8088
|
+
for (const field of fields) {
|
|
8089
|
+
const result = evaluateField(policy, provider, field);
|
|
8090
|
+
if (result.action === "hash") {
|
|
8091
|
+
const value = typeof context[field] === "string" ? context[field] : JSON.stringify(context[field]);
|
|
8092
|
+
result.hash_value = hashToString(stringToBytes(value));
|
|
8093
|
+
}
|
|
8094
|
+
decisions.push(result);
|
|
8095
|
+
switch (result.action) {
|
|
8096
|
+
case "allow":
|
|
8097
|
+
allowed++;
|
|
8098
|
+
break;
|
|
8099
|
+
case "redact":
|
|
8100
|
+
redacted++;
|
|
8101
|
+
break;
|
|
8102
|
+
case "hash":
|
|
8103
|
+
hashed++;
|
|
8104
|
+
break;
|
|
8105
|
+
case "summarize":
|
|
8106
|
+
summarized++;
|
|
8107
|
+
break;
|
|
8108
|
+
case "deny":
|
|
8109
|
+
denied++;
|
|
8110
|
+
break;
|
|
8111
|
+
}
|
|
8112
|
+
}
|
|
8113
|
+
const originalHash = hashToString(
|
|
8114
|
+
stringToBytes(JSON.stringify(context))
|
|
8115
|
+
);
|
|
8116
|
+
const filteredOutput = {};
|
|
8117
|
+
for (const decision of decisions) {
|
|
8118
|
+
switch (decision.action) {
|
|
8119
|
+
case "allow":
|
|
8120
|
+
filteredOutput[decision.field] = context[decision.field];
|
|
8121
|
+
break;
|
|
8122
|
+
case "redact":
|
|
8123
|
+
filteredOutput[decision.field] = "[REDACTED]";
|
|
8124
|
+
break;
|
|
8125
|
+
case "hash":
|
|
8126
|
+
filteredOutput[decision.field] = `[HASH:${decision.hash_value}]`;
|
|
8127
|
+
break;
|
|
8128
|
+
case "summarize":
|
|
8129
|
+
filteredOutput[decision.field] = "[SUMMARIZE]";
|
|
8130
|
+
break;
|
|
8131
|
+
}
|
|
8132
|
+
}
|
|
8133
|
+
const filteredHash = hashToString(
|
|
8134
|
+
stringToBytes(JSON.stringify(filteredOutput))
|
|
8135
|
+
);
|
|
8136
|
+
return {
|
|
8137
|
+
policy_id: policy.policy_id,
|
|
8138
|
+
provider,
|
|
8139
|
+
fields_allowed: allowed,
|
|
8140
|
+
fields_redacted: redacted,
|
|
8141
|
+
fields_hashed: hashed,
|
|
8142
|
+
fields_summarized: summarized,
|
|
8143
|
+
fields_denied: denied,
|
|
8144
|
+
decisions,
|
|
8145
|
+
original_context_hash: originalHash,
|
|
8146
|
+
filtered_context_hash: filteredHash,
|
|
8147
|
+
filtered_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
8148
|
+
};
|
|
8149
|
+
}
|
|
8150
|
+
function matchesPattern(field, patterns) {
|
|
8151
|
+
const normalizedField = field.toLowerCase();
|
|
8152
|
+
for (const pattern of patterns) {
|
|
8153
|
+
if (pattern === "*") return true;
|
|
8154
|
+
const normalizedPattern = pattern.toLowerCase();
|
|
8155
|
+
if (normalizedPattern === normalizedField) return true;
|
|
8156
|
+
if (normalizedPattern.endsWith("*") && normalizedField.startsWith(normalizedPattern.slice(0, -1))) return true;
|
|
8157
|
+
if (normalizedPattern.startsWith("*") && normalizedField.endsWith(normalizedPattern.slice(1))) return true;
|
|
8158
|
+
}
|
|
8159
|
+
return false;
|
|
8160
|
+
}
|
|
8161
|
+
var ContextGatePolicyStore = class {
|
|
8162
|
+
storage;
|
|
8163
|
+
encryptionKey;
|
|
8164
|
+
policies = /* @__PURE__ */ new Map();
|
|
8165
|
+
constructor(storage, masterKey) {
|
|
8166
|
+
this.storage = storage;
|
|
8167
|
+
this.encryptionKey = derivePurposeKey(masterKey, "l2-context-gate");
|
|
8168
|
+
}
|
|
8169
|
+
/**
|
|
8170
|
+
* Create and store a new context-gating policy.
|
|
8171
|
+
*/
|
|
8172
|
+
async create(policyName, rules, defaultAction, identityId) {
|
|
8173
|
+
const policyId = `cg-${Date.now()}-${toBase64url(randomBytes(8))}`;
|
|
8174
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8175
|
+
const policy = {
|
|
8176
|
+
policy_id: policyId,
|
|
8177
|
+
policy_name: policyName,
|
|
8178
|
+
rules,
|
|
8179
|
+
default_action: defaultAction,
|
|
8180
|
+
identity_id: identityId,
|
|
8181
|
+
created_at: now,
|
|
8182
|
+
updated_at: now
|
|
8183
|
+
};
|
|
8184
|
+
await this.persist(policy);
|
|
8185
|
+
this.policies.set(policyId, policy);
|
|
8186
|
+
return policy;
|
|
8187
|
+
}
|
|
8188
|
+
/**
|
|
8189
|
+
* Get a policy by ID.
|
|
8190
|
+
*/
|
|
8191
|
+
async get(policyId) {
|
|
8192
|
+
if (this.policies.has(policyId)) {
|
|
8193
|
+
return this.policies.get(policyId);
|
|
8194
|
+
}
|
|
8195
|
+
const raw = await this.storage.read("_context_gate_policies", policyId);
|
|
8196
|
+
if (!raw) return null;
|
|
8197
|
+
try {
|
|
8198
|
+
const encrypted = JSON.parse(bytesToString(raw));
|
|
8199
|
+
const decrypted = decrypt(encrypted, this.encryptionKey);
|
|
8200
|
+
const policy = JSON.parse(bytesToString(decrypted));
|
|
8201
|
+
this.policies.set(policyId, policy);
|
|
8202
|
+
return policy;
|
|
8203
|
+
} catch {
|
|
8204
|
+
return null;
|
|
8205
|
+
}
|
|
8206
|
+
}
|
|
8207
|
+
/**
|
|
8208
|
+
* List all context-gating policies.
|
|
8209
|
+
*/
|
|
8210
|
+
async list() {
|
|
8211
|
+
await this.loadAll();
|
|
8212
|
+
return Array.from(this.policies.values());
|
|
8213
|
+
}
|
|
8214
|
+
/**
|
|
8215
|
+
* Load all persisted policies into memory.
|
|
8216
|
+
*/
|
|
8217
|
+
async loadAll() {
|
|
8218
|
+
try {
|
|
8219
|
+
const entries = await this.storage.list("_context_gate_policies");
|
|
8220
|
+
for (const meta of entries) {
|
|
8221
|
+
if (this.policies.has(meta.key)) continue;
|
|
8222
|
+
const raw = await this.storage.read("_context_gate_policies", meta.key);
|
|
8223
|
+
if (!raw) continue;
|
|
8224
|
+
try {
|
|
8225
|
+
const encrypted = JSON.parse(bytesToString(raw));
|
|
8226
|
+
const decrypted = decrypt(encrypted, this.encryptionKey);
|
|
8227
|
+
const policy = JSON.parse(bytesToString(decrypted));
|
|
8228
|
+
this.policies.set(policy.policy_id, policy);
|
|
8229
|
+
} catch {
|
|
8230
|
+
}
|
|
8231
|
+
}
|
|
8232
|
+
} catch {
|
|
8233
|
+
}
|
|
8234
|
+
}
|
|
8235
|
+
async persist(policy) {
|
|
8236
|
+
const serialized = stringToBytes(JSON.stringify(policy));
|
|
8237
|
+
const encrypted = encrypt(serialized, this.encryptionKey);
|
|
8238
|
+
await this.storage.write(
|
|
8239
|
+
"_context_gate_policies",
|
|
8240
|
+
policy.policy_id,
|
|
8241
|
+
stringToBytes(JSON.stringify(encrypted))
|
|
8242
|
+
);
|
|
8243
|
+
}
|
|
8244
|
+
};
|
|
8245
|
+
|
|
8246
|
+
// src/l2-operational/context-gate-templates.ts
|
|
8247
|
+
var ALWAYS_REDACT_SECRETS = [
|
|
8248
|
+
"api_key",
|
|
8249
|
+
"secret_*",
|
|
8250
|
+
"*_secret",
|
|
8251
|
+
"*_token",
|
|
8252
|
+
"*_key",
|
|
8253
|
+
"password",
|
|
8254
|
+
"*_password",
|
|
8255
|
+
"credential",
|
|
8256
|
+
"*_credential",
|
|
8257
|
+
"private_key",
|
|
8258
|
+
"recovery_key",
|
|
8259
|
+
"passphrase",
|
|
8260
|
+
"auth_*"
|
|
8261
|
+
];
|
|
8262
|
+
var PII_PATTERNS = [
|
|
8263
|
+
"*_pii",
|
|
8264
|
+
"name",
|
|
8265
|
+
"full_name",
|
|
8266
|
+
"email",
|
|
8267
|
+
"email_address",
|
|
8268
|
+
"phone",
|
|
8269
|
+
"phone_number",
|
|
8270
|
+
"address",
|
|
8271
|
+
"ssn",
|
|
8272
|
+
"date_of_birth",
|
|
8273
|
+
"ip_address",
|
|
8274
|
+
"credit_card",
|
|
8275
|
+
"card_number",
|
|
8276
|
+
"cvv",
|
|
8277
|
+
"bank_account",
|
|
8278
|
+
"account_number",
|
|
8279
|
+
"routing_number"
|
|
8280
|
+
];
|
|
8281
|
+
var INTERNAL_STATE_PATTERNS = [
|
|
8282
|
+
"memory",
|
|
8283
|
+
"agent_memory",
|
|
8284
|
+
"internal_reasoning",
|
|
8285
|
+
"internal_state",
|
|
8286
|
+
"reasoning_trace",
|
|
8287
|
+
"chain_of_thought",
|
|
8288
|
+
"private_notes",
|
|
8289
|
+
"soul",
|
|
8290
|
+
"personality",
|
|
8291
|
+
"system_prompt"
|
|
8292
|
+
];
|
|
8293
|
+
var ID_PATTERNS = [
|
|
8294
|
+
"user_id",
|
|
8295
|
+
"session_id",
|
|
8296
|
+
"agent_id",
|
|
8297
|
+
"identity_id",
|
|
8298
|
+
"conversation_id",
|
|
8299
|
+
"thread_id"
|
|
8300
|
+
];
|
|
8301
|
+
var HISTORY_PATTERNS = [
|
|
8302
|
+
"conversation_history",
|
|
8303
|
+
"message_history",
|
|
8304
|
+
"chat_history",
|
|
8305
|
+
"context_window",
|
|
8306
|
+
"previous_messages"
|
|
8307
|
+
];
|
|
8308
|
+
var INFERENCE_MINIMAL = {
|
|
8309
|
+
id: "inference-minimal",
|
|
8310
|
+
name: "Inference Minimal",
|
|
8311
|
+
description: "Maximum privacy. Only the current task and query reach the LLM provider.",
|
|
8312
|
+
use_when: "You want the strictest possible context control for inference calls. The LLM sees only what it needs for the immediate task.",
|
|
8313
|
+
rules: [
|
|
8314
|
+
{
|
|
8315
|
+
provider: "inference",
|
|
8316
|
+
allow: [
|
|
8317
|
+
"task",
|
|
8318
|
+
"task_description",
|
|
8319
|
+
"current_query",
|
|
8320
|
+
"query",
|
|
8321
|
+
"prompt",
|
|
8322
|
+
"question",
|
|
8323
|
+
"instruction"
|
|
8324
|
+
],
|
|
8325
|
+
redact: [
|
|
8326
|
+
...ALWAYS_REDACT_SECRETS,
|
|
8327
|
+
...PII_PATTERNS,
|
|
8328
|
+
...INTERNAL_STATE_PATTERNS,
|
|
8329
|
+
...HISTORY_PATTERNS,
|
|
8330
|
+
"tool_results",
|
|
8331
|
+
"previous_results"
|
|
8332
|
+
],
|
|
8333
|
+
hash: [...ID_PATTERNS],
|
|
8334
|
+
summarize: []
|
|
8335
|
+
}
|
|
8336
|
+
],
|
|
8337
|
+
default_action: "redact"
|
|
8338
|
+
};
|
|
8339
|
+
var INFERENCE_STANDARD = {
|
|
8340
|
+
id: "inference-standard",
|
|
8341
|
+
name: "Inference Standard",
|
|
8342
|
+
description: "Balanced privacy. Task, query, and tool results pass through. History flagged for summarization. Secrets and PII redacted.",
|
|
8343
|
+
use_when: "You need the LLM to have enough context for multi-step tasks while keeping secrets, PII, and internal reasoning private.",
|
|
8344
|
+
rules: [
|
|
8345
|
+
{
|
|
8346
|
+
provider: "inference",
|
|
8347
|
+
allow: [
|
|
8348
|
+
"task",
|
|
8349
|
+
"task_description",
|
|
8350
|
+
"current_query",
|
|
8351
|
+
"query",
|
|
8352
|
+
"prompt",
|
|
8353
|
+
"question",
|
|
8354
|
+
"instruction",
|
|
8355
|
+
"tool_results",
|
|
8356
|
+
"tool_output",
|
|
8357
|
+
"previous_results",
|
|
8358
|
+
"current_step",
|
|
8359
|
+
"remaining_steps",
|
|
8360
|
+
"objective",
|
|
8361
|
+
"constraints",
|
|
8362
|
+
"format",
|
|
8363
|
+
"output_format"
|
|
8364
|
+
],
|
|
8365
|
+
redact: [
|
|
8366
|
+
...ALWAYS_REDACT_SECRETS,
|
|
8367
|
+
...PII_PATTERNS,
|
|
8368
|
+
...INTERNAL_STATE_PATTERNS
|
|
8369
|
+
],
|
|
8370
|
+
hash: [...ID_PATTERNS],
|
|
8371
|
+
summarize: [...HISTORY_PATTERNS]
|
|
8372
|
+
}
|
|
8373
|
+
],
|
|
8374
|
+
default_action: "redact"
|
|
8375
|
+
};
|
|
8376
|
+
var LOGGING_STRICT = {
|
|
8377
|
+
id: "logging-strict",
|
|
8378
|
+
name: "Logging Strict",
|
|
8379
|
+
description: "Redacts all content for logging and analytics providers. Only operation metadata passes through.",
|
|
8380
|
+
use_when: "You send telemetry to logging or analytics services and want usage metrics without any content exposure.",
|
|
8381
|
+
rules: [
|
|
8382
|
+
{
|
|
8383
|
+
provider: "logging",
|
|
8384
|
+
allow: [
|
|
8385
|
+
"operation",
|
|
8386
|
+
"operation_name",
|
|
8387
|
+
"tool_name",
|
|
8388
|
+
"timestamp",
|
|
8389
|
+
"duration_ms",
|
|
8390
|
+
"status",
|
|
8391
|
+
"error_code",
|
|
8392
|
+
"event_type"
|
|
8393
|
+
],
|
|
8394
|
+
redact: [
|
|
8395
|
+
...ALWAYS_REDACT_SECRETS,
|
|
8396
|
+
...PII_PATTERNS,
|
|
8397
|
+
...INTERNAL_STATE_PATTERNS,
|
|
8398
|
+
...HISTORY_PATTERNS
|
|
8399
|
+
],
|
|
8400
|
+
hash: [...ID_PATTERNS],
|
|
8401
|
+
summarize: []
|
|
8402
|
+
},
|
|
8403
|
+
{
|
|
8404
|
+
provider: "analytics",
|
|
8405
|
+
allow: [
|
|
8406
|
+
"event_type",
|
|
8407
|
+
"timestamp",
|
|
8408
|
+
"duration_ms",
|
|
8409
|
+
"status",
|
|
8410
|
+
"tool_name"
|
|
8411
|
+
],
|
|
8412
|
+
redact: [
|
|
8413
|
+
...ALWAYS_REDACT_SECRETS,
|
|
8414
|
+
...PII_PATTERNS,
|
|
8415
|
+
...INTERNAL_STATE_PATTERNS,
|
|
8416
|
+
...HISTORY_PATTERNS
|
|
8417
|
+
],
|
|
8418
|
+
hash: [...ID_PATTERNS],
|
|
8419
|
+
summarize: []
|
|
8420
|
+
}
|
|
8421
|
+
],
|
|
8422
|
+
default_action: "redact"
|
|
8423
|
+
};
|
|
8424
|
+
var TOOL_API_SCOPED = {
|
|
8425
|
+
id: "tool-api-scoped",
|
|
8426
|
+
name: "Tool API Scoped",
|
|
8427
|
+
description: "Allows tool-specific parameters for external API calls. Redacts memory, history, secrets, and PII.",
|
|
8428
|
+
use_when: "Your agent calls external APIs (search, database, web) and you want to send query parameters without full agent context. Note: 'headers' and 'body' are redacted by default because they frequently carry authorization tokens. Add them to 'allow' only if you verify they contain no credentials for your use case.",
|
|
8429
|
+
rules: [
|
|
8430
|
+
{
|
|
8431
|
+
provider: "tool-api",
|
|
8432
|
+
allow: [
|
|
8433
|
+
"task",
|
|
8434
|
+
"task_description",
|
|
8435
|
+
"query",
|
|
8436
|
+
"search_query",
|
|
8437
|
+
"tool_input",
|
|
8438
|
+
"tool_parameters",
|
|
8439
|
+
"url",
|
|
8440
|
+
"endpoint",
|
|
8441
|
+
"method",
|
|
8442
|
+
"filter",
|
|
8443
|
+
"sort",
|
|
8444
|
+
"limit",
|
|
8445
|
+
"offset"
|
|
8446
|
+
],
|
|
8447
|
+
redact: [
|
|
8448
|
+
...ALWAYS_REDACT_SECRETS,
|
|
8449
|
+
...PII_PATTERNS,
|
|
8450
|
+
...INTERNAL_STATE_PATTERNS,
|
|
8451
|
+
...HISTORY_PATTERNS
|
|
8452
|
+
],
|
|
8453
|
+
hash: [...ID_PATTERNS],
|
|
8454
|
+
summarize: []
|
|
8455
|
+
}
|
|
8456
|
+
],
|
|
8457
|
+
default_action: "redact"
|
|
8458
|
+
};
|
|
8459
|
+
var TEMPLATES = {
|
|
8460
|
+
"inference-minimal": INFERENCE_MINIMAL,
|
|
8461
|
+
"inference-standard": INFERENCE_STANDARD,
|
|
8462
|
+
"logging-strict": LOGGING_STRICT,
|
|
8463
|
+
"tool-api-scoped": TOOL_API_SCOPED
|
|
8464
|
+
};
|
|
8465
|
+
function listTemplateIds() {
|
|
8466
|
+
return Object.keys(TEMPLATES);
|
|
8467
|
+
}
|
|
8468
|
+
function getTemplate(id) {
|
|
8469
|
+
return TEMPLATES[id];
|
|
8470
|
+
}
|
|
8471
|
+
|
|
8472
|
+
// src/l2-operational/context-gate-recommend.ts
|
|
8473
|
+
var CLASSIFICATION_RULES = [
|
|
8474
|
+
// ── Secrets (always redact, high confidence) ─────────────────────
|
|
8475
|
+
{
|
|
8476
|
+
patterns: [
|
|
8477
|
+
"api_key",
|
|
8478
|
+
"apikey",
|
|
8479
|
+
"api_secret",
|
|
8480
|
+
"secret",
|
|
8481
|
+
"secret_key",
|
|
8482
|
+
"secret_token",
|
|
8483
|
+
"password",
|
|
8484
|
+
"passwd",
|
|
8485
|
+
"pass",
|
|
8486
|
+
"credential",
|
|
8487
|
+
"credentials",
|
|
8488
|
+
"private_key",
|
|
8489
|
+
"privkey",
|
|
8490
|
+
"recovery_key",
|
|
8491
|
+
"passphrase",
|
|
8492
|
+
"token",
|
|
8493
|
+
"access_token",
|
|
8494
|
+
"refresh_token",
|
|
8495
|
+
"bearer_token",
|
|
8496
|
+
"auth_token",
|
|
8497
|
+
"auth_header",
|
|
8498
|
+
"authorization",
|
|
8499
|
+
"encryption_key",
|
|
8500
|
+
"master_key",
|
|
8501
|
+
"signing_key",
|
|
8502
|
+
"webhook_secret",
|
|
8503
|
+
"client_secret",
|
|
8504
|
+
"connection_string"
|
|
8505
|
+
],
|
|
8506
|
+
action: "redact",
|
|
8507
|
+
confidence: "high",
|
|
8508
|
+
reason: "Matches known secret/credential pattern"
|
|
8509
|
+
},
|
|
8510
|
+
// ── PII (always redact, high confidence) ─────────────────────────
|
|
8511
|
+
{
|
|
8512
|
+
patterns: [
|
|
8513
|
+
"name",
|
|
8514
|
+
"full_name",
|
|
8515
|
+
"first_name",
|
|
8516
|
+
"last_name",
|
|
8517
|
+
"display_name",
|
|
8518
|
+
"email",
|
|
8519
|
+
"email_address",
|
|
8520
|
+
"phone",
|
|
8521
|
+
"phone_number",
|
|
8522
|
+
"mobile",
|
|
8523
|
+
"address",
|
|
8524
|
+
"street_address",
|
|
8525
|
+
"mailing_address",
|
|
8526
|
+
"ssn",
|
|
8527
|
+
"social_security",
|
|
8528
|
+
"date_of_birth",
|
|
8529
|
+
"dob",
|
|
8530
|
+
"birthday",
|
|
8531
|
+
"ip_address",
|
|
8532
|
+
"ip",
|
|
8533
|
+
"location",
|
|
8534
|
+
"geolocation",
|
|
8535
|
+
"coordinates",
|
|
8536
|
+
"credit_card",
|
|
8537
|
+
"card_number",
|
|
8538
|
+
"cvv",
|
|
8539
|
+
"bank_account",
|
|
8540
|
+
"routing_number",
|
|
8541
|
+
"passport",
|
|
8542
|
+
"drivers_license",
|
|
8543
|
+
"license_number"
|
|
8544
|
+
],
|
|
8545
|
+
action: "redact",
|
|
8546
|
+
confidence: "high",
|
|
8547
|
+
reason: "Matches known PII pattern"
|
|
8548
|
+
},
|
|
8549
|
+
// ── Internal agent state (redact, high confidence) ───────────────
|
|
8550
|
+
{
|
|
8551
|
+
patterns: [
|
|
8552
|
+
"memory",
|
|
8553
|
+
"agent_memory",
|
|
8554
|
+
"long_term_memory",
|
|
8555
|
+
"internal_reasoning",
|
|
8556
|
+
"reasoning_trace",
|
|
8557
|
+
"chain_of_thought",
|
|
8558
|
+
"internal_state",
|
|
8559
|
+
"agent_state",
|
|
8560
|
+
"private_notes",
|
|
8561
|
+
"scratchpad",
|
|
8562
|
+
"soul",
|
|
8563
|
+
"personality",
|
|
8564
|
+
"persona",
|
|
8565
|
+
"system_prompt",
|
|
8566
|
+
"system_message",
|
|
8567
|
+
"system_instruction",
|
|
8568
|
+
"preferences",
|
|
8569
|
+
"user_preferences",
|
|
8570
|
+
"agent_preferences",
|
|
8571
|
+
"beliefs",
|
|
8572
|
+
"goals",
|
|
8573
|
+
"motivations"
|
|
8574
|
+
],
|
|
8575
|
+
action: "redact",
|
|
8576
|
+
confidence: "high",
|
|
8577
|
+
reason: "Matches known internal agent state pattern"
|
|
8578
|
+
},
|
|
8579
|
+
// ── IDs (hash, medium confidence) ────────────────────────────────
|
|
8580
|
+
{
|
|
8581
|
+
patterns: [
|
|
8582
|
+
"user_id",
|
|
8583
|
+
"userid",
|
|
8584
|
+
"session_id",
|
|
8585
|
+
"sessionid",
|
|
8586
|
+
"agent_id",
|
|
8587
|
+
"agentid",
|
|
8588
|
+
"identity_id",
|
|
8589
|
+
"conversation_id",
|
|
8590
|
+
"thread_id",
|
|
8591
|
+
"threadid",
|
|
8592
|
+
"request_id",
|
|
8593
|
+
"requestid",
|
|
8594
|
+
"correlation_id",
|
|
8595
|
+
"trace_id",
|
|
8596
|
+
"traceid",
|
|
8597
|
+
"account_id",
|
|
8598
|
+
"accountid"
|
|
8599
|
+
],
|
|
8600
|
+
action: "hash",
|
|
8601
|
+
confidence: "medium",
|
|
8602
|
+
reason: "Matches known identifier pattern \u2014 hash preserves correlation without exposing value"
|
|
8603
|
+
},
|
|
8604
|
+
// ── History (summarize, medium confidence) ───────────────────────
|
|
8605
|
+
{
|
|
8606
|
+
patterns: [
|
|
8607
|
+
"conversation_history",
|
|
8608
|
+
"chat_history",
|
|
8609
|
+
"message_history",
|
|
8610
|
+
"messages",
|
|
8611
|
+
"previous_messages",
|
|
8612
|
+
"prior_messages",
|
|
8613
|
+
"context_window",
|
|
8614
|
+
"interaction_history",
|
|
8615
|
+
"audit_log",
|
|
8616
|
+
"event_log"
|
|
8617
|
+
],
|
|
8618
|
+
action: "summarize",
|
|
8619
|
+
confidence: "medium",
|
|
8620
|
+
reason: "Matches known history/log pattern \u2014 summarize to reduce exposure"
|
|
8621
|
+
},
|
|
8622
|
+
// ── Task/query (allow, medium confidence) ────────────────────────
|
|
8623
|
+
{
|
|
8624
|
+
patterns: [
|
|
8625
|
+
"task",
|
|
8626
|
+
"task_description",
|
|
8627
|
+
"query",
|
|
8628
|
+
"current_query",
|
|
8629
|
+
"search_query",
|
|
8630
|
+
"prompt",
|
|
8631
|
+
"user_prompt",
|
|
8632
|
+
"question",
|
|
8633
|
+
"current_question",
|
|
8634
|
+
"instruction",
|
|
8635
|
+
"instructions",
|
|
8636
|
+
"objective",
|
|
8637
|
+
"goal",
|
|
8638
|
+
"current_step",
|
|
8639
|
+
"next_step",
|
|
8640
|
+
"remaining_steps",
|
|
8641
|
+
"constraints",
|
|
8642
|
+
"requirements",
|
|
8643
|
+
"output_format",
|
|
8644
|
+
"format",
|
|
8645
|
+
"tool_results",
|
|
8646
|
+
"tool_output",
|
|
8647
|
+
"tool_input",
|
|
8648
|
+
"tool_parameters"
|
|
8649
|
+
],
|
|
8650
|
+
action: "allow",
|
|
8651
|
+
confidence: "medium",
|
|
8652
|
+
reason: "Matches known task/query pattern \u2014 likely needed for inference"
|
|
8653
|
+
}
|
|
8654
|
+
];
|
|
8655
|
+
function classifyField(fieldName) {
|
|
8656
|
+
const normalized = fieldName.toLowerCase().trim();
|
|
8657
|
+
for (const rule of CLASSIFICATION_RULES) {
|
|
8658
|
+
for (const pattern of rule.patterns) {
|
|
8659
|
+
if (matchesFieldPattern(normalized, pattern)) {
|
|
8660
|
+
return {
|
|
8661
|
+
field: fieldName,
|
|
8662
|
+
recommended_action: rule.action,
|
|
8663
|
+
reason: rule.reason,
|
|
8664
|
+
confidence: rule.confidence,
|
|
8665
|
+
matched_pattern: pattern
|
|
8666
|
+
};
|
|
8667
|
+
}
|
|
8668
|
+
}
|
|
8669
|
+
}
|
|
8670
|
+
return {
|
|
8671
|
+
field: fieldName,
|
|
8672
|
+
recommended_action: "redact",
|
|
8673
|
+
reason: "No known pattern matched \u2014 defaulting to redact (conservative)",
|
|
8674
|
+
confidence: "low",
|
|
8675
|
+
matched_pattern: null
|
|
8676
|
+
};
|
|
8677
|
+
}
|
|
8678
|
+
function recommendPolicy(context, provider = "inference") {
|
|
8679
|
+
const fields = Object.keys(context);
|
|
8680
|
+
const classifications = fields.map(classifyField);
|
|
8681
|
+
const warnings = [];
|
|
8682
|
+
const allow = [];
|
|
8683
|
+
const redact = [];
|
|
8684
|
+
const hash2 = [];
|
|
8685
|
+
const summarize = [];
|
|
8686
|
+
for (const c of classifications) {
|
|
8687
|
+
switch (c.recommended_action) {
|
|
8688
|
+
case "allow":
|
|
8689
|
+
allow.push(c.field);
|
|
8690
|
+
break;
|
|
8691
|
+
case "redact":
|
|
8692
|
+
redact.push(c.field);
|
|
8693
|
+
break;
|
|
8694
|
+
case "hash":
|
|
8695
|
+
hash2.push(c.field);
|
|
8696
|
+
break;
|
|
8697
|
+
case "summarize":
|
|
8698
|
+
summarize.push(c.field);
|
|
8699
|
+
break;
|
|
8700
|
+
}
|
|
8701
|
+
}
|
|
8702
|
+
const lowConfidence = classifications.filter((c) => c.confidence === "low");
|
|
8703
|
+
if (lowConfidence.length > 0) {
|
|
8704
|
+
warnings.push(
|
|
8705
|
+
`${lowConfidence.length} field(s) could not be classified by pattern and will default to redact: ${lowConfidence.map((c) => c.field).join(", ")}. Review these manually.`
|
|
8706
|
+
);
|
|
8707
|
+
}
|
|
8708
|
+
for (const [key, value] of Object.entries(context)) {
|
|
8709
|
+
if (typeof value === "string" && value.length > 5e3) {
|
|
8710
|
+
const existing = classifications.find((c) => c.field === key);
|
|
8711
|
+
if (existing && existing.recommended_action === "allow") {
|
|
8712
|
+
warnings.push(
|
|
8713
|
+
`Field "${key}" is allowed but contains ${value.length} characters. Consider summarizing it to reduce context size and exposure.`
|
|
8714
|
+
);
|
|
8715
|
+
}
|
|
8716
|
+
}
|
|
8717
|
+
}
|
|
8718
|
+
return {
|
|
8719
|
+
provider,
|
|
8720
|
+
classifications,
|
|
8721
|
+
recommended_rules: { allow, redact, hash: hash2, summarize },
|
|
8722
|
+
default_action: "redact",
|
|
8723
|
+
summary: {
|
|
8724
|
+
total_fields: fields.length,
|
|
8725
|
+
allow: allow.length,
|
|
8726
|
+
redact: redact.length,
|
|
8727
|
+
hash: hash2.length,
|
|
8728
|
+
summarize: summarize.length
|
|
8729
|
+
},
|
|
8730
|
+
warnings
|
|
8731
|
+
};
|
|
8732
|
+
}
|
|
8733
|
+
function matchesFieldPattern(normalizedField, pattern) {
|
|
8734
|
+
if (normalizedField === pattern) return true;
|
|
8735
|
+
if (pattern.length >= 3 && normalizedField.includes(pattern)) {
|
|
8736
|
+
const idx = normalizedField.indexOf(pattern);
|
|
8737
|
+
const before = idx === 0 || normalizedField[idx - 1] === "_" || normalizedField[idx - 1] === "-";
|
|
8738
|
+
const after = idx + pattern.length === normalizedField.length || normalizedField[idx + pattern.length] === "_" || normalizedField[idx + pattern.length] === "-";
|
|
8739
|
+
return before && after;
|
|
8740
|
+
}
|
|
8741
|
+
return false;
|
|
8742
|
+
}
|
|
8743
|
+
|
|
8744
|
+
// src/l2-operational/context-gate-tools.ts
|
|
8745
|
+
function createContextGateTools(storage, masterKey, auditLog) {
|
|
8746
|
+
const policyStore = new ContextGatePolicyStore(storage, masterKey);
|
|
8747
|
+
const tools = [
|
|
8748
|
+
// ── Set Policy ──────────────────────────────────────────────────
|
|
8749
|
+
{
|
|
8750
|
+
name: "sanctuary/context_gate_set_policy",
|
|
8751
|
+
description: "Create a context-gating policy that controls what information flows to remote providers (LLM APIs, tool APIs, logging services). Each rule specifies a provider category and which context fields to allow, redact, hash, or flag for summarization. Redact rules take absolute priority \u2014 if a field is in both 'allow' and 'redact', it is redacted. Default action applies to any field not mentioned in any rule. Use this to prevent your full agent context from being sent to remote LLM providers during inference calls.",
|
|
8752
|
+
inputSchema: {
|
|
8753
|
+
type: "object",
|
|
8754
|
+
properties: {
|
|
8755
|
+
policy_name: {
|
|
8756
|
+
type: "string",
|
|
8757
|
+
description: "Human-readable name for this policy (e.g., 'inference-minimal', 'tool-api-strict')"
|
|
8758
|
+
},
|
|
8759
|
+
rules: {
|
|
8760
|
+
type: "array",
|
|
8761
|
+
description: "Array of rules. Each rule has: provider (inference|tool-api|logging|analytics|peer-agent|custom|*), allow (fields to pass through), redact (fields to remove \u2014 highest priority), hash (fields to replace with SHA-256 hash), summarize (fields to flag for compression).",
|
|
8762
|
+
items: {
|
|
8763
|
+
type: "object",
|
|
8764
|
+
properties: {
|
|
8765
|
+
provider: {
|
|
8766
|
+
type: "string",
|
|
8767
|
+
description: "Provider category: inference, tool-api, logging, analytics, peer-agent, custom, or * for all"
|
|
8768
|
+
},
|
|
8769
|
+
allow: {
|
|
8770
|
+
type: "array",
|
|
8771
|
+
items: { type: "string" },
|
|
8772
|
+
description: "Fields/patterns to allow through (e.g., 'task_description', 'current_query', 'tool_*')"
|
|
8773
|
+
},
|
|
8774
|
+
redact: {
|
|
8775
|
+
type: "array",
|
|
8776
|
+
items: { type: "string" },
|
|
8777
|
+
description: "Fields/patterns to redact (e.g., 'conversation_history', 'secret_*', '*_pii'). Takes absolute priority."
|
|
8778
|
+
},
|
|
8779
|
+
hash: {
|
|
8780
|
+
type: "array",
|
|
8781
|
+
items: { type: "string" },
|
|
8782
|
+
description: "Fields/patterns to replace with SHA-256 hash (e.g., 'user_id', 'session_id')"
|
|
8783
|
+
},
|
|
8784
|
+
summarize: {
|
|
8785
|
+
type: "array",
|
|
8786
|
+
items: { type: "string" },
|
|
8787
|
+
description: "Fields/patterns to flag for summarization (advisory \u2014 agent should compress these before sending)"
|
|
8788
|
+
}
|
|
8789
|
+
},
|
|
8790
|
+
required: ["provider", "allow", "redact"]
|
|
8791
|
+
}
|
|
8792
|
+
},
|
|
8793
|
+
default_action: {
|
|
8794
|
+
type: "string",
|
|
8795
|
+
enum: ["redact", "deny"],
|
|
8796
|
+
description: "Action for fields not matched by any rule. 'redact' removes the field value; 'deny' blocks the entire request. Default: 'redact'."
|
|
8797
|
+
},
|
|
8798
|
+
identity_id: {
|
|
8799
|
+
type: "string",
|
|
8800
|
+
description: "Bind this policy to a specific identity (optional)"
|
|
8801
|
+
}
|
|
8802
|
+
},
|
|
8803
|
+
required: ["policy_name", "rules"]
|
|
8804
|
+
},
|
|
8805
|
+
handler: async (args) => {
|
|
8806
|
+
const policyName = args.policy_name;
|
|
8807
|
+
const rawRules = args.rules;
|
|
8808
|
+
const defaultAction = args.default_action ?? "redact";
|
|
8809
|
+
const identityId = args.identity_id;
|
|
8810
|
+
if (!Array.isArray(rawRules)) {
|
|
8811
|
+
return toolResult({ error: "invalid_rules", message: "rules must be an array" });
|
|
8812
|
+
}
|
|
8813
|
+
if (rawRules.length > MAX_POLICY_RULES) {
|
|
8814
|
+
return toolResult({
|
|
8815
|
+
error: "too_many_rules",
|
|
8816
|
+
message: `Policy has ${rawRules.length} rules, exceeding limit of ${MAX_POLICY_RULES}`
|
|
8817
|
+
});
|
|
8818
|
+
}
|
|
8819
|
+
const rules = [];
|
|
8820
|
+
for (const r of rawRules) {
|
|
8821
|
+
const allow = Array.isArray(r.allow) ? r.allow : [];
|
|
8822
|
+
const redact = Array.isArray(r.redact) ? r.redact : [];
|
|
8823
|
+
const hash2 = Array.isArray(r.hash) ? r.hash : [];
|
|
8824
|
+
const summarize = Array.isArray(r.summarize) ? r.summarize : [];
|
|
8825
|
+
for (const [name, arr] of [["allow", allow], ["redact", redact], ["hash", hash2], ["summarize", summarize]]) {
|
|
8826
|
+
if (arr.length > MAX_PATTERNS_PER_ARRAY) {
|
|
8827
|
+
return toolResult({
|
|
8828
|
+
error: "too_many_patterns",
|
|
8829
|
+
message: `Rule ${name} array has ${arr.length} patterns, exceeding limit of ${MAX_PATTERNS_PER_ARRAY}`
|
|
8830
|
+
});
|
|
8831
|
+
}
|
|
8832
|
+
}
|
|
8833
|
+
rules.push({
|
|
8834
|
+
provider: r.provider ?? "*",
|
|
8835
|
+
allow,
|
|
8836
|
+
redact,
|
|
8837
|
+
hash: hash2,
|
|
8838
|
+
summarize
|
|
8839
|
+
});
|
|
8840
|
+
}
|
|
8841
|
+
const policy = await policyStore.create(
|
|
8842
|
+
policyName,
|
|
8843
|
+
rules,
|
|
8844
|
+
defaultAction,
|
|
8845
|
+
identityId
|
|
8846
|
+
);
|
|
8847
|
+
auditLog.append("l2", "context_gate_set_policy", identityId ?? "system", {
|
|
8848
|
+
policy_id: policy.policy_id,
|
|
8849
|
+
policy_name: policyName,
|
|
8850
|
+
rule_count: rules.length,
|
|
8851
|
+
default_action: defaultAction
|
|
8852
|
+
});
|
|
8853
|
+
return toolResult({
|
|
8854
|
+
policy_id: policy.policy_id,
|
|
8855
|
+
policy_name: policy.policy_name,
|
|
8856
|
+
rules: policy.rules,
|
|
8857
|
+
default_action: policy.default_action,
|
|
8858
|
+
created_at: policy.created_at,
|
|
8859
|
+
message: "Context-gating policy created. Use sanctuary/context_gate_filter to apply this policy before making outbound calls."
|
|
8860
|
+
});
|
|
8861
|
+
}
|
|
8862
|
+
},
|
|
8863
|
+
// ── Apply Template ───────────────────────────────────────────────
|
|
8864
|
+
{
|
|
8865
|
+
name: "sanctuary/context_gate_apply_template",
|
|
8866
|
+
description: "Apply a starter context-gating template. Available templates: inference-minimal (strictest \u2014 only task and query pass through), inference-standard (balanced \u2014 adds tool results, summarizes history), logging-strict (redacts all content for telemetry services), tool-api-scoped (allows tool parameters, redacts agent state). Templates are starting points \u2014 customize after applying.",
|
|
8867
|
+
inputSchema: {
|
|
8868
|
+
type: "object",
|
|
8869
|
+
properties: {
|
|
8870
|
+
template_id: {
|
|
8871
|
+
type: "string",
|
|
8872
|
+
description: "Template to apply: inference-minimal, inference-standard, logging-strict, or tool-api-scoped"
|
|
8873
|
+
},
|
|
8874
|
+
identity_id: {
|
|
8875
|
+
type: "string",
|
|
8876
|
+
description: "Bind this policy to a specific identity (optional)"
|
|
8877
|
+
}
|
|
8878
|
+
},
|
|
8879
|
+
required: ["template_id"]
|
|
8880
|
+
},
|
|
8881
|
+
handler: async (args) => {
|
|
8882
|
+
const templateId = args.template_id;
|
|
8883
|
+
const identityId = args.identity_id;
|
|
8884
|
+
const template = getTemplate(templateId);
|
|
8885
|
+
if (!template) {
|
|
8886
|
+
return toolResult({
|
|
8887
|
+
error: "template_not_found",
|
|
8888
|
+
message: `Unknown template "${templateId}"`,
|
|
8889
|
+
available_templates: listTemplateIds().map((id) => {
|
|
8890
|
+
const t = TEMPLATES[id];
|
|
8891
|
+
return { id, name: t.name, description: t.description };
|
|
8892
|
+
})
|
|
8893
|
+
});
|
|
8894
|
+
}
|
|
8895
|
+
const policy = await policyStore.create(
|
|
8896
|
+
template.name,
|
|
8897
|
+
template.rules,
|
|
8898
|
+
template.default_action,
|
|
8899
|
+
identityId
|
|
8900
|
+
);
|
|
8901
|
+
auditLog.append("l2", "context_gate_apply_template", identityId ?? "system", {
|
|
8902
|
+
policy_id: policy.policy_id,
|
|
8903
|
+
template_id: templateId
|
|
8904
|
+
});
|
|
8905
|
+
return toolResult({
|
|
8906
|
+
policy_id: policy.policy_id,
|
|
8907
|
+
template_applied: templateId,
|
|
8908
|
+
policy_name: template.name,
|
|
8909
|
+
description: template.description,
|
|
8910
|
+
use_when: template.use_when,
|
|
8911
|
+
rules: policy.rules,
|
|
8912
|
+
default_action: policy.default_action,
|
|
8913
|
+
created_at: policy.created_at,
|
|
8914
|
+
message: "Template applied. Use sanctuary/context_gate_filter with this policy_id to filter context before outbound calls. Customize rules with sanctuary/context_gate_set_policy if needed."
|
|
8915
|
+
});
|
|
8916
|
+
}
|
|
8917
|
+
},
|
|
8918
|
+
// ── Recommend Policy ────────────────────────────────────────────
|
|
8919
|
+
{
|
|
8920
|
+
name: "sanctuary/context_gate_recommend",
|
|
8921
|
+
description: "Analyze a sample context object and recommend a context-gating policy based on field name heuristics. Classifies each field as allow, redact, hash, or summarize with confidence levels. Returns a ready-to-apply rule set. When in doubt, recommends redact (conservative). Review the recommendations before applying.",
|
|
8922
|
+
inputSchema: {
|
|
8923
|
+
type: "object",
|
|
8924
|
+
properties: {
|
|
8925
|
+
context: {
|
|
8926
|
+
type: "object",
|
|
8927
|
+
description: "A sample context object to analyze. Each top-level key will be classified. Values are inspected for size warnings but not stored."
|
|
8928
|
+
},
|
|
8929
|
+
provider: {
|
|
8930
|
+
type: "string",
|
|
8931
|
+
description: "Provider category to generate rules for. Default: 'inference'."
|
|
8932
|
+
}
|
|
8933
|
+
},
|
|
8934
|
+
required: ["context"]
|
|
8935
|
+
},
|
|
8936
|
+
handler: async (args) => {
|
|
8937
|
+
const context = args.context;
|
|
8938
|
+
const provider = args.provider ?? "inference";
|
|
8939
|
+
const contextKeys = Object.keys(context);
|
|
8940
|
+
if (contextKeys.length > MAX_CONTEXT_FIELDS) {
|
|
8941
|
+
return toolResult({
|
|
8942
|
+
error: "context_too_large",
|
|
8943
|
+
message: `Context has ${contextKeys.length} fields, exceeding limit of ${MAX_CONTEXT_FIELDS}`
|
|
8944
|
+
});
|
|
8945
|
+
}
|
|
8946
|
+
const recommendation = recommendPolicy(context, provider);
|
|
8947
|
+
auditLog.append("l2", "context_gate_recommend", "system", {
|
|
8948
|
+
provider,
|
|
8949
|
+
fields_analyzed: recommendation.summary.total_fields,
|
|
8950
|
+
fields_allow: recommendation.summary.allow,
|
|
8951
|
+
fields_redact: recommendation.summary.redact,
|
|
8952
|
+
fields_hash: recommendation.summary.hash,
|
|
8953
|
+
fields_summarize: recommendation.summary.summarize
|
|
8954
|
+
});
|
|
8955
|
+
return toolResult({
|
|
8956
|
+
...recommendation,
|
|
8957
|
+
next_steps: "Review the classifications above. If they look correct, you can apply them directly with sanctuary/context_gate_set_policy using the recommended_rules. Or start with a template via sanctuary/context_gate_apply_template and customize from there.",
|
|
8958
|
+
available_templates: listTemplateIds().map((id) => {
|
|
8959
|
+
const t = TEMPLATES[id];
|
|
8960
|
+
return { id, name: t.name, description: t.description };
|
|
8961
|
+
})
|
|
8962
|
+
});
|
|
8963
|
+
}
|
|
8964
|
+
},
|
|
8965
|
+
// ── Filter Context ──────────────────────────────────────────────
|
|
8966
|
+
{
|
|
8967
|
+
name: "sanctuary/context_gate_filter",
|
|
8968
|
+
description: "Filter agent context through a gating policy before sending to a remote provider. Returns per-field decisions (allow, redact, hash, summarize) and content hashes for the audit trail. Call this BEFORE making any outbound API call to ensure you are only sending the minimum necessary context. The filtered output tells you exactly what can be sent safely.",
|
|
8969
|
+
inputSchema: {
|
|
8970
|
+
type: "object",
|
|
8971
|
+
properties: {
|
|
8972
|
+
policy_id: {
|
|
8973
|
+
type: "string",
|
|
8974
|
+
description: "ID of the context-gating policy to apply"
|
|
8975
|
+
},
|
|
8976
|
+
provider: {
|
|
8977
|
+
type: "string",
|
|
8978
|
+
description: "Provider category for this call: inference, tool-api, logging, analytics, peer-agent, or custom"
|
|
8979
|
+
},
|
|
8980
|
+
context: {
|
|
8981
|
+
type: "object",
|
|
8982
|
+
description: "The context object to filter. Each top-level key is evaluated against the policy. Example keys: task_description, conversation_history, user_preferences, api_keys, memory, internal_reasoning"
|
|
8983
|
+
}
|
|
8984
|
+
},
|
|
8985
|
+
required: ["policy_id", "provider", "context"]
|
|
8986
|
+
},
|
|
8987
|
+
handler: async (args) => {
|
|
8988
|
+
const policyId = args.policy_id;
|
|
8989
|
+
const provider = args.provider;
|
|
8990
|
+
const context = args.context;
|
|
8991
|
+
const contextKeys = Object.keys(context);
|
|
8992
|
+
if (contextKeys.length > MAX_CONTEXT_FIELDS) {
|
|
8993
|
+
return toolResult({
|
|
8994
|
+
error: "context_too_large",
|
|
8995
|
+
message: `Context has ${contextKeys.length} fields, exceeding limit of ${MAX_CONTEXT_FIELDS}`
|
|
8996
|
+
});
|
|
8997
|
+
}
|
|
8998
|
+
const policy = await policyStore.get(policyId);
|
|
8999
|
+
if (!policy) {
|
|
9000
|
+
return toolResult({
|
|
9001
|
+
error: "policy_not_found",
|
|
9002
|
+
message: `No context-gating policy found with ID "${policyId}"`
|
|
9003
|
+
});
|
|
9004
|
+
}
|
|
9005
|
+
const result = filterContext(policy, provider, context);
|
|
9006
|
+
const deniedFields = result.decisions.filter((d) => d.action === "deny");
|
|
9007
|
+
if (deniedFields.length > 0) {
|
|
9008
|
+
auditLog.append("l2", "context_gate_deny", policy.identity_id ?? "system", {
|
|
9009
|
+
policy_id: policyId,
|
|
9010
|
+
provider,
|
|
9011
|
+
denied_fields: deniedFields.map((d) => d.field),
|
|
9012
|
+
original_context_hash: result.original_context_hash
|
|
9013
|
+
});
|
|
9014
|
+
return toolResult({
|
|
9015
|
+
blocked: true,
|
|
9016
|
+
reason: "Context contains fields that trigger deny action",
|
|
9017
|
+
denied_fields: deniedFields.map((d) => ({
|
|
9018
|
+
field: d.field,
|
|
9019
|
+
reason: d.reason
|
|
9020
|
+
})),
|
|
9021
|
+
recommendation: "Remove the denied fields from context before retrying, or update the policy to handle these fields differently."
|
|
9022
|
+
});
|
|
9023
|
+
}
|
|
9024
|
+
const safeContext = {};
|
|
9025
|
+
for (const decision of result.decisions) {
|
|
9026
|
+
switch (decision.action) {
|
|
9027
|
+
case "allow":
|
|
9028
|
+
safeContext[decision.field] = context[decision.field];
|
|
9029
|
+
break;
|
|
9030
|
+
case "redact":
|
|
9031
|
+
break;
|
|
9032
|
+
case "hash":
|
|
9033
|
+
safeContext[decision.field] = decision.hash_value;
|
|
9034
|
+
break;
|
|
9035
|
+
case "summarize":
|
|
9036
|
+
safeContext[decision.field] = context[decision.field];
|
|
9037
|
+
break;
|
|
9038
|
+
}
|
|
9039
|
+
}
|
|
9040
|
+
auditLog.append("l2", "context_gate_filter", policy.identity_id ?? "system", {
|
|
9041
|
+
policy_id: policyId,
|
|
9042
|
+
provider,
|
|
9043
|
+
fields_total: Object.keys(context).length,
|
|
9044
|
+
fields_allowed: result.fields_allowed,
|
|
9045
|
+
fields_redacted: result.fields_redacted,
|
|
9046
|
+
fields_hashed: result.fields_hashed,
|
|
9047
|
+
fields_summarized: result.fields_summarized,
|
|
9048
|
+
original_context_hash: result.original_context_hash,
|
|
9049
|
+
filtered_context_hash: result.filtered_context_hash
|
|
9050
|
+
});
|
|
9051
|
+
return toolResult({
|
|
9052
|
+
blocked: false,
|
|
9053
|
+
safe_context: safeContext,
|
|
9054
|
+
summary: {
|
|
9055
|
+
total_fields: Object.keys(context).length,
|
|
9056
|
+
allowed: result.fields_allowed,
|
|
9057
|
+
redacted: result.fields_redacted,
|
|
9058
|
+
hashed: result.fields_hashed,
|
|
9059
|
+
summarized: result.fields_summarized
|
|
9060
|
+
},
|
|
9061
|
+
decisions: result.decisions,
|
|
9062
|
+
audit: {
|
|
9063
|
+
original_context_hash: result.original_context_hash,
|
|
9064
|
+
filtered_context_hash: result.filtered_context_hash,
|
|
9065
|
+
filtered_at: result.filtered_at
|
|
9066
|
+
},
|
|
9067
|
+
guidance: result.fields_summarized > 0 ? "Some fields are marked for summarization. Consider compressing them before sending to reduce context size and information exposure." : void 0
|
|
9068
|
+
});
|
|
9069
|
+
}
|
|
9070
|
+
},
|
|
9071
|
+
// ── List Policies ───────────────────────────────────────────────
|
|
9072
|
+
{
|
|
9073
|
+
name: "sanctuary/context_gate_list_policies",
|
|
9074
|
+
description: "List all configured context-gating policies. Returns policy IDs, names, rule summaries, and default actions.",
|
|
9075
|
+
inputSchema: {
|
|
9076
|
+
type: "object",
|
|
9077
|
+
properties: {}
|
|
9078
|
+
},
|
|
9079
|
+
handler: async () => {
|
|
9080
|
+
const policies = await policyStore.list();
|
|
9081
|
+
auditLog.append("l2", "context_gate_list_policies", "system", {
|
|
9082
|
+
policy_count: policies.length
|
|
9083
|
+
});
|
|
9084
|
+
return toolResult({
|
|
9085
|
+
policies: policies.map((p) => ({
|
|
9086
|
+
policy_id: p.policy_id,
|
|
9087
|
+
policy_name: p.policy_name,
|
|
9088
|
+
rule_count: p.rules.length,
|
|
9089
|
+
providers: p.rules.map((r) => r.provider),
|
|
9090
|
+
default_action: p.default_action,
|
|
9091
|
+
identity_id: p.identity_id ?? null,
|
|
9092
|
+
created_at: p.created_at,
|
|
9093
|
+
updated_at: p.updated_at
|
|
9094
|
+
})),
|
|
9095
|
+
count: policies.length,
|
|
9096
|
+
message: policies.length === 0 ? "No context-gating policies configured. Use sanctuary/context_gate_set_policy to create one." : `${policies.length} context-gating ${policies.length === 1 ? "policy" : "policies"} configured.`
|
|
9097
|
+
});
|
|
9098
|
+
}
|
|
9099
|
+
}
|
|
9100
|
+
];
|
|
9101
|
+
return { tools, policyStore };
|
|
9102
|
+
}
|
|
9103
|
+
function checkMemoryProtection() {
|
|
9104
|
+
const checks = {
|
|
9105
|
+
aslr_enabled: checkASLR(),
|
|
9106
|
+
stack_canaries: true,
|
|
9107
|
+
// Enabled by default in Node.js runtime
|
|
9108
|
+
secure_buffer_zeros: true,
|
|
9109
|
+
// We use crypto.randomBytes and explicit zeroing
|
|
9110
|
+
argon2id_kdf: true
|
|
9111
|
+
// Master key derivation uses Argon2id
|
|
9112
|
+
};
|
|
9113
|
+
const activeCount = Object.values(checks).filter((v) => v).length;
|
|
9114
|
+
const overall = activeCount >= 4 ? "full" : activeCount >= 3 ? "partial" : "minimal";
|
|
9115
|
+
return {
|
|
9116
|
+
...checks,
|
|
9117
|
+
overall
|
|
9118
|
+
};
|
|
9119
|
+
}
|
|
9120
|
+
function checkASLR() {
|
|
9121
|
+
if (process.platform === "linux") {
|
|
9122
|
+
try {
|
|
9123
|
+
const result = child_process.execSync("cat /proc/sys/kernel/randomize_va_space", {
|
|
9124
|
+
encoding: "utf-8",
|
|
9125
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
9126
|
+
}).trim();
|
|
9127
|
+
return result === "2";
|
|
9128
|
+
} catch {
|
|
9129
|
+
return false;
|
|
9130
|
+
}
|
|
9131
|
+
}
|
|
9132
|
+
if (process.platform === "darwin") {
|
|
9133
|
+
return true;
|
|
9134
|
+
}
|
|
9135
|
+
return false;
|
|
9136
|
+
}
|
|
9137
|
+
function checkProcessIsolation() {
|
|
9138
|
+
const isContainer = detectContainer();
|
|
9139
|
+
const isVM = detectVM();
|
|
9140
|
+
const isSandboxed = detectSandbox();
|
|
9141
|
+
let isolationLevel = "none";
|
|
9142
|
+
if (isContainer) isolationLevel = "hardened";
|
|
9143
|
+
else if (isVM) isolationLevel = "hardened";
|
|
9144
|
+
else if (isSandboxed) isolationLevel = "basic";
|
|
9145
|
+
const details = {};
|
|
9146
|
+
if (isContainer && isContainer !== true) details.container_type = isContainer;
|
|
9147
|
+
if (isVM && isVM !== true) details.vm_type = isVM;
|
|
9148
|
+
if (isSandboxed && isSandboxed !== true) details.sandbox_type = isSandboxed;
|
|
9149
|
+
return {
|
|
9150
|
+
isolation_level: isolationLevel,
|
|
9151
|
+
is_container: isContainer !== false,
|
|
9152
|
+
is_vm: isVM !== false,
|
|
9153
|
+
is_sandboxed: isSandboxed !== false,
|
|
9154
|
+
is_tee: false,
|
|
9155
|
+
details
|
|
9156
|
+
};
|
|
9157
|
+
}
|
|
9158
|
+
function detectContainer() {
|
|
9159
|
+
try {
|
|
9160
|
+
if (process.env.DOCKER_HOST) return "docker";
|
|
9161
|
+
try {
|
|
9162
|
+
fs.statSync("/.dockerenv");
|
|
9163
|
+
return "docker";
|
|
9164
|
+
} catch {
|
|
9165
|
+
}
|
|
9166
|
+
if (process.platform === "linux") {
|
|
9167
|
+
const cgroup = child_process.execSync("cat /proc/1/cgroup 2>/dev/null || echo ''", {
|
|
9168
|
+
encoding: "utf-8"
|
|
9169
|
+
});
|
|
9170
|
+
if (cgroup.includes("docker")) return "docker";
|
|
9171
|
+
if (cgroup.includes("lxc")) return "lxc";
|
|
9172
|
+
if (cgroup.includes("kubepods") || cgroup.includes("kubernetes")) return "kubernetes";
|
|
9173
|
+
}
|
|
9174
|
+
if (process.env.container === "podman") return "podman";
|
|
9175
|
+
if (process.env.CONTAINER_ID) return "oci";
|
|
9176
|
+
return false;
|
|
9177
|
+
} catch {
|
|
9178
|
+
return false;
|
|
9179
|
+
}
|
|
9180
|
+
}
|
|
9181
|
+
function detectVM() {
|
|
9182
|
+
if (process.platform === "linux") {
|
|
9183
|
+
try {
|
|
9184
|
+
const dmidecode = child_process.execSync("dmidecode -s system-product-name 2>/dev/null || echo ''", {
|
|
9185
|
+
encoding: "utf-8"
|
|
9186
|
+
}).toLowerCase();
|
|
9187
|
+
if (dmidecode.includes("vmware")) return "vmware";
|
|
9188
|
+
if (dmidecode.includes("virtualbox")) return "virtualbox";
|
|
9189
|
+
if (dmidecode.includes("kvm")) return "kvm";
|
|
9190
|
+
if (dmidecode.includes("xen")) return "xen";
|
|
9191
|
+
if (dmidecode.includes("hyper-v")) return "hyper-v";
|
|
9192
|
+
const cpuinfo = child_process.execSync("grep -i hypervisor /proc/cpuinfo || echo ''", {
|
|
9193
|
+
encoding: "utf-8"
|
|
9194
|
+
});
|
|
9195
|
+
if (cpuinfo.length > 0) return "detected";
|
|
9196
|
+
} catch {
|
|
9197
|
+
}
|
|
9198
|
+
}
|
|
9199
|
+
if (process.platform === "darwin") {
|
|
9200
|
+
try {
|
|
9201
|
+
const bootargs = child_process.execSync(
|
|
9202
|
+
"nvram boot-args 2>/dev/null | grep -i 'parallels\\|vmware\\|virtualbox' || echo ''",
|
|
9203
|
+
{
|
|
9204
|
+
encoding: "utf-8"
|
|
9205
|
+
}
|
|
9206
|
+
);
|
|
9207
|
+
if (bootargs.length > 0) return "detected";
|
|
9208
|
+
} catch {
|
|
9209
|
+
}
|
|
9210
|
+
}
|
|
9211
|
+
return false;
|
|
9212
|
+
}
|
|
9213
|
+
function detectSandbox() {
|
|
9214
|
+
if (process.platform === "darwin") {
|
|
9215
|
+
if (process.env.APP_SANDBOX_READ_ONLY_HOME === "1") return "app-sandbox";
|
|
9216
|
+
if (process.env.TMPDIR && process.env.TMPDIR.includes("AppSandbox")) return "app-sandbox";
|
|
9217
|
+
}
|
|
9218
|
+
if (process.platform === "openbsd") {
|
|
9219
|
+
try {
|
|
9220
|
+
const pledge = child_process.execSync("pledge -v 2>/dev/null || echo ''", {
|
|
9221
|
+
encoding: "utf-8"
|
|
9222
|
+
});
|
|
9223
|
+
if (pledge.length > 0) return "pledge";
|
|
9224
|
+
} catch {
|
|
9225
|
+
}
|
|
9226
|
+
}
|
|
9227
|
+
if (process.platform === "linux") {
|
|
9228
|
+
if (process.env.container === "lxc") return "lxc";
|
|
9229
|
+
try {
|
|
9230
|
+
const context = child_process.execSync("getenforce 2>/dev/null || echo ''", {
|
|
9231
|
+
encoding: "utf-8"
|
|
9232
|
+
}).trim();
|
|
9233
|
+
if (context === "Enforcing") return "selinux";
|
|
9234
|
+
} catch {
|
|
9235
|
+
}
|
|
9236
|
+
}
|
|
9237
|
+
return false;
|
|
9238
|
+
}
|
|
9239
|
+
function checkFilesystemPermissions(storagePath) {
|
|
9240
|
+
try {
|
|
9241
|
+
const stats = fs.statSync(storagePath);
|
|
9242
|
+
const mode = stats.mode & parseInt("777", 8);
|
|
9243
|
+
const modeString = mode.toString(8).padStart(3, "0");
|
|
9244
|
+
const isSecure = mode === parseInt("700", 8);
|
|
9245
|
+
const groupReadable = (mode & parseInt("040", 8)) !== 0;
|
|
9246
|
+
const othersReadable = (mode & parseInt("007", 8)) !== 0;
|
|
9247
|
+
const currentUid = process.getuid?.() || -1;
|
|
9248
|
+
const ownerIsCurrentUser = stats.uid === currentUid;
|
|
9249
|
+
let overall = "secure";
|
|
9250
|
+
if (groupReadable || othersReadable) overall = "insecure";
|
|
9251
|
+
else if (!ownerIsCurrentUser) overall = "warning";
|
|
9252
|
+
return {
|
|
9253
|
+
sanctuary_storage_protected: isSecure,
|
|
9254
|
+
sanctuary_storage_mode: modeString,
|
|
9255
|
+
owner_is_current_user: ownerIsCurrentUser,
|
|
9256
|
+
group_readable: groupReadable,
|
|
9257
|
+
others_readable: othersReadable,
|
|
9258
|
+
overall
|
|
9259
|
+
};
|
|
9260
|
+
} catch {
|
|
9261
|
+
return {
|
|
9262
|
+
sanctuary_storage_protected: false,
|
|
9263
|
+
sanctuary_storage_mode: "unknown",
|
|
9264
|
+
owner_is_current_user: false,
|
|
9265
|
+
group_readable: false,
|
|
9266
|
+
others_readable: false,
|
|
9267
|
+
overall: "warning"
|
|
9268
|
+
};
|
|
9269
|
+
}
|
|
9270
|
+
}
|
|
9271
|
+
function checkRuntimeIntegrity() {
|
|
9272
|
+
return {
|
|
9273
|
+
config_hash_stable: true,
|
|
9274
|
+
environment_state: "clean",
|
|
9275
|
+
discrepancies: []
|
|
9276
|
+
};
|
|
9277
|
+
}
|
|
9278
|
+
function assessL2Hardening(storagePath) {
|
|
9279
|
+
const memory = checkMemoryProtection();
|
|
9280
|
+
const isolation = checkProcessIsolation();
|
|
9281
|
+
const filesystem = checkFilesystemPermissions(storagePath);
|
|
9282
|
+
const integrity = checkRuntimeIntegrity();
|
|
9283
|
+
let checksPassed = 0;
|
|
9284
|
+
let checksTotal = 0;
|
|
9285
|
+
if (memory.aslr_enabled) checksPassed++;
|
|
9286
|
+
checksTotal++;
|
|
9287
|
+
if (memory.stack_canaries) checksPassed++;
|
|
9288
|
+
checksTotal++;
|
|
9289
|
+
if (memory.secure_buffer_zeros) checksPassed++;
|
|
9290
|
+
checksTotal++;
|
|
9291
|
+
if (memory.argon2id_kdf) checksPassed++;
|
|
9292
|
+
checksTotal++;
|
|
9293
|
+
if (isolation.is_container) checksPassed++;
|
|
9294
|
+
checksTotal++;
|
|
9295
|
+
if (isolation.is_vm) checksPassed++;
|
|
9296
|
+
checksTotal++;
|
|
9297
|
+
if (isolation.is_sandboxed) checksPassed++;
|
|
9298
|
+
checksTotal++;
|
|
9299
|
+
if (filesystem.sanctuary_storage_protected) checksPassed++;
|
|
9300
|
+
checksTotal++;
|
|
9301
|
+
{
|
|
9302
|
+
checksPassed++;
|
|
9303
|
+
}
|
|
9304
|
+
checksTotal++;
|
|
9305
|
+
let hardeningLevel = isolation.isolation_level;
|
|
9306
|
+
if (filesystem.overall === "insecure" || memory.overall === "none" || memory.overall === "minimal") {
|
|
9307
|
+
if (hardeningLevel === "hardened") {
|
|
9308
|
+
hardeningLevel = "basic";
|
|
9309
|
+
} else if (hardeningLevel === "basic") {
|
|
9310
|
+
hardeningLevel = "none";
|
|
9311
|
+
}
|
|
9312
|
+
}
|
|
9313
|
+
const summaryParts = [];
|
|
9314
|
+
if (isolation.is_container || isolation.is_vm) {
|
|
9315
|
+
summaryParts.push(`Running in ${isolation.details.container_type || isolation.details.vm_type || "isolated environment"}`);
|
|
9316
|
+
}
|
|
9317
|
+
if (memory.aslr_enabled) {
|
|
9318
|
+
summaryParts.push("ASLR enabled");
|
|
9319
|
+
}
|
|
9320
|
+
if (filesystem.sanctuary_storage_protected) {
|
|
9321
|
+
summaryParts.push("Storage permissions secured (0700)");
|
|
9322
|
+
}
|
|
9323
|
+
const summary = summaryParts.length > 0 ? summaryParts.join("; ") : "No process-level hardening detected";
|
|
9324
|
+
return {
|
|
9325
|
+
hardening_level: hardeningLevel,
|
|
9326
|
+
memory_protection: memory,
|
|
9327
|
+
process_isolation: isolation,
|
|
9328
|
+
filesystem_permissions: filesystem,
|
|
9329
|
+
runtime_integrity: integrity,
|
|
9330
|
+
checks_passed: checksPassed,
|
|
9331
|
+
checks_total: checksTotal,
|
|
9332
|
+
summary
|
|
9333
|
+
};
|
|
9334
|
+
}
|
|
9335
|
+
|
|
9336
|
+
// src/l2-operational/hardening-tools.ts
|
|
9337
|
+
function createL2HardeningTools(storagePath, auditLog) {
|
|
9338
|
+
return [
|
|
9339
|
+
{
|
|
9340
|
+
name: "sanctuary/l2_hardening_status",
|
|
9341
|
+
description: "L2 Process Hardening Status \u2014 Verify software-based operational isolation. Reports memory protection, process isolation level, filesystem permissions, and overall hardening assessment. Read-only. Tier 3 \u2014 always allowed.",
|
|
9342
|
+
inputSchema: {
|
|
9343
|
+
type: "object",
|
|
9344
|
+
properties: {
|
|
9345
|
+
include_details: {
|
|
9346
|
+
type: "boolean",
|
|
9347
|
+
description: "If true, include detailed check results for memory, process, and filesystem. If false, show summary only.",
|
|
9348
|
+
default: false
|
|
9349
|
+
}
|
|
9350
|
+
}
|
|
9351
|
+
},
|
|
9352
|
+
handler: async (args) => {
|
|
9353
|
+
const includeDetails = args.include_details ?? false;
|
|
9354
|
+
const status = assessL2Hardening(storagePath);
|
|
9355
|
+
auditLog.append(
|
|
9356
|
+
"l2",
|
|
9357
|
+
"l2_hardening_status",
|
|
9358
|
+
"system",
|
|
9359
|
+
{ include_details: includeDetails }
|
|
9360
|
+
);
|
|
9361
|
+
if (includeDetails) {
|
|
9362
|
+
return toolResult({
|
|
9363
|
+
hardening_level: status.hardening_level,
|
|
9364
|
+
summary: status.summary,
|
|
9365
|
+
checks_passed: status.checks_passed,
|
|
9366
|
+
checks_total: status.checks_total,
|
|
9367
|
+
memory_protection: {
|
|
9368
|
+
aslr_enabled: status.memory_protection.aslr_enabled,
|
|
9369
|
+
stack_canaries: status.memory_protection.stack_canaries,
|
|
9370
|
+
secure_buffer_zeros: status.memory_protection.secure_buffer_zeros,
|
|
9371
|
+
argon2id_kdf: status.memory_protection.argon2id_kdf,
|
|
9372
|
+
overall: status.memory_protection.overall
|
|
9373
|
+
},
|
|
9374
|
+
process_isolation: {
|
|
9375
|
+
isolation_level: status.process_isolation.isolation_level,
|
|
9376
|
+
is_container: status.process_isolation.is_container,
|
|
9377
|
+
is_vm: status.process_isolation.is_vm,
|
|
9378
|
+
is_sandboxed: status.process_isolation.is_sandboxed,
|
|
9379
|
+
is_tee: status.process_isolation.is_tee,
|
|
9380
|
+
details: status.process_isolation.details
|
|
9381
|
+
},
|
|
9382
|
+
filesystem_permissions: {
|
|
9383
|
+
sanctuary_storage_protected: status.filesystem_permissions.sanctuary_storage_protected,
|
|
9384
|
+
sanctuary_storage_mode: status.filesystem_permissions.sanctuary_storage_mode,
|
|
9385
|
+
owner_is_current_user: status.filesystem_permissions.owner_is_current_user,
|
|
9386
|
+
group_readable: status.filesystem_permissions.group_readable,
|
|
9387
|
+
others_readable: status.filesystem_permissions.others_readable,
|
|
9388
|
+
overall: status.filesystem_permissions.overall
|
|
9389
|
+
},
|
|
9390
|
+
runtime_integrity: {
|
|
9391
|
+
config_hash_stable: status.runtime_integrity.config_hash_stable,
|
|
9392
|
+
environment_state: status.runtime_integrity.environment_state,
|
|
9393
|
+
discrepancies: status.runtime_integrity.discrepancies
|
|
9394
|
+
}
|
|
9395
|
+
});
|
|
9396
|
+
} else {
|
|
9397
|
+
return toolResult({
|
|
9398
|
+
hardening_level: status.hardening_level,
|
|
9399
|
+
summary: status.summary,
|
|
9400
|
+
checks_passed: status.checks_passed,
|
|
9401
|
+
checks_total: status.checks_total,
|
|
9402
|
+
note: "Pass include_details: true to see full breakdown of memory, process isolation, and filesystem checks."
|
|
9403
|
+
});
|
|
9404
|
+
}
|
|
9405
|
+
}
|
|
9406
|
+
},
|
|
9407
|
+
{
|
|
9408
|
+
name: "sanctuary/l2_verify_isolation",
|
|
9409
|
+
description: "Verify L2 process isolation at runtime. Checks whether the Sanctuary server is running in an isolated environment (container, VM, sandbox) and validates filesystem and memory protections. Reports isolation level and any issues. Read-only. Tier 3 \u2014 always allowed.",
|
|
9410
|
+
inputSchema: {
|
|
9411
|
+
type: "object",
|
|
9412
|
+
properties: {
|
|
9413
|
+
check_filesystem: {
|
|
9414
|
+
type: "boolean",
|
|
9415
|
+
description: "If true, verify Sanctuary storage directory permissions.",
|
|
9416
|
+
default: true
|
|
9417
|
+
},
|
|
9418
|
+
check_memory: {
|
|
9419
|
+
type: "boolean",
|
|
9420
|
+
description: "If true, verify memory protection mechanisms (ASLR, etc.).",
|
|
9421
|
+
default: true
|
|
9422
|
+
},
|
|
9423
|
+
check_process: {
|
|
9424
|
+
type: "boolean",
|
|
9425
|
+
description: "If true, detect container, VM, or sandbox environment.",
|
|
9426
|
+
default: true
|
|
9427
|
+
}
|
|
9428
|
+
}
|
|
9429
|
+
},
|
|
9430
|
+
handler: async (args) => {
|
|
9431
|
+
const checkFilesystem = args.check_filesystem ?? true;
|
|
9432
|
+
const checkMemory = args.check_memory ?? true;
|
|
9433
|
+
const checkProcess = args.check_process ?? true;
|
|
9434
|
+
const status = assessL2Hardening(storagePath);
|
|
9435
|
+
auditLog.append(
|
|
9436
|
+
"l2",
|
|
9437
|
+
"l2_verify_isolation",
|
|
9438
|
+
"system",
|
|
9439
|
+
{
|
|
9440
|
+
check_filesystem: checkFilesystem,
|
|
9441
|
+
check_memory: checkMemory,
|
|
9442
|
+
check_process: checkProcess
|
|
9443
|
+
}
|
|
9444
|
+
);
|
|
9445
|
+
const results = {
|
|
9446
|
+
isolation_level: status.hardening_level,
|
|
9447
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
9448
|
+
};
|
|
9449
|
+
if (checkFilesystem) {
|
|
9450
|
+
const fs = status.filesystem_permissions;
|
|
9451
|
+
results.filesystem = {
|
|
9452
|
+
sanctuary_storage_protected: fs.sanctuary_storage_protected,
|
|
9453
|
+
storage_mode: fs.sanctuary_storage_mode,
|
|
9454
|
+
is_secure: fs.overall === "secure",
|
|
9455
|
+
issues: fs.overall === "insecure" ? [
|
|
9456
|
+
"Storage directory is readable by group or others. Recommend: chmod 700 on Sanctuary storage path."
|
|
9457
|
+
] : fs.overall === "warning" ? [
|
|
9458
|
+
"Storage directory not owned by current user. Verify correct user is running Sanctuary."
|
|
9459
|
+
] : []
|
|
9460
|
+
};
|
|
9461
|
+
}
|
|
9462
|
+
if (checkMemory) {
|
|
9463
|
+
const mem = status.memory_protection;
|
|
9464
|
+
const issues = [];
|
|
9465
|
+
if (!mem.aslr_enabled) {
|
|
9466
|
+
issues.push(
|
|
9467
|
+
"ASLR not detected. On Linux, enable with: echo 2 | sudo tee /proc/sys/kernel/randomize_va_space"
|
|
9468
|
+
);
|
|
9469
|
+
}
|
|
9470
|
+
results.memory = {
|
|
9471
|
+
aslr_enabled: mem.aslr_enabled,
|
|
9472
|
+
stack_canaries: mem.stack_canaries,
|
|
9473
|
+
secure_buffer_handling: mem.secure_buffer_zeros,
|
|
9474
|
+
argon2id_key_derivation: mem.argon2id_kdf,
|
|
9475
|
+
protection_level: mem.overall,
|
|
9476
|
+
issues
|
|
9477
|
+
};
|
|
9478
|
+
}
|
|
9479
|
+
if (checkProcess) {
|
|
9480
|
+
const iso = status.process_isolation;
|
|
9481
|
+
results.process = {
|
|
9482
|
+
isolation_level: iso.isolation_level,
|
|
9483
|
+
in_container: iso.is_container,
|
|
9484
|
+
in_vm: iso.is_vm,
|
|
9485
|
+
sandboxed: iso.is_sandboxed,
|
|
9486
|
+
has_tee: iso.is_tee,
|
|
9487
|
+
environment: iso.details,
|
|
9488
|
+
recommendation: iso.isolation_level === "none" ? "Consider running Sanctuary in a container or VM for improved isolation." : iso.isolation_level === "basic" ? "Basic isolation detected. Container or VM would provide stronger guarantees." : "Running in isolated environment \u2014 process-level isolation is strong."
|
|
9489
|
+
};
|
|
9490
|
+
}
|
|
9491
|
+
return toolResult({
|
|
9492
|
+
status: "verified",
|
|
9493
|
+
results
|
|
9494
|
+
});
|
|
9495
|
+
}
|
|
9496
|
+
}
|
|
9497
|
+
];
|
|
9498
|
+
}
|
|
9499
|
+
|
|
9500
|
+
// src/index.ts
|
|
9501
|
+
init_encoding();
|
|
9502
|
+
async function createSanctuaryServer(options) {
|
|
9503
|
+
const config = await loadConfig(options?.configPath);
|
|
9504
|
+
await promises.mkdir(config.storage_path, { recursive: true, mode: 448 });
|
|
9505
|
+
const storage = options?.storage ?? new FilesystemStorage(
|
|
9506
|
+
`${config.storage_path}/state`
|
|
9507
|
+
);
|
|
9508
|
+
let masterKey;
|
|
9509
|
+
let keyProtection;
|
|
9510
|
+
let recoveryKey;
|
|
9511
|
+
const passphrase = options?.passphrase ?? process.env.SANCTUARY_PASSPHRASE;
|
|
9512
|
+
if (passphrase) {
|
|
9513
|
+
keyProtection = "passphrase";
|
|
9514
|
+
let existingParams;
|
|
9515
|
+
try {
|
|
9516
|
+
const raw = await storage.read("_meta", "key-params");
|
|
9517
|
+
if (raw) {
|
|
9518
|
+
const { bytesToString: bytesToString2 } = await Promise.resolve().then(() => (init_encoding(), encoding_exports));
|
|
9519
|
+
existingParams = JSON.parse(bytesToString2(raw));
|
|
9520
|
+
}
|
|
9521
|
+
} catch {
|
|
9522
|
+
}
|
|
9523
|
+
const result = await deriveMasterKey(passphrase, existingParams);
|
|
9524
|
+
masterKey = result.key;
|
|
9525
|
+
if (!existingParams) {
|
|
9526
|
+
const { stringToBytes: stringToBytes2 } = await Promise.resolve().then(() => (init_encoding(), encoding_exports));
|
|
9527
|
+
await storage.write(
|
|
6667
9528
|
"_meta",
|
|
6668
9529
|
"key-params",
|
|
6669
9530
|
stringToBytes2(JSON.stringify(result.params))
|
|
@@ -6671,15 +9532,51 @@ async function createSanctuaryServer(options) {
|
|
|
6671
9532
|
}
|
|
6672
9533
|
} else {
|
|
6673
9534
|
keyProtection = "recovery-key";
|
|
6674
|
-
const
|
|
6675
|
-
|
|
6676
|
-
|
|
6677
|
-
|
|
9535
|
+
const { hashToString: hashToString2 } = await Promise.resolve().then(() => (init_hashing(), hashing_exports));
|
|
9536
|
+
const { stringToBytes: stringToBytes2, bytesToString: bytesToString2 } = await Promise.resolve().then(() => (init_encoding(), encoding_exports));
|
|
9537
|
+
const { fromBase64url: fromBase64url2 } = await Promise.resolve().then(() => (init_encoding(), encoding_exports));
|
|
9538
|
+
const { constantTimeEqual: constantTimeEqual2 } = await Promise.resolve().then(() => (init_encoding(), encoding_exports));
|
|
9539
|
+
const existingHash = await storage.read("_meta", "recovery-key-hash");
|
|
9540
|
+
if (existingHash) {
|
|
9541
|
+
const envRecoveryKey = process.env.SANCTUARY_RECOVERY_KEY;
|
|
9542
|
+
if (!envRecoveryKey) {
|
|
9543
|
+
throw new Error(
|
|
9544
|
+
"Sanctuary: Existing encrypted data found but no credentials provided.\nThis installation was previously set up with a recovery key.\n\nTo start the server, provide one of:\n - SANCTUARY_PASSPHRASE (if you later configured a passphrase)\n - SANCTUARY_RECOVERY_KEY (the recovery key shown at first run)\n\nWithout the correct credentials, encrypted state cannot be accessed.\nRefusing to start to prevent silent data loss."
|
|
9545
|
+
);
|
|
9546
|
+
}
|
|
9547
|
+
let recoveryKeyBytes;
|
|
9548
|
+
try {
|
|
9549
|
+
recoveryKeyBytes = fromBase64url2(envRecoveryKey);
|
|
9550
|
+
} catch {
|
|
9551
|
+
throw new Error(
|
|
9552
|
+
"Sanctuary: SANCTUARY_RECOVERY_KEY is not valid base64url. The recovery key should be the exact string shown at first run."
|
|
9553
|
+
);
|
|
9554
|
+
}
|
|
9555
|
+
if (recoveryKeyBytes.length !== 32) {
|
|
9556
|
+
throw new Error(
|
|
9557
|
+
"Sanctuary: SANCTUARY_RECOVERY_KEY has incorrect length. The recovery key should be the exact string shown at first run."
|
|
9558
|
+
);
|
|
9559
|
+
}
|
|
9560
|
+
const providedHash = hashToString2(recoveryKeyBytes);
|
|
9561
|
+
const storedHash = bytesToString2(existingHash);
|
|
9562
|
+
const providedHashBytes = stringToBytes2(providedHash);
|
|
9563
|
+
const storedHashBytes = stringToBytes2(storedHash);
|
|
9564
|
+
if (!constantTimeEqual2(providedHashBytes, storedHashBytes)) {
|
|
9565
|
+
throw new Error(
|
|
9566
|
+
"Sanctuary: Recovery key does not match the stored key hash.\nThe recovery key provided via SANCTUARY_RECOVERY_KEY is incorrect.\nUse the exact recovery key that was displayed at first run."
|
|
9567
|
+
);
|
|
9568
|
+
}
|
|
9569
|
+
masterKey = recoveryKeyBytes;
|
|
6678
9570
|
} else {
|
|
9571
|
+
const existingNamespaces = await storage.list("_meta");
|
|
9572
|
+
const hasKeyParams = existingNamespaces.some((e) => e.key === "key-params");
|
|
9573
|
+
if (hasKeyParams) {
|
|
9574
|
+
throw new Error(
|
|
9575
|
+
"Sanctuary: Found existing key derivation parameters but no recovery key hash.\nThis indicates a corrupted or incomplete installation.\nIf you previously used a passphrase, set SANCTUARY_PASSPHRASE to start."
|
|
9576
|
+
);
|
|
9577
|
+
}
|
|
6679
9578
|
masterKey = generateRandomKey();
|
|
6680
9579
|
recoveryKey = toBase64url(masterKey);
|
|
6681
|
-
const { hashToString: hashToString2 } = await Promise.resolve().then(() => (init_hashing(), hashing_exports));
|
|
6682
|
-
const { stringToBytes: stringToBytes2 } = await Promise.resolve().then(() => (init_encoding(), encoding_exports));
|
|
6683
9580
|
const keyHash = hashToString2(masterKey);
|
|
6684
9581
|
await storage.write(
|
|
6685
9582
|
"_meta",
|
|
@@ -6766,7 +9663,7 @@ async function createSanctuaryServer(options) {
|
|
|
6766
9663
|
layer: "l2",
|
|
6767
9664
|
description: "Process-level isolation only (no TEE)",
|
|
6768
9665
|
severity: "warning",
|
|
6769
|
-
mitigation: "TEE support planned for
|
|
9666
|
+
mitigation: "TEE support planned for a future release"
|
|
6770
9667
|
});
|
|
6771
9668
|
if (config.disclosure.proof_system === "commitment-only") {
|
|
6772
9669
|
degradations.push({
|
|
@@ -6906,7 +9803,7 @@ async function createSanctuaryServer(options) {
|
|
|
6906
9803
|
},
|
|
6907
9804
|
limitations: [
|
|
6908
9805
|
"L1 identity uses ed25519 only; KERI support planned for v0.2.0",
|
|
6909
|
-
"L2 isolation is process-level only; TEE support planned for
|
|
9806
|
+
"L2 isolation is process-level only; TEE support planned for a future release",
|
|
6910
9807
|
"L3 uses commitment schemes only; ZK proofs planned for v0.2.0",
|
|
6911
9808
|
"L4 Sybil resistance is escrow-based only",
|
|
6912
9809
|
"Spec license: CC-BY-4.0 | Code license: Apache-2.0"
|
|
@@ -6927,7 +9824,7 @@ async function createSanctuaryServer(options) {
|
|
|
6927
9824
|
masterKey,
|
|
6928
9825
|
auditLog
|
|
6929
9826
|
);
|
|
6930
|
-
const { tools: l4Tools
|
|
9827
|
+
const { tools: l4Tools} = createL4Tools(
|
|
6931
9828
|
storage,
|
|
6932
9829
|
masterKey,
|
|
6933
9830
|
identityManager,
|
|
@@ -6945,6 +9842,13 @@ async function createSanctuaryServer(options) {
|
|
|
6945
9842
|
auditLog,
|
|
6946
9843
|
handshakeResults
|
|
6947
9844
|
);
|
|
9845
|
+
const { tools: auditTools } = createAuditTools(config);
|
|
9846
|
+
const { tools: contextGateTools } = createContextGateTools(
|
|
9847
|
+
storage,
|
|
9848
|
+
masterKey,
|
|
9849
|
+
auditLog
|
|
9850
|
+
);
|
|
9851
|
+
const hardeningTools = createL2HardeningTools(config.storage_path, auditLog);
|
|
6948
9852
|
const policy = await loadPrincipalPolicy(config.storage_path);
|
|
6949
9853
|
const baseline = new BaselineTracker(storage, masterKey);
|
|
6950
9854
|
await baseline.load();
|
|
@@ -6960,7 +9864,7 @@ async function createSanctuaryServer(options) {
|
|
|
6960
9864
|
port: config.dashboard.port,
|
|
6961
9865
|
host: config.dashboard.host,
|
|
6962
9866
|
timeout_seconds: policy.approval_channel.timeout_seconds,
|
|
6963
|
-
|
|
9867
|
+
// SEC-002: auto_deny removed — timeout always denies
|
|
6964
9868
|
auth_token: authToken,
|
|
6965
9869
|
tls: config.dashboard.tls
|
|
6966
9870
|
});
|
|
@@ -6973,8 +9877,8 @@ async function createSanctuaryServer(options) {
|
|
|
6973
9877
|
webhook_secret: config.webhook.secret,
|
|
6974
9878
|
callback_port: config.webhook.callback_port,
|
|
6975
9879
|
callback_host: config.webhook.callback_host,
|
|
6976
|
-
timeout_seconds: policy.approval_channel.timeout_seconds
|
|
6977
|
-
|
|
9880
|
+
timeout_seconds: policy.approval_channel.timeout_seconds
|
|
9881
|
+
// SEC-002: auto_deny removed — timeout always denies
|
|
6978
9882
|
});
|
|
6979
9883
|
await webhook.start();
|
|
6980
9884
|
approvalChannel = webhook;
|
|
@@ -6993,6 +9897,9 @@ async function createSanctuaryServer(options) {
|
|
|
6993
9897
|
...handshakeTools,
|
|
6994
9898
|
...federationTools,
|
|
6995
9899
|
...bridgeTools,
|
|
9900
|
+
...auditTools,
|
|
9901
|
+
...contextGateTools,
|
|
9902
|
+
...hardeningTools,
|
|
6996
9903
|
manifestTool
|
|
6997
9904
|
];
|
|
6998
9905
|
const server = createServer(allTools, { gate });
|
|
@@ -7017,8 +9924,78 @@ async function createSanctuaryServer(options) {
|
|
|
7017
9924
|
}
|
|
7018
9925
|
return { server, config };
|
|
7019
9926
|
}
|
|
7020
|
-
|
|
7021
|
-
|
|
9927
|
+
var REGISTRY_URL = "https://registry.npmjs.org/@sanctuary-framework/mcp-server/latest";
|
|
9928
|
+
var TIMEOUT_MS = 3e3;
|
|
9929
|
+
function isNewerVersion(current, latest) {
|
|
9930
|
+
const parse = (v) => v.replace(/^v/, "").split(".").map(Number);
|
|
9931
|
+
const [curMajor = 0, curMinor = 0, curPatch = 0] = parse(current);
|
|
9932
|
+
const [latMajor = 0, latMinor = 0, latPatch = 0] = parse(latest);
|
|
9933
|
+
if (latMajor !== curMajor) return latMajor > curMajor;
|
|
9934
|
+
if (latMinor !== curMinor) return latMinor > curMinor;
|
|
9935
|
+
return latPatch > curPatch;
|
|
9936
|
+
}
|
|
9937
|
+
function formatUpdateMessage(current, latest) {
|
|
9938
|
+
return `[Sanctuary] Update available: ${current} \u2192 ${latest} \u2014 run: npx @sanctuary-framework/mcp-server@latest`;
|
|
9939
|
+
}
|
|
9940
|
+
function fetchLatestVersion(currentVersion) {
|
|
9941
|
+
return new Promise((resolve) => {
|
|
9942
|
+
const req = https.get(
|
|
9943
|
+
REGISTRY_URL,
|
|
9944
|
+
{
|
|
9945
|
+
headers: { Accept: "application/json" },
|
|
9946
|
+
timeout: TIMEOUT_MS
|
|
9947
|
+
},
|
|
9948
|
+
(res) => {
|
|
9949
|
+
if (res.statusCode !== 200) {
|
|
9950
|
+
res.resume();
|
|
9951
|
+
resolve(null);
|
|
9952
|
+
return;
|
|
9953
|
+
}
|
|
9954
|
+
let data = "";
|
|
9955
|
+
res.setEncoding("utf-8");
|
|
9956
|
+
res.on("data", (chunk) => {
|
|
9957
|
+
data += chunk;
|
|
9958
|
+
if (data.length > 32768) {
|
|
9959
|
+
res.destroy();
|
|
9960
|
+
resolve(null);
|
|
9961
|
+
}
|
|
9962
|
+
});
|
|
9963
|
+
res.on("end", () => {
|
|
9964
|
+
try {
|
|
9965
|
+
const json = JSON.parse(data);
|
|
9966
|
+
const latest = json.version;
|
|
9967
|
+
if (typeof latest === "string" && isNewerVersion(currentVersion, latest)) {
|
|
9968
|
+
resolve(latest);
|
|
9969
|
+
} else {
|
|
9970
|
+
resolve(null);
|
|
9971
|
+
}
|
|
9972
|
+
} catch {
|
|
9973
|
+
resolve(null);
|
|
9974
|
+
}
|
|
9975
|
+
});
|
|
9976
|
+
}
|
|
9977
|
+
);
|
|
9978
|
+
req.on("error", () => resolve(null));
|
|
9979
|
+
req.on("timeout", () => {
|
|
9980
|
+
req.destroy();
|
|
9981
|
+
resolve(null);
|
|
9982
|
+
});
|
|
9983
|
+
});
|
|
9984
|
+
}
|
|
9985
|
+
async function checkForUpdate(currentVersion) {
|
|
9986
|
+
if (process.env.SANCTUARY_NO_UPDATE_CHECK === "1") {
|
|
9987
|
+
return;
|
|
9988
|
+
}
|
|
9989
|
+
try {
|
|
9990
|
+
const latest = await fetchLatestVersion(currentVersion);
|
|
9991
|
+
if (latest) {
|
|
9992
|
+
console.error(formatUpdateMessage(currentVersion, latest));
|
|
9993
|
+
}
|
|
9994
|
+
} catch {
|
|
9995
|
+
}
|
|
9996
|
+
}
|
|
9997
|
+
var require5 = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href)));
|
|
9998
|
+
var { version: PKG_VERSION4 } = require5("../package.json");
|
|
7022
9999
|
async function main() {
|
|
7023
10000
|
const args = process.argv.slice(2);
|
|
7024
10001
|
let passphrase = process.env.SANCTUARY_PASSPHRASE;
|
|
@@ -7031,7 +10008,7 @@ async function main() {
|
|
|
7031
10008
|
printHelp();
|
|
7032
10009
|
process.exit(0);
|
|
7033
10010
|
} else if (args[i] === "--version" || args[i] === "-v") {
|
|
7034
|
-
console.log(
|
|
10011
|
+
console.log(`@sanctuary-framework/mcp-server ${PKG_VERSION4}`);
|
|
7035
10012
|
process.exit(0);
|
|
7036
10013
|
}
|
|
7037
10014
|
}
|
|
@@ -7042,6 +10019,7 @@ async function main() {
|
|
|
7042
10019
|
console.error(`Sanctuary MCP Server v${config.version} running (stdio)`);
|
|
7043
10020
|
console.error(`Storage: ${config.storage_path}`);
|
|
7044
10021
|
console.error("Tools: all registered");
|
|
10022
|
+
checkForUpdate(PKG_VERSION4);
|
|
7045
10023
|
} else {
|
|
7046
10024
|
console.error("HTTP transport not yet implemented. Use stdio.");
|
|
7047
10025
|
process.exit(1);
|
|
@@ -7049,7 +10027,7 @@ async function main() {
|
|
|
7049
10027
|
}
|
|
7050
10028
|
function printHelp() {
|
|
7051
10029
|
console.log(`
|
|
7052
|
-
@sanctuary-framework/mcp-server
|
|
10030
|
+
@sanctuary-framework/mcp-server v${PKG_VERSION4}
|
|
7053
10031
|
|
|
7054
10032
|
Sovereignty infrastructure for agents in the agentic economy.
|
|
7055
10033
|
|
|
@@ -7071,6 +10049,7 @@ Environment variables:
|
|
|
7071
10049
|
SANCTUARY_WEBHOOK_ENABLED "true" to enable webhook approvals
|
|
7072
10050
|
SANCTUARY_WEBHOOK_URL Webhook target URL
|
|
7073
10051
|
SANCTUARY_WEBHOOK_SECRET HMAC-SHA256 shared secret
|
|
10052
|
+
SANCTUARY_NO_UPDATE_CHECK "1" to disable startup update check
|
|
7074
10053
|
|
|
7075
10054
|
For more info: https://github.com/eriknewton/sanctuary-framework
|
|
7076
10055
|
`);
|