@sanctuary-framework/mcp-server 0.3.0 → 0.3.1
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 +1044 -86
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1045 -87
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1044 -86
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +58 -15
- package/dist/index.d.ts +58 -15
- package/dist/index.js +1045 -87
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -300,8 +300,13 @@ async function loadConfig(configPath) {
|
|
|
300
300
|
try {
|
|
301
301
|
const raw = await promises.readFile(path$1, "utf-8");
|
|
302
302
|
const fileConfig = JSON.parse(raw);
|
|
303
|
-
|
|
304
|
-
|
|
303
|
+
const merged = deepMerge(config, fileConfig);
|
|
304
|
+
validateConfig(merged);
|
|
305
|
+
return merged;
|
|
306
|
+
} catch (err) {
|
|
307
|
+
if (err instanceof Error && err.message.includes("unimplemented features")) {
|
|
308
|
+
throw err;
|
|
309
|
+
}
|
|
305
310
|
return config;
|
|
306
311
|
}
|
|
307
312
|
}
|
|
@@ -309,6 +314,33 @@ async function saveConfig(config, configPath) {
|
|
|
309
314
|
const path$1 = path.join(config.storage_path, "sanctuary.json");
|
|
310
315
|
await promises.writeFile(path$1, JSON.stringify(config, null, 2), { mode: 384 });
|
|
311
316
|
}
|
|
317
|
+
function validateConfig(config) {
|
|
318
|
+
const errors = [];
|
|
319
|
+
const implementedKeyProtection = /* @__PURE__ */ new Set(["passphrase", "none"]);
|
|
320
|
+
if (!implementedKeyProtection.has(config.state.key_protection)) {
|
|
321
|
+
errors.push(
|
|
322
|
+
`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.`
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
const implementedEnvironment = /* @__PURE__ */ new Set(["local-process", "docker"]);
|
|
326
|
+
if (!implementedEnvironment.has(config.execution.environment)) {
|
|
327
|
+
errors.push(
|
|
328
|
+
`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.`
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
const implementedProofSystem = /* @__PURE__ */ new Set(["commitment-only"]);
|
|
332
|
+
if (!implementedProofSystem.has(config.disclosure.proof_system)) {
|
|
333
|
+
errors.push(
|
|
334
|
+
`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.`
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
if (errors.length > 0) {
|
|
338
|
+
throw new Error(
|
|
339
|
+
`Sanctuary configuration references unimplemented features:
|
|
340
|
+
${errors.join("\n")}`
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
312
344
|
function deepMerge(base, override) {
|
|
313
345
|
const result = { ...base };
|
|
314
346
|
for (const [key, value] of Object.entries(override)) {
|
|
@@ -652,7 +684,11 @@ var RESERVED_NAMESPACE_PREFIXES = [
|
|
|
652
684
|
"_commitments",
|
|
653
685
|
"_reputation",
|
|
654
686
|
"_escrow",
|
|
655
|
-
"_guarantees"
|
|
687
|
+
"_guarantees",
|
|
688
|
+
"_bridge",
|
|
689
|
+
"_federation",
|
|
690
|
+
"_handshake",
|
|
691
|
+
"_shr"
|
|
656
692
|
];
|
|
657
693
|
var StateStore = class {
|
|
658
694
|
storage;
|
|
@@ -919,12 +955,14 @@ var StateStore = class {
|
|
|
919
955
|
/**
|
|
920
956
|
* Import a previously exported state bundle.
|
|
921
957
|
*/
|
|
922
|
-
async import(bundleBase64, conflictResolution = "skip") {
|
|
958
|
+
async import(bundleBase64, conflictResolution = "skip", publicKeyResolver) {
|
|
923
959
|
const bundleBytes = fromBase64url(bundleBase64);
|
|
924
960
|
const bundleJson = bytesToString(bundleBytes);
|
|
925
961
|
const bundle = JSON.parse(bundleJson);
|
|
926
962
|
let importedKeys = 0;
|
|
927
963
|
let skippedKeys = 0;
|
|
964
|
+
let skippedInvalidSig = 0;
|
|
965
|
+
let skippedUnknownKid = 0;
|
|
928
966
|
let conflicts = 0;
|
|
929
967
|
const namespaces = [];
|
|
930
968
|
for (const [ns, entries] of Object.entries(
|
|
@@ -938,6 +976,26 @@ var StateStore = class {
|
|
|
938
976
|
}
|
|
939
977
|
namespaces.push(ns);
|
|
940
978
|
for (const { key, entry } of entries) {
|
|
979
|
+
const signerPublicKey = publicKeyResolver(entry.kid);
|
|
980
|
+
if (!signerPublicKey) {
|
|
981
|
+
skippedUnknownKid++;
|
|
982
|
+
skippedKeys++;
|
|
983
|
+
continue;
|
|
984
|
+
}
|
|
985
|
+
try {
|
|
986
|
+
const ciphertextBytes = fromBase64url(entry.payload.ct);
|
|
987
|
+
const signatureBytes = fromBase64url(entry.sig);
|
|
988
|
+
const sigValid = verify(ciphertextBytes, signatureBytes, signerPublicKey);
|
|
989
|
+
if (!sigValid) {
|
|
990
|
+
skippedInvalidSig++;
|
|
991
|
+
skippedKeys++;
|
|
992
|
+
continue;
|
|
993
|
+
}
|
|
994
|
+
} catch {
|
|
995
|
+
skippedInvalidSig++;
|
|
996
|
+
skippedKeys++;
|
|
997
|
+
continue;
|
|
998
|
+
}
|
|
941
999
|
const exists = await this.storage.exists(ns, key);
|
|
942
1000
|
if (exists) {
|
|
943
1001
|
conflicts++;
|
|
@@ -973,6 +1031,8 @@ var StateStore = class {
|
|
|
973
1031
|
return {
|
|
974
1032
|
imported_keys: importedKeys,
|
|
975
1033
|
skipped_keys: skippedKeys,
|
|
1034
|
+
skipped_invalid_sig: skippedInvalidSig,
|
|
1035
|
+
skipped_unknown_kid: skippedUnknownKid,
|
|
976
1036
|
conflicts,
|
|
977
1037
|
namespaces,
|
|
978
1038
|
imported_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -1161,7 +1221,11 @@ var RESERVED_NAMESPACE_PREFIXES2 = [
|
|
|
1161
1221
|
"_commitments",
|
|
1162
1222
|
"_reputation",
|
|
1163
1223
|
"_escrow",
|
|
1164
|
-
"_guarantees"
|
|
1224
|
+
"_guarantees",
|
|
1225
|
+
"_bridge",
|
|
1226
|
+
"_federation",
|
|
1227
|
+
"_handshake",
|
|
1228
|
+
"_shr"
|
|
1165
1229
|
];
|
|
1166
1230
|
function getReservedNamespaceViolation(namespace) {
|
|
1167
1231
|
for (const prefix of RESERVED_NAMESPACE_PREFIXES2) {
|
|
@@ -1498,6 +1562,13 @@ function createL1Tools(stateStore, storage, masterKey, keyProtection, auditLog)
|
|
|
1498
1562
|
required: ["namespace", "key"]
|
|
1499
1563
|
},
|
|
1500
1564
|
handler: async (args) => {
|
|
1565
|
+
const reservedViolation = getReservedNamespaceViolation(args.namespace);
|
|
1566
|
+
if (reservedViolation) {
|
|
1567
|
+
return toolResult({
|
|
1568
|
+
error: "namespace_reserved",
|
|
1569
|
+
message: `Namespace "${args.namespace}" is reserved for internal use (prefix: ${reservedViolation}). Cannot read from reserved namespaces.`
|
|
1570
|
+
});
|
|
1571
|
+
}
|
|
1501
1572
|
const result = await stateStore.read(
|
|
1502
1573
|
args.namespace,
|
|
1503
1574
|
args.key,
|
|
@@ -1534,6 +1605,13 @@ function createL1Tools(stateStore, storage, masterKey, keyProtection, auditLog)
|
|
|
1534
1605
|
required: ["namespace"]
|
|
1535
1606
|
},
|
|
1536
1607
|
handler: async (args) => {
|
|
1608
|
+
const reservedViolation = getReservedNamespaceViolation(args.namespace);
|
|
1609
|
+
if (reservedViolation) {
|
|
1610
|
+
return toolResult({
|
|
1611
|
+
error: "namespace_reserved",
|
|
1612
|
+
message: `Namespace "${args.namespace}" is reserved for internal use (prefix: ${reservedViolation}). Cannot list reserved namespaces.`
|
|
1613
|
+
});
|
|
1614
|
+
}
|
|
1537
1615
|
const result = await stateStore.list(
|
|
1538
1616
|
args.namespace,
|
|
1539
1617
|
args.prefix,
|
|
@@ -1612,9 +1690,15 @@ function createL1Tools(stateStore, storage, masterKey, keyProtection, auditLog)
|
|
|
1612
1690
|
required: ["bundle"]
|
|
1613
1691
|
},
|
|
1614
1692
|
handler: async (args) => {
|
|
1693
|
+
const publicKeyResolver = (kid) => {
|
|
1694
|
+
const identity = identityMgr.get(kid);
|
|
1695
|
+
if (!identity) return null;
|
|
1696
|
+
return fromBase64url(identity.public_key);
|
|
1697
|
+
};
|
|
1615
1698
|
const result = await stateStore.import(
|
|
1616
1699
|
args.bundle,
|
|
1617
|
-
args.conflict_resolution ?? "skip"
|
|
1700
|
+
args.conflict_resolution ?? "skip",
|
|
1701
|
+
publicKeyResolver
|
|
1618
1702
|
);
|
|
1619
1703
|
auditLog?.append("l1", "state_import", "principal", {
|
|
1620
1704
|
imported_keys: result.imported_keys
|
|
@@ -2059,7 +2143,7 @@ function createRangeProof(value, blindingFactor, commitment, min, max) {
|
|
|
2059
2143
|
bitProofs.push(bitProof);
|
|
2060
2144
|
}
|
|
2061
2145
|
const sumBlinding = bitBlindings.reduce(
|
|
2062
|
-
(acc, bi, i) => mod(acc + mod(BigInt(1 << i)) * bi),
|
|
2146
|
+
(acc, bi, i) => mod(acc + mod(BigInt(1) << BigInt(i)) * bi),
|
|
2063
2147
|
0n
|
|
2064
2148
|
);
|
|
2065
2149
|
const blindingDiff = mod(b - sumBlinding);
|
|
@@ -2101,7 +2185,7 @@ function verifyRangeProof(proof) {
|
|
|
2101
2185
|
let reconstructed = ed25519.RistrettoPoint.ZERO;
|
|
2102
2186
|
for (let i = 0; i < numBits; i++) {
|
|
2103
2187
|
const C_i = ed25519.RistrettoPoint.fromHex(fromBase64url(proof.bit_commitments[i]));
|
|
2104
|
-
const weight = mod(BigInt(1 << i));
|
|
2188
|
+
const weight = mod(BigInt(1) << BigInt(i));
|
|
2105
2189
|
reconstructed = reconstructed.add(safeMultiply(C_i, weight));
|
|
2106
2190
|
}
|
|
2107
2191
|
const diff = C.subtract(safeMultiply(G, mod(BigInt(proof.min)))).subtract(reconstructed);
|
|
@@ -3156,7 +3240,9 @@ function createL4Tools(storage, masterKey, identityManager, auditLog, handshakeR
|
|
|
3156
3240
|
contexts: summary.contexts
|
|
3157
3241
|
});
|
|
3158
3242
|
return toolResult({
|
|
3159
|
-
summary
|
|
3243
|
+
summary,
|
|
3244
|
+
// SEC-ADD-03: Tag response as containing counterparty-generated attestation data
|
|
3245
|
+
_content_trust: "external"
|
|
3160
3246
|
});
|
|
3161
3247
|
}
|
|
3162
3248
|
},
|
|
@@ -3477,14 +3563,16 @@ var DEFAULT_TIER2 = {
|
|
|
3477
3563
|
};
|
|
3478
3564
|
var DEFAULT_CHANNEL = {
|
|
3479
3565
|
type: "stderr",
|
|
3480
|
-
timeout_seconds: 300
|
|
3481
|
-
|
|
3566
|
+
timeout_seconds: 300
|
|
3567
|
+
// SEC-002: auto_deny is not configurable. Timeout always denies.
|
|
3568
|
+
// Field omitted intentionally — all channels hardcode deny on timeout.
|
|
3482
3569
|
};
|
|
3483
3570
|
var DEFAULT_POLICY = {
|
|
3484
3571
|
version: 1,
|
|
3485
3572
|
tier1_always_approve: [
|
|
3486
3573
|
"state_export",
|
|
3487
3574
|
"state_import",
|
|
3575
|
+
"state_delete",
|
|
3488
3576
|
"identity_rotate",
|
|
3489
3577
|
"reputation_import",
|
|
3490
3578
|
"bootstrap_provide_guarantee"
|
|
@@ -3494,7 +3582,6 @@ var DEFAULT_POLICY = {
|
|
|
3494
3582
|
"state_read",
|
|
3495
3583
|
"state_write",
|
|
3496
3584
|
"state_list",
|
|
3497
|
-
"state_delete",
|
|
3498
3585
|
"identity_create",
|
|
3499
3586
|
"identity_list",
|
|
3500
3587
|
"identity_sign",
|
|
@@ -3602,10 +3689,14 @@ function validatePolicy(raw) {
|
|
|
3602
3689
|
...raw.tier2_anomaly ?? {}
|
|
3603
3690
|
},
|
|
3604
3691
|
tier3_always_allow: raw.tier3_always_allow ?? DEFAULT_POLICY.tier3_always_allow,
|
|
3605
|
-
approval_channel: {
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3692
|
+
approval_channel: (() => {
|
|
3693
|
+
const merged = {
|
|
3694
|
+
...DEFAULT_CHANNEL,
|
|
3695
|
+
...raw.approval_channel ?? {}
|
|
3696
|
+
};
|
|
3697
|
+
delete merged.auto_deny;
|
|
3698
|
+
return merged;
|
|
3699
|
+
})()
|
|
3609
3700
|
};
|
|
3610
3701
|
}
|
|
3611
3702
|
function generateDefaultPolicyYaml() {
|
|
@@ -3622,6 +3713,7 @@ version: 1
|
|
|
3622
3713
|
tier1_always_approve:
|
|
3623
3714
|
- state_export
|
|
3624
3715
|
- state_import
|
|
3716
|
+
- state_delete
|
|
3625
3717
|
- identity_rotate
|
|
3626
3718
|
- reputation_import
|
|
3627
3719
|
- bootstrap_provide_guarantee
|
|
@@ -3643,7 +3735,6 @@ tier3_always_allow:
|
|
|
3643
3735
|
- state_read
|
|
3644
3736
|
- state_write
|
|
3645
3737
|
- state_list
|
|
3646
|
-
- state_delete
|
|
3647
3738
|
- identity_create
|
|
3648
3739
|
- identity_list
|
|
3649
3740
|
- identity_sign
|
|
@@ -3680,10 +3771,10 @@ tier3_always_allow:
|
|
|
3680
3771
|
|
|
3681
3772
|
# \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
3773
|
# How Sanctuary reaches you when approval is needed.
|
|
3774
|
+
# NOTE: Timeout always results in denial. This is not configurable (SEC-002).
|
|
3683
3775
|
approval_channel:
|
|
3684
3776
|
type: stderr
|
|
3685
3777
|
timeout_seconds: 300
|
|
3686
|
-
auto_deny: true
|
|
3687
3778
|
`;
|
|
3688
3779
|
}
|
|
3689
3780
|
async function loadPrincipalPolicy(storagePath) {
|
|
@@ -3860,27 +3951,16 @@ var BaselineTracker = class {
|
|
|
3860
3951
|
|
|
3861
3952
|
// src/principal-policy/approval-channel.ts
|
|
3862
3953
|
var StderrApprovalChannel = class {
|
|
3863
|
-
|
|
3864
|
-
constructor(config) {
|
|
3865
|
-
this.config = config;
|
|
3954
|
+
constructor(_config) {
|
|
3866
3955
|
}
|
|
3867
3956
|
async requestApproval(request) {
|
|
3868
3957
|
const prompt = this.formatPrompt(request);
|
|
3869
3958
|
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
|
-
}
|
|
3959
|
+
return {
|
|
3960
|
+
decision: "deny",
|
|
3961
|
+
decided_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3962
|
+
decided_by: "stderr:non-interactive"
|
|
3963
|
+
};
|
|
3884
3964
|
}
|
|
3885
3965
|
formatPrompt(request) {
|
|
3886
3966
|
const tierLabel = request.tier === 1 ? "Tier 1 \u2014 always requires approval" : "Tier 2 \u2014 behavioral anomaly detected";
|
|
@@ -3888,7 +3968,7 @@ var StderrApprovalChannel = class {
|
|
|
3888
3968
|
return [
|
|
3889
3969
|
"",
|
|
3890
3970
|
"\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:
|
|
3971
|
+
"\u2551 SANCTUARY: Operation Denied (non-interactive channel) \u2551",
|
|
3892
3972
|
"\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
3973
|
`\u2551 Operation: ${request.operation.padEnd(50)}\u2551`,
|
|
3894
3974
|
`\u2551 ${tierLabel.padEnd(62)}\u2551`,
|
|
@@ -3899,7 +3979,8 @@ var StderrApprovalChannel = class {
|
|
|
3899
3979
|
(line) => `\u2551 ${line.padEnd(60)}\u2551`
|
|
3900
3980
|
),
|
|
3901
3981
|
"\u2551 \u2551",
|
|
3902
|
-
|
|
3982
|
+
"\u2551 Denied: stderr channel cannot accept input (SEC-016) \u2551",
|
|
3983
|
+
"\u2551 Use dashboard or webhook channel for interactive approval. \u2551",
|
|
3903
3984
|
"\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
3985
|
""
|
|
3905
3986
|
].join("\n");
|
|
@@ -4203,20 +4284,38 @@ function generateDashboardHTML(options) {
|
|
|
4203
4284
|
<script>
|
|
4204
4285
|
(function() {
|
|
4205
4286
|
const TIMEOUT = ${options.timeoutSeconds};
|
|
4206
|
-
|
|
4287
|
+
// SEC-012: Auth token is passed via Authorization header only \u2014 never in URLs.
|
|
4288
|
+
// The token is provided by the server at generation time (embedded for initial auth).
|
|
4289
|
+
const AUTH_TOKEN = ${options.authToken ? JSON.stringify(options.authToken) : "null"};
|
|
4290
|
+
let SESSION_ID = null; // Short-lived session for SSE and URL-based requests
|
|
4207
4291
|
const pending = new Map();
|
|
4208
4292
|
let auditCount = 0;
|
|
4209
4293
|
|
|
4210
|
-
// Auth helpers
|
|
4294
|
+
// Auth helpers \u2014 SEC-012: token goes in header, session goes in URL
|
|
4211
4295
|
function authHeaders() {
|
|
4212
4296
|
const h = { 'Content-Type': 'application/json' };
|
|
4213
4297
|
if (AUTH_TOKEN) h['Authorization'] = 'Bearer ' + AUTH_TOKEN;
|
|
4214
4298
|
return h;
|
|
4215
4299
|
}
|
|
4216
|
-
function
|
|
4217
|
-
if (!
|
|
4300
|
+
function sessionQuery(url) {
|
|
4301
|
+
if (!SESSION_ID) return url;
|
|
4218
4302
|
const sep = url.includes('?') ? '&' : '?';
|
|
4219
|
-
return url + sep + '
|
|
4303
|
+
return url + sep + 'session=' + SESSION_ID;
|
|
4304
|
+
}
|
|
4305
|
+
|
|
4306
|
+
// SEC-012: Exchange the long-lived token for a short-lived session
|
|
4307
|
+
async function exchangeSession() {
|
|
4308
|
+
if (!AUTH_TOKEN) return;
|
|
4309
|
+
try {
|
|
4310
|
+
const resp = await fetch('/auth/session', { method: 'POST', headers: authHeaders() });
|
|
4311
|
+
if (resp.ok) {
|
|
4312
|
+
const data = await resp.json();
|
|
4313
|
+
SESSION_ID = data.session_id;
|
|
4314
|
+
// Refresh session before expiry (at 80% of TTL)
|
|
4315
|
+
const refreshMs = (data.expires_in_seconds || 300) * 800;
|
|
4316
|
+
setTimeout(async () => { await exchangeSession(); reconnectSSE(); }, refreshMs);
|
|
4317
|
+
}
|
|
4318
|
+
} catch(e) { /* will retry on next connect */ }
|
|
4220
4319
|
}
|
|
4221
4320
|
|
|
4222
4321
|
// Tab switching
|
|
@@ -4229,10 +4328,14 @@ function generateDashboardHTML(options) {
|
|
|
4229
4328
|
});
|
|
4230
4329
|
});
|
|
4231
4330
|
|
|
4232
|
-
// SSE Connection
|
|
4331
|
+
// SSE Connection \u2014 SEC-012: uses short-lived session token in URL, not auth token
|
|
4233
4332
|
let evtSource;
|
|
4333
|
+
function reconnectSSE() {
|
|
4334
|
+
if (evtSource) { evtSource.close(); }
|
|
4335
|
+
connect();
|
|
4336
|
+
}
|
|
4234
4337
|
function connect() {
|
|
4235
|
-
evtSource = new EventSource(
|
|
4338
|
+
evtSource = new EventSource(sessionQuery('/events'));
|
|
4236
4339
|
evtSource.onopen = () => {
|
|
4237
4340
|
document.getElementById('statusDot').classList.remove('disconnected');
|
|
4238
4341
|
document.getElementById('statusText').textContent = 'Connected';
|
|
@@ -4420,12 +4523,20 @@ function generateDashboardHTML(options) {
|
|
|
4420
4523
|
return d.innerHTML;
|
|
4421
4524
|
}
|
|
4422
4525
|
|
|
4423
|
-
// Init
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
if (
|
|
4427
|
-
if (
|
|
4428
|
-
|
|
4526
|
+
// Init \u2014 SEC-012: exchange token for session before connecting SSE
|
|
4527
|
+
(async function init() {
|
|
4528
|
+
await exchangeSession();
|
|
4529
|
+
// Clean token from URL if present (legacy bookmarks)
|
|
4530
|
+
if (window.location.search.includes('token=')) {
|
|
4531
|
+
const clean = window.location.pathname;
|
|
4532
|
+
window.history.replaceState({}, '', clean);
|
|
4533
|
+
}
|
|
4534
|
+
connect();
|
|
4535
|
+
fetch('/api/status', { headers: authHeaders() }).then(r => r.json()).then(data => {
|
|
4536
|
+
if (data.baseline) updateBaseline(data.baseline);
|
|
4537
|
+
if (data.policy) updatePolicy(data.policy);
|
|
4538
|
+
}).catch(() => {});
|
|
4539
|
+
})();
|
|
4429
4540
|
})();
|
|
4430
4541
|
</script>
|
|
4431
4542
|
</body>
|
|
@@ -4433,6 +4544,8 @@ function generateDashboardHTML(options) {
|
|
|
4433
4544
|
}
|
|
4434
4545
|
|
|
4435
4546
|
// src/principal-policy/dashboard.ts
|
|
4547
|
+
var SESSION_TTL_MS = 5 * 60 * 1e3;
|
|
4548
|
+
var MAX_SESSIONS = 1e3;
|
|
4436
4549
|
var DashboardApprovalChannel = class {
|
|
4437
4550
|
config;
|
|
4438
4551
|
pending = /* @__PURE__ */ new Map();
|
|
@@ -4444,6 +4557,9 @@ var DashboardApprovalChannel = class {
|
|
|
4444
4557
|
dashboardHTML;
|
|
4445
4558
|
authToken;
|
|
4446
4559
|
useTLS;
|
|
4560
|
+
/** SEC-012: Short-lived session store. Sessions replace URL query tokens. */
|
|
4561
|
+
sessions = /* @__PURE__ */ new Map();
|
|
4562
|
+
sessionCleanupTimer = null;
|
|
4447
4563
|
constructor(config) {
|
|
4448
4564
|
this.config = config;
|
|
4449
4565
|
this.authToken = config.auth_token;
|
|
@@ -4453,6 +4569,7 @@ var DashboardApprovalChannel = class {
|
|
|
4453
4569
|
serverVersion: "0.3.0",
|
|
4454
4570
|
authToken: this.authToken
|
|
4455
4571
|
});
|
|
4572
|
+
this.sessionCleanupTimer = setInterval(() => this.cleanupSessions(), 6e4);
|
|
4456
4573
|
}
|
|
4457
4574
|
/**
|
|
4458
4575
|
* Inject dependencies after construction.
|
|
@@ -4482,13 +4599,14 @@ var DashboardApprovalChannel = class {
|
|
|
4482
4599
|
const baseUrl = `${protocol}://${this.config.host}:${this.config.port}`;
|
|
4483
4600
|
this.httpServer.listen(this.config.port, this.config.host, () => {
|
|
4484
4601
|
if (this.authToken) {
|
|
4602
|
+
const hint = this.authToken.slice(0, 4) + "..." + this.authToken.slice(-4);
|
|
4485
4603
|
process.stderr.write(
|
|
4486
4604
|
`
|
|
4487
|
-
Sanctuary Principal Dashboard: ${baseUrl}
|
|
4605
|
+
Sanctuary Principal Dashboard: ${baseUrl}
|
|
4488
4606
|
`
|
|
4489
4607
|
);
|
|
4490
4608
|
process.stderr.write(
|
|
4491
|
-
` Auth token: ${
|
|
4609
|
+
` Auth required (token: ${hint}). Use Authorization: Bearer <TOKEN> header.
|
|
4492
4610
|
|
|
4493
4611
|
`
|
|
4494
4612
|
);
|
|
@@ -4522,6 +4640,11 @@ var DashboardApprovalChannel = class {
|
|
|
4522
4640
|
client.end();
|
|
4523
4641
|
}
|
|
4524
4642
|
this.sseClients.clear();
|
|
4643
|
+
this.sessions.clear();
|
|
4644
|
+
if (this.sessionCleanupTimer) {
|
|
4645
|
+
clearInterval(this.sessionCleanupTimer);
|
|
4646
|
+
this.sessionCleanupTimer = null;
|
|
4647
|
+
}
|
|
4525
4648
|
if (this.httpServer) {
|
|
4526
4649
|
return new Promise((resolve) => {
|
|
4527
4650
|
this.httpServer.close(() => resolve());
|
|
@@ -4542,7 +4665,8 @@ var DashboardApprovalChannel = class {
|
|
|
4542
4665
|
const timer = setTimeout(() => {
|
|
4543
4666
|
this.pending.delete(id);
|
|
4544
4667
|
const response = {
|
|
4545
|
-
|
|
4668
|
+
// SEC-002: Timeout ALWAYS denies. No configuration can change this.
|
|
4669
|
+
decision: "deny",
|
|
4546
4670
|
decided_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4547
4671
|
decided_by: "timeout"
|
|
4548
4672
|
};
|
|
@@ -4574,7 +4698,12 @@ var DashboardApprovalChannel = class {
|
|
|
4574
4698
|
// ── Authentication ──────────────────────────────────────────────────
|
|
4575
4699
|
/**
|
|
4576
4700
|
* Verify bearer token authentication.
|
|
4577
|
-
*
|
|
4701
|
+
*
|
|
4702
|
+
* SEC-012: The long-lived auth token is ONLY accepted via the Authorization
|
|
4703
|
+
* header — never in URL query strings. For SSE and page loads that cannot
|
|
4704
|
+
* set headers, a short-lived session token (obtained via POST /auth/session)
|
|
4705
|
+
* is accepted via ?session= query parameter.
|
|
4706
|
+
*
|
|
4578
4707
|
* Returns true if auth passes, false if blocked (response already sent).
|
|
4579
4708
|
*/
|
|
4580
4709
|
checkAuth(req, url, res) {
|
|
@@ -4586,19 +4715,71 @@ var DashboardApprovalChannel = class {
|
|
|
4586
4715
|
return true;
|
|
4587
4716
|
}
|
|
4588
4717
|
}
|
|
4589
|
-
const
|
|
4590
|
-
if (
|
|
4718
|
+
const sessionId = url.searchParams.get("session");
|
|
4719
|
+
if (sessionId && this.validateSession(sessionId)) {
|
|
4591
4720
|
return true;
|
|
4592
4721
|
}
|
|
4593
4722
|
res.writeHead(401, { "Content-Type": "application/json" });
|
|
4594
|
-
res.end(JSON.stringify({ error: "Unauthorized \u2014
|
|
4723
|
+
res.end(JSON.stringify({ error: "Unauthorized \u2014 use Authorization: Bearer header or a valid session" }));
|
|
4595
4724
|
return false;
|
|
4596
4725
|
}
|
|
4726
|
+
// ── Session Management (SEC-012) ──────────────────────────────────
|
|
4727
|
+
/**
|
|
4728
|
+
* Create a short-lived session by exchanging the long-lived auth token
|
|
4729
|
+
* (provided in the Authorization header) for a session ID.
|
|
4730
|
+
*/
|
|
4731
|
+
createSession() {
|
|
4732
|
+
if (this.sessions.size >= MAX_SESSIONS) {
|
|
4733
|
+
this.cleanupSessions();
|
|
4734
|
+
if (this.sessions.size >= MAX_SESSIONS) {
|
|
4735
|
+
const oldest = [...this.sessions.entries()].sort(
|
|
4736
|
+
(a, b) => a[1].created_at - b[1].created_at
|
|
4737
|
+
)[0];
|
|
4738
|
+
if (oldest) this.sessions.delete(oldest[0]);
|
|
4739
|
+
}
|
|
4740
|
+
}
|
|
4741
|
+
const id = crypto.randomBytes(32).toString("hex");
|
|
4742
|
+
const now = Date.now();
|
|
4743
|
+
this.sessions.set(id, {
|
|
4744
|
+
id,
|
|
4745
|
+
created_at: now,
|
|
4746
|
+
expires_at: now + SESSION_TTL_MS
|
|
4747
|
+
});
|
|
4748
|
+
return id;
|
|
4749
|
+
}
|
|
4750
|
+
/**
|
|
4751
|
+
* Validate a session ID — must exist and not be expired.
|
|
4752
|
+
*/
|
|
4753
|
+
validateSession(sessionId) {
|
|
4754
|
+
const session = this.sessions.get(sessionId);
|
|
4755
|
+
if (!session) return false;
|
|
4756
|
+
if (Date.now() > session.expires_at) {
|
|
4757
|
+
this.sessions.delete(sessionId);
|
|
4758
|
+
return false;
|
|
4759
|
+
}
|
|
4760
|
+
return true;
|
|
4761
|
+
}
|
|
4762
|
+
/**
|
|
4763
|
+
* Remove all expired sessions.
|
|
4764
|
+
*/
|
|
4765
|
+
cleanupSessions() {
|
|
4766
|
+
const now = Date.now();
|
|
4767
|
+
for (const [id, session] of this.sessions) {
|
|
4768
|
+
if (now > session.expires_at) {
|
|
4769
|
+
this.sessions.delete(id);
|
|
4770
|
+
}
|
|
4771
|
+
}
|
|
4772
|
+
}
|
|
4597
4773
|
// ── HTTP Request Handler ────────────────────────────────────────────
|
|
4598
4774
|
handleRequest(req, res) {
|
|
4599
4775
|
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
4600
4776
|
const method = req.method ?? "GET";
|
|
4601
|
-
|
|
4777
|
+
const origin = req.headers.origin;
|
|
4778
|
+
const protocol = this.useTLS ? "https" : "http";
|
|
4779
|
+
const selfOrigin = `${protocol}://${this.config.host}:${this.config.port}`;
|
|
4780
|
+
if (origin === selfOrigin) {
|
|
4781
|
+
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
4782
|
+
}
|
|
4602
4783
|
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
4603
4784
|
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
4604
4785
|
if (method === "OPTIONS") {
|
|
@@ -4608,6 +4789,10 @@ var DashboardApprovalChannel = class {
|
|
|
4608
4789
|
}
|
|
4609
4790
|
if (!this.checkAuth(req, url, res)) return;
|
|
4610
4791
|
try {
|
|
4792
|
+
if (method === "POST" && url.pathname === "/auth/session") {
|
|
4793
|
+
this.handleSessionExchange(req, res);
|
|
4794
|
+
return;
|
|
4795
|
+
}
|
|
4611
4796
|
if (method === "GET" && url.pathname === "/") {
|
|
4612
4797
|
this.serveDashboard(res);
|
|
4613
4798
|
} else if (method === "GET" && url.pathname === "/events") {
|
|
@@ -4634,6 +4819,40 @@ var DashboardApprovalChannel = class {
|
|
|
4634
4819
|
}
|
|
4635
4820
|
}
|
|
4636
4821
|
// ── Route Handlers ──────────────────────────────────────────────────
|
|
4822
|
+
/**
|
|
4823
|
+
* SEC-012: Exchange a long-lived auth token (in Authorization header)
|
|
4824
|
+
* for a short-lived session ID. The session ID can be used in URL
|
|
4825
|
+
* query parameters without exposing the long-lived credential.
|
|
4826
|
+
*
|
|
4827
|
+
* This endpoint performs its OWN auth check (header-only) because it
|
|
4828
|
+
* must reject query-parameter tokens and is called before the
|
|
4829
|
+
* normal checkAuth flow.
|
|
4830
|
+
*/
|
|
4831
|
+
handleSessionExchange(req, res) {
|
|
4832
|
+
if (!this.authToken) {
|
|
4833
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4834
|
+
res.end(JSON.stringify({ session_id: "no-auth" }));
|
|
4835
|
+
return;
|
|
4836
|
+
}
|
|
4837
|
+
const authHeader = req.headers.authorization;
|
|
4838
|
+
if (!authHeader) {
|
|
4839
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
4840
|
+
res.end(JSON.stringify({ error: "Authorization header required" }));
|
|
4841
|
+
return;
|
|
4842
|
+
}
|
|
4843
|
+
const parts = authHeader.split(" ");
|
|
4844
|
+
if (parts.length !== 2 || parts[0] !== "Bearer" || parts[1] !== this.authToken) {
|
|
4845
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
4846
|
+
res.end(JSON.stringify({ error: "Invalid bearer token" }));
|
|
4847
|
+
return;
|
|
4848
|
+
}
|
|
4849
|
+
const sessionId = this.createSession();
|
|
4850
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4851
|
+
res.end(JSON.stringify({
|
|
4852
|
+
session_id: sessionId,
|
|
4853
|
+
expires_in_seconds: SESSION_TTL_MS / 1e3
|
|
4854
|
+
}));
|
|
4855
|
+
}
|
|
4637
4856
|
serveDashboard(res) {
|
|
4638
4857
|
res.writeHead(200, {
|
|
4639
4858
|
"Content-Type": "text/html; charset=utf-8",
|
|
@@ -4659,7 +4878,8 @@ var DashboardApprovalChannel = class {
|
|
|
4659
4878
|
approval_channel: {
|
|
4660
4879
|
type: this.policy.approval_channel.type,
|
|
4661
4880
|
timeout_seconds: this.policy.approval_channel.timeout_seconds,
|
|
4662
|
-
auto_deny:
|
|
4881
|
+
auto_deny: true
|
|
4882
|
+
// SEC-002: hardcoded, not configurable
|
|
4663
4883
|
}
|
|
4664
4884
|
};
|
|
4665
4885
|
}
|
|
@@ -4700,7 +4920,8 @@ data: ${JSON.stringify(initData)}
|
|
|
4700
4920
|
approval_channel: {
|
|
4701
4921
|
type: this.policy.approval_channel.type,
|
|
4702
4922
|
timeout_seconds: this.policy.approval_channel.timeout_seconds,
|
|
4703
|
-
auto_deny:
|
|
4923
|
+
auto_deny: true
|
|
4924
|
+
// SEC-002: hardcoded, not configurable
|
|
4704
4925
|
}
|
|
4705
4926
|
};
|
|
4706
4927
|
}
|
|
@@ -4873,7 +5094,8 @@ var WebhookApprovalChannel = class {
|
|
|
4873
5094
|
const timer = setTimeout(() => {
|
|
4874
5095
|
this.pending.delete(id);
|
|
4875
5096
|
const response = {
|
|
4876
|
-
|
|
5097
|
+
// SEC-002: Timeout ALWAYS denies. No configuration can change this.
|
|
5098
|
+
decision: "deny",
|
|
4877
5099
|
decided_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4878
5100
|
decided_by: "timeout"
|
|
4879
5101
|
};
|
|
@@ -5061,16 +5283,29 @@ var ApprovalGate = class {
|
|
|
5061
5283
|
if (anomaly) {
|
|
5062
5284
|
return this.requestApproval(operation, 2, anomaly.reason, anomaly.context);
|
|
5063
5285
|
}
|
|
5064
|
-
this.
|
|
5065
|
-
|
|
5066
|
-
|
|
5286
|
+
if (this.policy.tier3_always_allow.includes(operation)) {
|
|
5287
|
+
this.auditLog.append("l2", `gate_allow:${operation}`, "system", {
|
|
5288
|
+
tier: 3,
|
|
5289
|
+
operation
|
|
5290
|
+
});
|
|
5291
|
+
return {
|
|
5292
|
+
allowed: true,
|
|
5293
|
+
tier: 3,
|
|
5294
|
+
reason: "Operation allowed (Tier 3)",
|
|
5295
|
+
approval_required: false
|
|
5296
|
+
};
|
|
5297
|
+
}
|
|
5298
|
+
this.auditLog.append("l2", `gate_unclassified:${operation}`, "system", {
|
|
5299
|
+
tier: 1,
|
|
5300
|
+
operation,
|
|
5301
|
+
warning: "Operation is not classified in any policy tier \u2014 defaulting to Tier 1 (require approval)"
|
|
5067
5302
|
});
|
|
5068
|
-
return
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5303
|
+
return this.requestApproval(
|
|
5304
|
+
operation,
|
|
5305
|
+
1,
|
|
5306
|
+
`"${operation}" is not classified in any policy tier \u2014 requires approval (SEC-011 safe default)`,
|
|
5307
|
+
{ operation, unclassified: true }
|
|
5308
|
+
);
|
|
5074
5309
|
}
|
|
5075
5310
|
/**
|
|
5076
5311
|
* Detect Tier 2 behavioral anomalies.
|
|
@@ -5243,7 +5478,8 @@ function createPrincipalPolicyTools(policy, baseline, auditLog) {
|
|
|
5243
5478
|
approval_channel: {
|
|
5244
5479
|
type: policy.approval_channel.type,
|
|
5245
5480
|
timeout_seconds: policy.approval_channel.timeout_seconds,
|
|
5246
|
-
auto_deny:
|
|
5481
|
+
auto_deny: true
|
|
5482
|
+
// SEC-002: hardcoded, not configurable
|
|
5247
5483
|
}
|
|
5248
5484
|
};
|
|
5249
5485
|
if (includeDefaults) {
|
|
@@ -5774,7 +6010,9 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog) {
|
|
|
5774
6010
|
return toolResult({
|
|
5775
6011
|
session_id: result.session.session_id,
|
|
5776
6012
|
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."
|
|
6013
|
+
instructions: "Send the 'response' object back to the initiator. When you receive their completion, pass it to sanctuary/handshake_status with this session_id.",
|
|
6014
|
+
// SEC-ADD-03: Tag response — contains SHR data that will be sent to counterparty
|
|
6015
|
+
_content_trust: "external"
|
|
5778
6016
|
});
|
|
5779
6017
|
}
|
|
5780
6018
|
},
|
|
@@ -5827,7 +6065,9 @@ function createHandshakeTools(config, identityManager, masterKey, auditLog) {
|
|
|
5827
6065
|
return toolResult({
|
|
5828
6066
|
completion: result.completion,
|
|
5829
6067
|
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."
|
|
6068
|
+
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.",
|
|
6069
|
+
// SEC-ADD-03: Tag response as containing counterparty-controlled SHR data
|
|
6070
|
+
_content_trust: "external"
|
|
5831
6071
|
});
|
|
5832
6072
|
}
|
|
5833
6073
|
},
|
|
@@ -6252,7 +6492,21 @@ function canonicalize(outcome) {
|
|
|
6252
6492
|
return stringToBytes(stableStringify(outcome));
|
|
6253
6493
|
}
|
|
6254
6494
|
function stableStringify(value) {
|
|
6255
|
-
if (value === null
|
|
6495
|
+
if (value === null) return "null";
|
|
6496
|
+
if (value === void 0) return "null";
|
|
6497
|
+
if (typeof value === "number") {
|
|
6498
|
+
if (!Number.isFinite(value)) {
|
|
6499
|
+
throw new Error(
|
|
6500
|
+
`Cannot canonicalize non-finite number: ${value}. NaN, Infinity, and -Infinity are not representable in JSON.`
|
|
6501
|
+
);
|
|
6502
|
+
}
|
|
6503
|
+
if (Object.is(value, -0)) {
|
|
6504
|
+
throw new Error(
|
|
6505
|
+
"Cannot canonicalize negative zero (-0). Use 0 instead for deterministic cross-language serialization."
|
|
6506
|
+
);
|
|
6507
|
+
}
|
|
6508
|
+
return JSON.stringify(value);
|
|
6509
|
+
}
|
|
6256
6510
|
if (typeof value !== "object") return JSON.stringify(value);
|
|
6257
6511
|
if (Array.isArray(value)) {
|
|
6258
6512
|
return "[" + value.map((v) => stableStringify(v)).join(",") + "]";
|
|
@@ -6280,11 +6534,12 @@ function createBridgeCommitment(outcome, identity, identityEncryptionKey, includ
|
|
|
6280
6534
|
bridge_commitment_id: commitmentId,
|
|
6281
6535
|
session_id: outcome.session_id,
|
|
6282
6536
|
sha256_commitment: sha2564.commitment,
|
|
6537
|
+
terms_hash: outcome.terms_hash,
|
|
6283
6538
|
committer_did: identity.did,
|
|
6284
6539
|
committed_at: now,
|
|
6285
6540
|
bridge_version: "sanctuary-concordia-bridge-v1"
|
|
6286
6541
|
};
|
|
6287
|
-
const payloadBytes = stringToBytes(
|
|
6542
|
+
const payloadBytes = stringToBytes(stableStringify(commitmentPayload));
|
|
6288
6543
|
const signature = sign(payloadBytes, identity.encrypted_private_key, identityEncryptionKey);
|
|
6289
6544
|
return {
|
|
6290
6545
|
bridge_commitment_id: commitmentId,
|
|
@@ -6310,11 +6565,12 @@ function verifyBridgeCommitment(commitment, outcome, committerPublicKey) {
|
|
|
6310
6565
|
bridge_commitment_id: commitment.bridge_commitment_id,
|
|
6311
6566
|
session_id: commitment.session_id,
|
|
6312
6567
|
sha256_commitment: commitment.sha256_commitment,
|
|
6568
|
+
terms_hash: outcome.terms_hash,
|
|
6313
6569
|
committer_did: commitment.committer_did,
|
|
6314
6570
|
committed_at: commitment.committed_at,
|
|
6315
6571
|
bridge_version: commitment.bridge_version
|
|
6316
6572
|
};
|
|
6317
|
-
const payloadBytes = stringToBytes(
|
|
6573
|
+
const payloadBytes = stringToBytes(stableStringify(commitmentPayload));
|
|
6318
6574
|
const sigBytes = fromBase64url(commitment.signature);
|
|
6319
6575
|
const signatureValid = verify(payloadBytes, sigBytes, committerPublicKey);
|
|
6320
6576
|
const sessionIdMatch = commitment.session_id === outcome.session_id;
|
|
@@ -6541,7 +6797,9 @@ function createBridgeTools(storage, masterKey, identityManager, auditLog, handsh
|
|
|
6541
6797
|
return toolResult({
|
|
6542
6798
|
...result,
|
|
6543
6799
|
session_id: storedCommitment.session_id,
|
|
6544
|
-
committer_did: storedCommitment.committer_did
|
|
6800
|
+
committer_did: storedCommitment.committer_did,
|
|
6801
|
+
// SEC-ADD-03: Tag response as containing counterparty-controlled data
|
|
6802
|
+
_content_trust: "external"
|
|
6545
6803
|
});
|
|
6546
6804
|
}
|
|
6547
6805
|
},
|
|
@@ -6635,6 +6893,668 @@ function createBridgeTools(storage, masterKey, identityManager, auditLog, handsh
|
|
|
6635
6893
|
];
|
|
6636
6894
|
return { tools };
|
|
6637
6895
|
}
|
|
6896
|
+
function lenientJsonParse(raw) {
|
|
6897
|
+
let cleaned = raw.replace(/\/\/[^\n]*/g, "");
|
|
6898
|
+
cleaned = cleaned.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
6899
|
+
cleaned = cleaned.replace(/,\s*([\]}])/g, "$1");
|
|
6900
|
+
return JSON.parse(cleaned);
|
|
6901
|
+
}
|
|
6902
|
+
async function fileExists(path) {
|
|
6903
|
+
try {
|
|
6904
|
+
await promises.access(path);
|
|
6905
|
+
return true;
|
|
6906
|
+
} catch {
|
|
6907
|
+
return false;
|
|
6908
|
+
}
|
|
6909
|
+
}
|
|
6910
|
+
async function safeReadFile(path) {
|
|
6911
|
+
try {
|
|
6912
|
+
return await promises.readFile(path, "utf-8");
|
|
6913
|
+
} catch {
|
|
6914
|
+
return null;
|
|
6915
|
+
}
|
|
6916
|
+
}
|
|
6917
|
+
async function detectEnvironment(config, deepScan) {
|
|
6918
|
+
const fingerprint = {
|
|
6919
|
+
sanctuary_installed: true,
|
|
6920
|
+
// We're running inside Sanctuary
|
|
6921
|
+
sanctuary_version: config.version,
|
|
6922
|
+
openclaw_detected: false,
|
|
6923
|
+
openclaw_version: null,
|
|
6924
|
+
openclaw_config: null,
|
|
6925
|
+
node_version: process.version,
|
|
6926
|
+
platform: `${process.platform}-${process.arch}`
|
|
6927
|
+
};
|
|
6928
|
+
if (!deepScan) {
|
|
6929
|
+
return fingerprint;
|
|
6930
|
+
}
|
|
6931
|
+
const home = os.homedir();
|
|
6932
|
+
const openclawConfigPath = path.join(home, ".openclaw", "openclaw.json");
|
|
6933
|
+
const openclawEnvPath = path.join(home, ".openclaw", ".env");
|
|
6934
|
+
const openclawMemoryPath = path.join(home, ".openclaw", "workspace", "MEMORY.md");
|
|
6935
|
+
const openclawMemoryDir = path.join(home, ".openclaw", "workspace", "memory");
|
|
6936
|
+
const configExists = await fileExists(openclawConfigPath);
|
|
6937
|
+
const envExists = await fileExists(openclawEnvPath);
|
|
6938
|
+
const memoryExists = await fileExists(openclawMemoryPath);
|
|
6939
|
+
const memoryDirExists = await fileExists(openclawMemoryDir);
|
|
6940
|
+
if (configExists || memoryExists || memoryDirExists) {
|
|
6941
|
+
fingerprint.openclaw_detected = true;
|
|
6942
|
+
fingerprint.openclaw_config = await auditOpenClawConfig(
|
|
6943
|
+
openclawConfigPath,
|
|
6944
|
+
openclawEnvPath,
|
|
6945
|
+
openclawMemoryPath,
|
|
6946
|
+
configExists,
|
|
6947
|
+
envExists,
|
|
6948
|
+
memoryExists
|
|
6949
|
+
);
|
|
6950
|
+
}
|
|
6951
|
+
return fingerprint;
|
|
6952
|
+
}
|
|
6953
|
+
async function auditOpenClawConfig(configPath, envPath, _memoryPath, configExists, envExists, memoryExists) {
|
|
6954
|
+
const audit = {
|
|
6955
|
+
config_path: configExists ? configPath : null,
|
|
6956
|
+
require_approval_enabled: false,
|
|
6957
|
+
sandbox_policy_active: false,
|
|
6958
|
+
sandbox_allow_list: [],
|
|
6959
|
+
sandbox_deny_list: [],
|
|
6960
|
+
memory_encrypted: false,
|
|
6961
|
+
// Stock OpenClaw never encrypts memory
|
|
6962
|
+
env_file_exposed: false,
|
|
6963
|
+
gateway_token_set: false,
|
|
6964
|
+
dm_pairing_enabled: false,
|
|
6965
|
+
mcp_bridge_active: false
|
|
6966
|
+
};
|
|
6967
|
+
if (configExists) {
|
|
6968
|
+
const raw = await safeReadFile(configPath);
|
|
6969
|
+
if (raw) {
|
|
6970
|
+
try {
|
|
6971
|
+
const parsed = lenientJsonParse(raw);
|
|
6972
|
+
const hooks = parsed.hooks;
|
|
6973
|
+
if (hooks) {
|
|
6974
|
+
const beforeToolCall = hooks.before_tool_call;
|
|
6975
|
+
if (beforeToolCall) {
|
|
6976
|
+
const hookStr = JSON.stringify(beforeToolCall);
|
|
6977
|
+
audit.require_approval_enabled = hookStr.includes("requireApproval");
|
|
6978
|
+
}
|
|
6979
|
+
}
|
|
6980
|
+
const tools = parsed.tools;
|
|
6981
|
+
if (tools) {
|
|
6982
|
+
const sandbox = tools.sandbox;
|
|
6983
|
+
if (sandbox) {
|
|
6984
|
+
const sandboxTools = sandbox.tools;
|
|
6985
|
+
if (sandboxTools) {
|
|
6986
|
+
audit.sandbox_policy_active = true;
|
|
6987
|
+
if (Array.isArray(sandboxTools.allow)) {
|
|
6988
|
+
audit.sandbox_allow_list = sandboxTools.allow.filter(
|
|
6989
|
+
(item) => typeof item === "string"
|
|
6990
|
+
);
|
|
6991
|
+
}
|
|
6992
|
+
if (Array.isArray(sandboxTools.alsoAllow)) {
|
|
6993
|
+
audit.sandbox_allow_list = [
|
|
6994
|
+
...audit.sandbox_allow_list,
|
|
6995
|
+
...sandboxTools.alsoAllow.filter(
|
|
6996
|
+
(item) => typeof item === "string"
|
|
6997
|
+
)
|
|
6998
|
+
];
|
|
6999
|
+
}
|
|
7000
|
+
if (Array.isArray(sandboxTools.deny)) {
|
|
7001
|
+
audit.sandbox_deny_list = sandboxTools.deny.filter(
|
|
7002
|
+
(item) => typeof item === "string"
|
|
7003
|
+
);
|
|
7004
|
+
}
|
|
7005
|
+
}
|
|
7006
|
+
}
|
|
7007
|
+
}
|
|
7008
|
+
const mcpServers = parsed.mcpServers;
|
|
7009
|
+
if (mcpServers && Object.keys(mcpServers).length > 0) {
|
|
7010
|
+
audit.mcp_bridge_active = true;
|
|
7011
|
+
}
|
|
7012
|
+
} catch {
|
|
7013
|
+
}
|
|
7014
|
+
}
|
|
7015
|
+
}
|
|
7016
|
+
if (envExists) {
|
|
7017
|
+
const envContent = await safeReadFile(envPath);
|
|
7018
|
+
if (envContent) {
|
|
7019
|
+
const secretPatterns = [
|
|
7020
|
+
/[A-Z_]*API_KEY\s*=/,
|
|
7021
|
+
/[A-Z_]*TOKEN\s*=/,
|
|
7022
|
+
/[A-Z_]*SECRET\s*=/,
|
|
7023
|
+
/[A-Z_]*PASSWORD\s*=/,
|
|
7024
|
+
/[A-Z_]*PRIVATE_KEY\s*=/
|
|
7025
|
+
];
|
|
7026
|
+
audit.env_file_exposed = secretPatterns.some((p) => p.test(envContent));
|
|
7027
|
+
audit.gateway_token_set = /OPENCLAW_GATEWAY_TOKEN\s*=/.test(envContent);
|
|
7028
|
+
}
|
|
7029
|
+
}
|
|
7030
|
+
if (memoryExists) {
|
|
7031
|
+
audit.memory_encrypted = false;
|
|
7032
|
+
}
|
|
7033
|
+
return audit;
|
|
7034
|
+
}
|
|
7035
|
+
|
|
7036
|
+
// src/audit/analyzer.ts
|
|
7037
|
+
var L1_ENCRYPTION_AT_REST = 10;
|
|
7038
|
+
var L1_IDENTITY_CRYPTOGRAPHIC = 10;
|
|
7039
|
+
var L1_INTEGRITY_VERIFICATION = 8;
|
|
7040
|
+
var L1_STATE_PORTABLE = 7;
|
|
7041
|
+
var L2_THREE_TIER_GATE = 10;
|
|
7042
|
+
var L2_BINARY_GATE = 3;
|
|
7043
|
+
var L2_ANOMALY_DETECTION = 7;
|
|
7044
|
+
var L2_ENCRYPTED_AUDIT = 5;
|
|
7045
|
+
var L2_TOOL_SANDBOXING = 3;
|
|
7046
|
+
var L3_COMMITMENT_SCHEME = 8;
|
|
7047
|
+
var L3_ZK_PROOFS = 7;
|
|
7048
|
+
var L3_DISCLOSURE_POLICIES = 5;
|
|
7049
|
+
var L4_PORTABLE_REPUTATION = 6;
|
|
7050
|
+
var L4_SIGNED_ATTESTATIONS = 6;
|
|
7051
|
+
var L4_SYBIL_DETECTION = 4;
|
|
7052
|
+
var L4_SOVEREIGNTY_GATED = 4;
|
|
7053
|
+
var SEVERITY_ORDER = {
|
|
7054
|
+
critical: 0,
|
|
7055
|
+
high: 1,
|
|
7056
|
+
medium: 2,
|
|
7057
|
+
low: 3
|
|
7058
|
+
};
|
|
7059
|
+
function analyzeSovereignty(env, config) {
|
|
7060
|
+
const l1 = assessL1(env, config);
|
|
7061
|
+
const l2 = assessL2(env);
|
|
7062
|
+
const l3 = assessL3(env);
|
|
7063
|
+
const l4 = assessL4(env);
|
|
7064
|
+
const l1Score = scoreL1(l1);
|
|
7065
|
+
const l2Score = scoreL2(l2);
|
|
7066
|
+
const l3Score = scoreL3(l3);
|
|
7067
|
+
const l4Score = scoreL4(l4);
|
|
7068
|
+
const overallScore = l1Score + l2Score + l3Score + l4Score;
|
|
7069
|
+
const sovereigntyLevel = overallScore >= 80 ? "full" : overallScore >= 50 ? "partial" : overallScore >= 20 ? "minimal" : "none";
|
|
7070
|
+
const gaps = generateGaps(env, l1, l2, l3, l4);
|
|
7071
|
+
gaps.sort((a, b) => SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity]);
|
|
7072
|
+
const recommendations = generateRecommendations(env, l1, l2, l3, l4);
|
|
7073
|
+
return {
|
|
7074
|
+
version: "1.0",
|
|
7075
|
+
audited_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7076
|
+
environment: env,
|
|
7077
|
+
layers: {
|
|
7078
|
+
l1_cognitive: l1,
|
|
7079
|
+
l2_operational: l2,
|
|
7080
|
+
l3_selective_disclosure: l3,
|
|
7081
|
+
l4_reputation: l4
|
|
7082
|
+
},
|
|
7083
|
+
overall_score: overallScore,
|
|
7084
|
+
sovereignty_level: sovereigntyLevel,
|
|
7085
|
+
gaps,
|
|
7086
|
+
recommendations
|
|
7087
|
+
};
|
|
7088
|
+
}
|
|
7089
|
+
function assessL1(env, config) {
|
|
7090
|
+
const findings = [];
|
|
7091
|
+
const sanctuaryActive = env.sanctuary_installed;
|
|
7092
|
+
const encryptionAtRest = sanctuaryActive;
|
|
7093
|
+
const keyCustody = sanctuaryActive ? "self" : "none";
|
|
7094
|
+
const integrityVerification = sanctuaryActive;
|
|
7095
|
+
const identityCryptographic = sanctuaryActive;
|
|
7096
|
+
const statePortable = sanctuaryActive;
|
|
7097
|
+
if (sanctuaryActive) {
|
|
7098
|
+
findings.push("AES-256-GCM encryption active for all state");
|
|
7099
|
+
findings.push(`Key derivation: ${config.state.key_derivation}`);
|
|
7100
|
+
findings.push(`Identity provider: ${config.state.identity_provider}`);
|
|
7101
|
+
findings.push("Merkle integrity verification enabled");
|
|
7102
|
+
findings.push("State export/import available");
|
|
7103
|
+
}
|
|
7104
|
+
if (env.openclaw_detected && env.openclaw_config) {
|
|
7105
|
+
if (!env.openclaw_config.memory_encrypted) {
|
|
7106
|
+
findings.push("OpenClaw agent memory (MEMORY.md, daily notes) stored in plaintext");
|
|
7107
|
+
}
|
|
7108
|
+
if (env.openclaw_config.env_file_exposed) {
|
|
7109
|
+
findings.push("OpenClaw .env file contains plaintext API keys/tokens");
|
|
7110
|
+
}
|
|
7111
|
+
}
|
|
7112
|
+
const status = encryptionAtRest && identityCryptographic ? "active" : encryptionAtRest || identityCryptographic ? "partial" : "inactive";
|
|
7113
|
+
return {
|
|
7114
|
+
status,
|
|
7115
|
+
encryption_at_rest: encryptionAtRest,
|
|
7116
|
+
key_custody: keyCustody,
|
|
7117
|
+
integrity_verification: integrityVerification,
|
|
7118
|
+
identity_cryptographic: identityCryptographic,
|
|
7119
|
+
state_portable: statePortable,
|
|
7120
|
+
findings
|
|
7121
|
+
};
|
|
7122
|
+
}
|
|
7123
|
+
function assessL2(env, _config) {
|
|
7124
|
+
const findings = [];
|
|
7125
|
+
const sanctuaryActive = env.sanctuary_installed;
|
|
7126
|
+
let approvalGate = "none";
|
|
7127
|
+
let behavioralAnomalyDetection = false;
|
|
7128
|
+
let auditTrailEncrypted = false;
|
|
7129
|
+
let auditTrailExists = false;
|
|
7130
|
+
let toolSandboxing = "none";
|
|
7131
|
+
if (sanctuaryActive) {
|
|
7132
|
+
approvalGate = "three-tier";
|
|
7133
|
+
behavioralAnomalyDetection = true;
|
|
7134
|
+
auditTrailEncrypted = true;
|
|
7135
|
+
auditTrailExists = true;
|
|
7136
|
+
findings.push("Three-tier Principal Policy gate active");
|
|
7137
|
+
findings.push("Behavioral anomaly detection (BaselineTracker) enabled");
|
|
7138
|
+
findings.push("Encrypted audit trail active");
|
|
7139
|
+
}
|
|
7140
|
+
if (env.openclaw_detected && env.openclaw_config) {
|
|
7141
|
+
if (env.openclaw_config.require_approval_enabled) {
|
|
7142
|
+
if (!sanctuaryActive) {
|
|
7143
|
+
approvalGate = "binary";
|
|
7144
|
+
}
|
|
7145
|
+
findings.push("OpenClaw requireApproval hook enabled (binary approve/deny)");
|
|
7146
|
+
}
|
|
7147
|
+
if (env.openclaw_config.sandbox_policy_active) {
|
|
7148
|
+
if (!sanctuaryActive) {
|
|
7149
|
+
toolSandboxing = "basic";
|
|
7150
|
+
}
|
|
7151
|
+
findings.push(
|
|
7152
|
+
`OpenClaw sandbox policy active (${env.openclaw_config.sandbox_allow_list.length} allowed, ${env.openclaw_config.sandbox_deny_list.length} denied)`
|
|
7153
|
+
);
|
|
7154
|
+
}
|
|
7155
|
+
}
|
|
7156
|
+
const status = approvalGate === "three-tier" && auditTrailEncrypted ? "active" : approvalGate !== "none" || auditTrailExists ? "partial" : "inactive";
|
|
7157
|
+
return {
|
|
7158
|
+
status,
|
|
7159
|
+
approval_gate: approvalGate,
|
|
7160
|
+
behavioral_anomaly_detection: behavioralAnomalyDetection,
|
|
7161
|
+
audit_trail_encrypted: auditTrailEncrypted,
|
|
7162
|
+
audit_trail_exists: auditTrailExists,
|
|
7163
|
+
tool_sandboxing: sanctuaryActive ? "policy-enforced" : toolSandboxing,
|
|
7164
|
+
findings
|
|
7165
|
+
};
|
|
7166
|
+
}
|
|
7167
|
+
function assessL3(env, _config) {
|
|
7168
|
+
const findings = [];
|
|
7169
|
+
const sanctuaryActive = env.sanctuary_installed;
|
|
7170
|
+
let commitmentScheme = "none";
|
|
7171
|
+
let zkProofs = false;
|
|
7172
|
+
let selectiveDisclosurePolicy = false;
|
|
7173
|
+
if (sanctuaryActive) {
|
|
7174
|
+
commitmentScheme = "pedersen+sha256";
|
|
7175
|
+
zkProofs = true;
|
|
7176
|
+
selectiveDisclosurePolicy = true;
|
|
7177
|
+
findings.push("SHA-256 + Pedersen commitment schemes active");
|
|
7178
|
+
findings.push("Schnorr ZK proofs and range proofs available");
|
|
7179
|
+
findings.push("Selective disclosure policies configurable");
|
|
7180
|
+
}
|
|
7181
|
+
const status = commitmentScheme === "pedersen+sha256" && zkProofs ? "active" : commitmentScheme !== "none" ? "partial" : "inactive";
|
|
7182
|
+
return {
|
|
7183
|
+
status,
|
|
7184
|
+
commitment_scheme: commitmentScheme,
|
|
7185
|
+
zero_knowledge_proofs: zkProofs,
|
|
7186
|
+
selective_disclosure_policy: selectiveDisclosurePolicy,
|
|
7187
|
+
findings
|
|
7188
|
+
};
|
|
7189
|
+
}
|
|
7190
|
+
function assessL4(env, _config) {
|
|
7191
|
+
const findings = [];
|
|
7192
|
+
const sanctuaryActive = env.sanctuary_installed;
|
|
7193
|
+
const reputationPortable = sanctuaryActive;
|
|
7194
|
+
const reputationSigned = sanctuaryActive;
|
|
7195
|
+
const sybilDetection = sanctuaryActive;
|
|
7196
|
+
const sovereigntyGated = sanctuaryActive;
|
|
7197
|
+
if (sanctuaryActive) {
|
|
7198
|
+
findings.push("Signed EAS-compatible attestations active");
|
|
7199
|
+
findings.push("Reputation export/import available");
|
|
7200
|
+
findings.push("Sybil detection heuristics enabled");
|
|
7201
|
+
findings.push("Sovereignty-gated reputation tiers active");
|
|
7202
|
+
} else {
|
|
7203
|
+
findings.push("No portable reputation system detected");
|
|
7204
|
+
}
|
|
7205
|
+
const status = reputationPortable && reputationSigned && sovereigntyGated ? "active" : reputationPortable || reputationSigned ? "partial" : "inactive";
|
|
7206
|
+
return {
|
|
7207
|
+
status,
|
|
7208
|
+
reputation_portable: reputationPortable,
|
|
7209
|
+
reputation_signed: reputationSigned,
|
|
7210
|
+
reputation_sybil_detection: sybilDetection,
|
|
7211
|
+
sovereignty_gated_tiers: sovereigntyGated,
|
|
7212
|
+
findings
|
|
7213
|
+
};
|
|
7214
|
+
}
|
|
7215
|
+
function scoreL1(l1) {
|
|
7216
|
+
let score = 0;
|
|
7217
|
+
if (l1.encryption_at_rest) score += L1_ENCRYPTION_AT_REST;
|
|
7218
|
+
if (l1.identity_cryptographic) score += L1_IDENTITY_CRYPTOGRAPHIC;
|
|
7219
|
+
if (l1.integrity_verification) score += L1_INTEGRITY_VERIFICATION;
|
|
7220
|
+
if (l1.state_portable) score += L1_STATE_PORTABLE;
|
|
7221
|
+
return score;
|
|
7222
|
+
}
|
|
7223
|
+
function scoreL2(l2) {
|
|
7224
|
+
let score = 0;
|
|
7225
|
+
if (l2.approval_gate === "three-tier") score += L2_THREE_TIER_GATE;
|
|
7226
|
+
else if (l2.approval_gate === "binary") score += L2_BINARY_GATE;
|
|
7227
|
+
if (l2.behavioral_anomaly_detection) score += L2_ANOMALY_DETECTION;
|
|
7228
|
+
if (l2.audit_trail_encrypted) score += L2_ENCRYPTED_AUDIT;
|
|
7229
|
+
if (l2.tool_sandboxing === "policy-enforced") score += L2_TOOL_SANDBOXING;
|
|
7230
|
+
else if (l2.tool_sandboxing === "basic") score += 1;
|
|
7231
|
+
return score;
|
|
7232
|
+
}
|
|
7233
|
+
function scoreL3(l3) {
|
|
7234
|
+
let score = 0;
|
|
7235
|
+
if (l3.commitment_scheme === "pedersen+sha256") score += L3_COMMITMENT_SCHEME;
|
|
7236
|
+
else if (l3.commitment_scheme === "sha256-only") score += 4;
|
|
7237
|
+
if (l3.zero_knowledge_proofs) score += L3_ZK_PROOFS;
|
|
7238
|
+
if (l3.selective_disclosure_policy) score += L3_DISCLOSURE_POLICIES;
|
|
7239
|
+
return score;
|
|
7240
|
+
}
|
|
7241
|
+
function scoreL4(l4) {
|
|
7242
|
+
let score = 0;
|
|
7243
|
+
if (l4.reputation_portable) score += L4_PORTABLE_REPUTATION;
|
|
7244
|
+
if (l4.reputation_signed) score += L4_SIGNED_ATTESTATIONS;
|
|
7245
|
+
if (l4.reputation_sybil_detection) score += L4_SYBIL_DETECTION;
|
|
7246
|
+
if (l4.sovereignty_gated_tiers) score += L4_SOVEREIGNTY_GATED;
|
|
7247
|
+
return score;
|
|
7248
|
+
}
|
|
7249
|
+
function generateGaps(env, l1, l2, l3, l4) {
|
|
7250
|
+
const gaps = [];
|
|
7251
|
+
const oc = env.openclaw_config;
|
|
7252
|
+
if (oc && !oc.memory_encrypted) {
|
|
7253
|
+
gaps.push({
|
|
7254
|
+
id: "GAP-L1-001",
|
|
7255
|
+
layer: "L1",
|
|
7256
|
+
severity: "critical",
|
|
7257
|
+
title: "Agent memory stored in plaintext",
|
|
7258
|
+
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.",
|
|
7259
|
+
openclaw_relevance: "Stock OpenClaw stores all agent memory in plaintext files. There is no built-in encryption for agent state.",
|
|
7260
|
+
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."
|
|
7261
|
+
});
|
|
7262
|
+
}
|
|
7263
|
+
if (oc && oc.env_file_exposed) {
|
|
7264
|
+
gaps.push({
|
|
7265
|
+
id: "GAP-L1-002",
|
|
7266
|
+
layer: "L1",
|
|
7267
|
+
severity: "critical",
|
|
7268
|
+
title: "Plaintext API keys in .env file",
|
|
7269
|
+
description: "Your .env file contains plaintext API keys and tokens. These secrets are readable by any process with filesystem access.",
|
|
7270
|
+
openclaw_relevance: "OpenClaw stores API keys (LLM providers, gateway tokens) in a plaintext .env file.",
|
|
7271
|
+
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'."
|
|
7272
|
+
});
|
|
7273
|
+
}
|
|
7274
|
+
if (!l1.identity_cryptographic) {
|
|
7275
|
+
gaps.push({
|
|
7276
|
+
id: "GAP-L1-003",
|
|
7277
|
+
layer: "L1",
|
|
7278
|
+
severity: "critical",
|
|
7279
|
+
title: "No cryptographic agent identity",
|
|
7280
|
+
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.",
|
|
7281
|
+
openclaw_relevance: env.openclaw_detected ? "OpenClaw has no cryptographic agent identity. Agent identity is implicit (tied to the process/session), not cryptographically verifiable." : null,
|
|
7282
|
+
sanctuary_solution: "Sanctuary provides Ed25519 self-custodied identity with key rotation and delegation. Use sanctuary/identity_create to establish your cryptographic identity."
|
|
7283
|
+
});
|
|
7284
|
+
}
|
|
7285
|
+
if (l2.approval_gate === "binary" && !l2.behavioral_anomaly_detection) {
|
|
7286
|
+
gaps.push({
|
|
7287
|
+
id: "GAP-L2-001",
|
|
7288
|
+
layer: "L2",
|
|
7289
|
+
severity: "high",
|
|
7290
|
+
title: "Binary approval gate (no anomaly detection)",
|
|
7291
|
+
description: "Your approval gate provides binary approve/deny gating without behavioral anomaly detection. Routine operations require the same manual approval as sensitive ones.",
|
|
7292
|
+
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,
|
|
7293
|
+
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."
|
|
7294
|
+
});
|
|
7295
|
+
} else if (l2.approval_gate === "none") {
|
|
7296
|
+
gaps.push({
|
|
7297
|
+
id: "GAP-L2-001",
|
|
7298
|
+
layer: "L2",
|
|
7299
|
+
severity: "critical",
|
|
7300
|
+
title: "No approval gate",
|
|
7301
|
+
description: "No approval gate is configured. All tool calls execute without oversight.",
|
|
7302
|
+
openclaw_relevance: null,
|
|
7303
|
+
sanctuary_solution: "Sanctuary's Principal Policy evaluates every tool call before execution. Enable it to get three-tier approval gating with behavioral anomaly detection."
|
|
7304
|
+
});
|
|
7305
|
+
}
|
|
7306
|
+
if (l2.tool_sandboxing === "basic") {
|
|
7307
|
+
gaps.push({
|
|
7308
|
+
id: "GAP-L2-002",
|
|
7309
|
+
layer: "L2",
|
|
7310
|
+
severity: "medium",
|
|
7311
|
+
title: "Basic tool sandboxing (no cryptographic attestation)",
|
|
7312
|
+
description: "Your tool sandbox enforces allow/deny lists but provides no cryptographic attestation of execution context.",
|
|
7313
|
+
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,
|
|
7314
|
+
sanctuary_solution: "Sanctuary provides cryptographic execution attestation via sanctuary/exec_attest and policy-enforced sandboxing with encrypted audit trails."
|
|
7315
|
+
});
|
|
7316
|
+
}
|
|
7317
|
+
if (!l2.audit_trail_exists) {
|
|
7318
|
+
gaps.push({
|
|
7319
|
+
id: "GAP-L2-003",
|
|
7320
|
+
layer: "L2",
|
|
7321
|
+
severity: "high",
|
|
7322
|
+
title: "No audit trail",
|
|
7323
|
+
description: "No audit trail exists for tool call history. There is no record of what operations were executed, when, or by whom.",
|
|
7324
|
+
openclaw_relevance: null,
|
|
7325
|
+
sanctuary_solution: "Sanctuary maintains an encrypted audit log of all operations, queryable via sanctuary/monitor_audit_log."
|
|
7326
|
+
});
|
|
7327
|
+
}
|
|
7328
|
+
if (l3.commitment_scheme === "none") {
|
|
7329
|
+
gaps.push({
|
|
7330
|
+
id: "GAP-L3-001",
|
|
7331
|
+
layer: "L3",
|
|
7332
|
+
severity: "high",
|
|
7333
|
+
title: "No selective disclosure capability",
|
|
7334
|
+
description: "Your agent has no way to prove facts about its state without revealing the state itself. Every disclosure is all-or-nothing.",
|
|
7335
|
+
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,
|
|
7336
|
+
sanctuary_solution: "Sanctuary's L3 provides SHA-256 + Pedersen commitments and Schnorr zero-knowledge proofs. 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."
|
|
7337
|
+
});
|
|
7338
|
+
}
|
|
7339
|
+
if (!l4.reputation_portable) {
|
|
7340
|
+
gaps.push({
|
|
7341
|
+
id: "GAP-L4-001",
|
|
7342
|
+
layer: "L4",
|
|
7343
|
+
severity: "high",
|
|
7344
|
+
title: "No portable reputation",
|
|
7345
|
+
description: "Your agent's reputation is platform-locked. If you move to a different harness or platform, your track record doesn't follow.",
|
|
7346
|
+
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,
|
|
7347
|
+
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."
|
|
7348
|
+
});
|
|
7349
|
+
}
|
|
7350
|
+
return gaps;
|
|
7351
|
+
}
|
|
7352
|
+
function generateRecommendations(env, l1, l2, l3, l4) {
|
|
7353
|
+
const recs = [];
|
|
7354
|
+
if (!l1.identity_cryptographic) {
|
|
7355
|
+
recs.push({
|
|
7356
|
+
priority: 1,
|
|
7357
|
+
action: "Create a cryptographic identity \u2014 your agent's foundation for all sovereignty operations",
|
|
7358
|
+
tool: "sanctuary/identity_create",
|
|
7359
|
+
effort: "immediate",
|
|
7360
|
+
impact: "critical"
|
|
7361
|
+
});
|
|
7362
|
+
}
|
|
7363
|
+
if (!l1.encryption_at_rest || env.openclaw_config && !env.openclaw_config.memory_encrypted) {
|
|
7364
|
+
recs.push({
|
|
7365
|
+
priority: 2,
|
|
7366
|
+
action: "Migrate plaintext agent state to Sanctuary's encrypted store",
|
|
7367
|
+
tool: "sanctuary/state_write",
|
|
7368
|
+
effort: "minutes",
|
|
7369
|
+
impact: "critical"
|
|
7370
|
+
});
|
|
7371
|
+
}
|
|
7372
|
+
recs.push({
|
|
7373
|
+
priority: 3,
|
|
7374
|
+
action: "Generate a Sovereignty Health Report to present to counterparties",
|
|
7375
|
+
tool: "sanctuary/shr_generate",
|
|
7376
|
+
effort: "immediate",
|
|
7377
|
+
impact: "high"
|
|
7378
|
+
});
|
|
7379
|
+
if (l2.approval_gate !== "three-tier") {
|
|
7380
|
+
recs.push({
|
|
7381
|
+
priority: 4,
|
|
7382
|
+
action: "Enable the three-tier Principal Policy gate for graduated approval",
|
|
7383
|
+
tool: "sanctuary/principal_policy_view",
|
|
7384
|
+
effort: "minutes",
|
|
7385
|
+
impact: "high"
|
|
7386
|
+
});
|
|
7387
|
+
}
|
|
7388
|
+
if (!l4.reputation_signed) {
|
|
7389
|
+
recs.push({
|
|
7390
|
+
priority: 5,
|
|
7391
|
+
action: "Start recording reputation attestations from completed interactions",
|
|
7392
|
+
tool: "sanctuary/reputation_record",
|
|
7393
|
+
effort: "minutes",
|
|
7394
|
+
impact: "medium"
|
|
7395
|
+
});
|
|
7396
|
+
}
|
|
7397
|
+
if (!l3.selective_disclosure_policy) {
|
|
7398
|
+
recs.push({
|
|
7399
|
+
priority: 6,
|
|
7400
|
+
action: "Configure selective disclosure policies for data sharing",
|
|
7401
|
+
tool: "sanctuary/disclosure_set_policy",
|
|
7402
|
+
effort: "hours",
|
|
7403
|
+
impact: "medium"
|
|
7404
|
+
});
|
|
7405
|
+
}
|
|
7406
|
+
return recs;
|
|
7407
|
+
}
|
|
7408
|
+
function formatAuditReport(result) {
|
|
7409
|
+
const { environment: env, layers, overall_score, sovereignty_level, gaps, recommendations } = result;
|
|
7410
|
+
const scoreBar = formatScoreBar(overall_score);
|
|
7411
|
+
const levelLabel = sovereignty_level.toUpperCase();
|
|
7412
|
+
let report = "";
|
|
7413
|
+
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";
|
|
7414
|
+
report += " SOVEREIGNTY AUDIT REPORT\n";
|
|
7415
|
+
report += ` Generated: ${result.audited_at}
|
|
7416
|
+
`;
|
|
7417
|
+
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";
|
|
7418
|
+
report += "\n";
|
|
7419
|
+
report += ` Overall Score: ${overall_score} / 100 ${scoreBar} ${levelLabel}
|
|
7420
|
+
`;
|
|
7421
|
+
report += "\n";
|
|
7422
|
+
report += " Environment:\n";
|
|
7423
|
+
report += ` \u2022 Sanctuary v${env.sanctuary_version ?? "?"} ${padDots("Sanctuary v" + (env.sanctuary_version ?? "?"))} ${env.sanctuary_installed ? "\u2713 installed" : "\u2717 not found"}
|
|
7424
|
+
`;
|
|
7425
|
+
if (env.openclaw_detected) {
|
|
7426
|
+
report += ` \u2022 OpenClaw ${padDots("OpenClaw")} \u2713 detected
|
|
7427
|
+
`;
|
|
7428
|
+
if (env.openclaw_config) {
|
|
7429
|
+
report += ` \u2022 OpenClaw requireApproval ${padDots("OpenClaw requireApproval")} ${env.openclaw_config.require_approval_enabled ? "\u2713 enabled" : "\u2717 disabled"}
|
|
7430
|
+
`;
|
|
7431
|
+
report += ` \u2022 OpenClaw sandbox policy ${padDots("OpenClaw sandbox policy")} ${env.openclaw_config.sandbox_policy_active ? "\u2713 active" : "\u2717 inactive"}
|
|
7432
|
+
`;
|
|
7433
|
+
}
|
|
7434
|
+
}
|
|
7435
|
+
report += "\n";
|
|
7436
|
+
const l1Score = scoreL1(layers.l1_cognitive);
|
|
7437
|
+
const l2Score = scoreL2(layers.l2_operational);
|
|
7438
|
+
const l3Score = scoreL3(layers.l3_selective_disclosure);
|
|
7439
|
+
const l4Score = scoreL4(layers.l4_reputation);
|
|
7440
|
+
report += " Layer Assessment:\n";
|
|
7441
|
+
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";
|
|
7442
|
+
report += " \u2502 Layer \u2502 Status \u2502 Score \u2502\n";
|
|
7443
|
+
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";
|
|
7444
|
+
report += ` \u2502 L1 Cognitive Sovereignty \u2502 ${padStatus(layers.l1_cognitive.status)} \u2502 ${padScore(l1Score, 35)} \u2502
|
|
7445
|
+
`;
|
|
7446
|
+
report += ` \u2502 L2 Operational Isolation \u2502 ${padStatus(layers.l2_operational.status)} \u2502 ${padScore(l2Score, 25)} \u2502
|
|
7447
|
+
`;
|
|
7448
|
+
report += ` \u2502 L3 Selective Disclosure \u2502 ${padStatus(layers.l3_selective_disclosure.status)} \u2502 ${padScore(l3Score, 20)} \u2502
|
|
7449
|
+
`;
|
|
7450
|
+
report += ` \u2502 L4 Verifiable Reputation \u2502 ${padStatus(layers.l4_reputation.status)} \u2502 ${padScore(l4Score, 20)} \u2502
|
|
7451
|
+
`;
|
|
7452
|
+
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";
|
|
7453
|
+
report += "\n";
|
|
7454
|
+
if (gaps.length > 0) {
|
|
7455
|
+
report += ` \u26A0 ${gaps.length} SOVEREIGNTY GAP${gaps.length !== 1 ? "S" : ""} FOUND
|
|
7456
|
+
`;
|
|
7457
|
+
report += "\n";
|
|
7458
|
+
for (const gap of gaps) {
|
|
7459
|
+
const severityLabel = `[${gap.severity.toUpperCase()}]`;
|
|
7460
|
+
report += ` ${severityLabel} ${gap.id}: ${gap.title}
|
|
7461
|
+
`;
|
|
7462
|
+
const descLines = wordWrap(gap.description, 66);
|
|
7463
|
+
for (const line of descLines) {
|
|
7464
|
+
report += ` ${line}
|
|
7465
|
+
`;
|
|
7466
|
+
}
|
|
7467
|
+
report += ` \u2192 Fix: ${gap.sanctuary_solution.split(".")[0]}.
|
|
7468
|
+
`;
|
|
7469
|
+
if (gap.openclaw_relevance) {
|
|
7470
|
+
report += ` \u2192 OpenClaw context: ${gap.openclaw_relevance.split(".")[0]}.
|
|
7471
|
+
`;
|
|
7472
|
+
}
|
|
7473
|
+
report += "\n";
|
|
7474
|
+
}
|
|
7475
|
+
} else {
|
|
7476
|
+
report += " \u2713 NO SOVEREIGNTY GAPS FOUND\n";
|
|
7477
|
+
report += "\n";
|
|
7478
|
+
}
|
|
7479
|
+
if (recommendations.length > 0) {
|
|
7480
|
+
report += " RECOMMENDED NEXT STEPS (in order):\n";
|
|
7481
|
+
for (const rec of recommendations) {
|
|
7482
|
+
const effortLabel = rec.effort === "immediate" ? "immediate" : rec.effort === "minutes" ? "5 min" : "30 min";
|
|
7483
|
+
report += ` ${rec.priority}. [${effortLabel}] ${rec.action}`;
|
|
7484
|
+
if (rec.tool) {
|
|
7485
|
+
report += `: ${rec.tool}`;
|
|
7486
|
+
}
|
|
7487
|
+
report += "\n";
|
|
7488
|
+
}
|
|
7489
|
+
report += "\n";
|
|
7490
|
+
}
|
|
7491
|
+
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";
|
|
7492
|
+
return report;
|
|
7493
|
+
}
|
|
7494
|
+
function formatScoreBar(score) {
|
|
7495
|
+
const filled = Math.round(score / 10);
|
|
7496
|
+
return "[" + "\u25A0".repeat(filled) + "\u2591".repeat(10 - filled) + "]";
|
|
7497
|
+
}
|
|
7498
|
+
function padDots(label) {
|
|
7499
|
+
const totalWidth = 30;
|
|
7500
|
+
const dotsNeeded = Math.max(2, totalWidth - label.length - 4);
|
|
7501
|
+
return ".".repeat(dotsNeeded);
|
|
7502
|
+
}
|
|
7503
|
+
function padStatus(status) {
|
|
7504
|
+
const label = status.toUpperCase();
|
|
7505
|
+
return label + " ".repeat(Math.max(0, 8 - label.length));
|
|
7506
|
+
}
|
|
7507
|
+
function padScore(score, max) {
|
|
7508
|
+
const text = `${score}/${max}`;
|
|
7509
|
+
return " ".repeat(Math.max(0, 5 - text.length)) + text;
|
|
7510
|
+
}
|
|
7511
|
+
function wordWrap(text, maxWidth) {
|
|
7512
|
+
const words = text.split(" ");
|
|
7513
|
+
const lines = [];
|
|
7514
|
+
let current = "";
|
|
7515
|
+
for (const word of words) {
|
|
7516
|
+
if (current.length + word.length + 1 > maxWidth && current.length > 0) {
|
|
7517
|
+
lines.push(current);
|
|
7518
|
+
current = word;
|
|
7519
|
+
} else {
|
|
7520
|
+
current = current.length > 0 ? current + " " + word : word;
|
|
7521
|
+
}
|
|
7522
|
+
}
|
|
7523
|
+
if (current.length > 0) lines.push(current);
|
|
7524
|
+
return lines;
|
|
7525
|
+
}
|
|
7526
|
+
|
|
7527
|
+
// src/audit/tools.ts
|
|
7528
|
+
function createAuditTools(config) {
|
|
7529
|
+
const tools = [
|
|
7530
|
+
{
|
|
7531
|
+
name: "sanctuary/sovereignty_audit",
|
|
7532
|
+
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.",
|
|
7533
|
+
inputSchema: {
|
|
7534
|
+
type: "object",
|
|
7535
|
+
properties: {
|
|
7536
|
+
deep_scan: {
|
|
7537
|
+
type: "boolean",
|
|
7538
|
+
description: "If true (default), also scans for OpenClaw config, .env files, and memory files. Set to false for a Sanctuary-only assessment."
|
|
7539
|
+
}
|
|
7540
|
+
}
|
|
7541
|
+
},
|
|
7542
|
+
handler: async (args) => {
|
|
7543
|
+
const deepScan = args.deep_scan !== false;
|
|
7544
|
+
const env = await detectEnvironment(config, deepScan);
|
|
7545
|
+
const result = analyzeSovereignty(env, config);
|
|
7546
|
+
const report = formatAuditReport(result);
|
|
7547
|
+
return {
|
|
7548
|
+
content: [
|
|
7549
|
+
{ type: "text", text: report },
|
|
7550
|
+
{ type: "text", text: JSON.stringify(result, null, 2) }
|
|
7551
|
+
]
|
|
7552
|
+
};
|
|
7553
|
+
}
|
|
7554
|
+
}
|
|
7555
|
+
];
|
|
7556
|
+
return { tools };
|
|
7557
|
+
}
|
|
6638
7558
|
|
|
6639
7559
|
// src/index.ts
|
|
6640
7560
|
init_encoding();
|
|
@@ -6671,15 +7591,51 @@ async function createSanctuaryServer(options) {
|
|
|
6671
7591
|
}
|
|
6672
7592
|
} else {
|
|
6673
7593
|
keyProtection = "recovery-key";
|
|
6674
|
-
const
|
|
6675
|
-
|
|
6676
|
-
|
|
6677
|
-
|
|
7594
|
+
const { hashToString: hashToString2 } = await Promise.resolve().then(() => (init_hashing(), hashing_exports));
|
|
7595
|
+
const { stringToBytes: stringToBytes2, bytesToString: bytesToString2 } = await Promise.resolve().then(() => (init_encoding(), encoding_exports));
|
|
7596
|
+
const { fromBase64url: fromBase64url2 } = await Promise.resolve().then(() => (init_encoding(), encoding_exports));
|
|
7597
|
+
const { constantTimeEqual: constantTimeEqual2 } = await Promise.resolve().then(() => (init_encoding(), encoding_exports));
|
|
7598
|
+
const existingHash = await storage.read("_meta", "recovery-key-hash");
|
|
7599
|
+
if (existingHash) {
|
|
7600
|
+
const envRecoveryKey = process.env.SANCTUARY_RECOVERY_KEY;
|
|
7601
|
+
if (!envRecoveryKey) {
|
|
7602
|
+
throw new Error(
|
|
7603
|
+
"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."
|
|
7604
|
+
);
|
|
7605
|
+
}
|
|
7606
|
+
let recoveryKeyBytes;
|
|
7607
|
+
try {
|
|
7608
|
+
recoveryKeyBytes = fromBase64url2(envRecoveryKey);
|
|
7609
|
+
} catch {
|
|
7610
|
+
throw new Error(
|
|
7611
|
+
"Sanctuary: SANCTUARY_RECOVERY_KEY is not valid base64url. The recovery key should be the exact string shown at first run."
|
|
7612
|
+
);
|
|
7613
|
+
}
|
|
7614
|
+
if (recoveryKeyBytes.length !== 32) {
|
|
7615
|
+
throw new Error(
|
|
7616
|
+
"Sanctuary: SANCTUARY_RECOVERY_KEY has incorrect length. The recovery key should be the exact string shown at first run."
|
|
7617
|
+
);
|
|
7618
|
+
}
|
|
7619
|
+
const providedHash = hashToString2(recoveryKeyBytes);
|
|
7620
|
+
const storedHash = bytesToString2(existingHash);
|
|
7621
|
+
const providedHashBytes = stringToBytes2(providedHash);
|
|
7622
|
+
const storedHashBytes = stringToBytes2(storedHash);
|
|
7623
|
+
if (!constantTimeEqual2(providedHashBytes, storedHashBytes)) {
|
|
7624
|
+
throw new Error(
|
|
7625
|
+
"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."
|
|
7626
|
+
);
|
|
7627
|
+
}
|
|
7628
|
+
masterKey = recoveryKeyBytes;
|
|
6678
7629
|
} else {
|
|
7630
|
+
const existingNamespaces = await storage.list("_meta");
|
|
7631
|
+
const hasKeyParams = existingNamespaces.some((e) => e.key === "key-params");
|
|
7632
|
+
if (hasKeyParams) {
|
|
7633
|
+
throw new Error(
|
|
7634
|
+
"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."
|
|
7635
|
+
);
|
|
7636
|
+
}
|
|
6679
7637
|
masterKey = generateRandomKey();
|
|
6680
7638
|
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
7639
|
const keyHash = hashToString2(masterKey);
|
|
6684
7640
|
await storage.write(
|
|
6685
7641
|
"_meta",
|
|
@@ -6945,6 +7901,7 @@ async function createSanctuaryServer(options) {
|
|
|
6945
7901
|
auditLog,
|
|
6946
7902
|
handshakeResults
|
|
6947
7903
|
);
|
|
7904
|
+
const { tools: auditTools } = createAuditTools(config);
|
|
6948
7905
|
const policy = await loadPrincipalPolicy(config.storage_path);
|
|
6949
7906
|
const baseline = new BaselineTracker(storage, masterKey);
|
|
6950
7907
|
await baseline.load();
|
|
@@ -6960,7 +7917,7 @@ async function createSanctuaryServer(options) {
|
|
|
6960
7917
|
port: config.dashboard.port,
|
|
6961
7918
|
host: config.dashboard.host,
|
|
6962
7919
|
timeout_seconds: policy.approval_channel.timeout_seconds,
|
|
6963
|
-
|
|
7920
|
+
// SEC-002: auto_deny removed — timeout always denies
|
|
6964
7921
|
auth_token: authToken,
|
|
6965
7922
|
tls: config.dashboard.tls
|
|
6966
7923
|
});
|
|
@@ -6973,8 +7930,8 @@ async function createSanctuaryServer(options) {
|
|
|
6973
7930
|
webhook_secret: config.webhook.secret,
|
|
6974
7931
|
callback_port: config.webhook.callback_port,
|
|
6975
7932
|
callback_host: config.webhook.callback_host,
|
|
6976
|
-
timeout_seconds: policy.approval_channel.timeout_seconds
|
|
6977
|
-
|
|
7933
|
+
timeout_seconds: policy.approval_channel.timeout_seconds
|
|
7934
|
+
// SEC-002: auto_deny removed — timeout always denies
|
|
6978
7935
|
});
|
|
6979
7936
|
await webhook.start();
|
|
6980
7937
|
approvalChannel = webhook;
|
|
@@ -6993,6 +7950,7 @@ async function createSanctuaryServer(options) {
|
|
|
6993
7950
|
...handshakeTools,
|
|
6994
7951
|
...federationTools,
|
|
6995
7952
|
...bridgeTools,
|
|
7953
|
+
...auditTools,
|
|
6996
7954
|
manifestTool
|
|
6997
7955
|
];
|
|
6998
7956
|
const server = createServer(allTools, { gate });
|