protect-mcp 0.4.1 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/{chunk-7HBHIKLN.mjs → chunk-VF3OCG4D.mjs} +422 -9
- package/dist/{chunk-VWUN6AI6.mjs → chunk-VIA2B65K.mjs} +1 -1
- package/dist/cli.js +697 -94
- package/dist/cli.mjs +190 -8
- package/dist/{http-transport-RIVV2RVQ.mjs → http-transport-VLIPOPIC.mjs} +1 -1
- package/dist/index.d.mts +1429 -2
- package/dist/index.d.ts +1429 -2
- package/dist/index.js +1728 -25
- package/dist/index.mjs +1273 -3
- package/package.json +4 -3
- package/policies/cedar/clinejection.cedar +50 -0
- package/policies/cedar/terraform-destroy.cedar +44 -0
- package/policies/clinejection.json +6 -0
- package/policies/data-exfiltration.json +6 -0
- package/policies/financial-safe.json +8 -0
- package/policies/github-mcp-hijack.json +6 -0
- package/policies/terraform-destroy.json +6 -0
package/dist/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,22 +17,54 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
|
|
20
30
|
// src/index.ts
|
|
21
31
|
var index_exports = {};
|
|
22
32
|
__export(index_exports, {
|
|
33
|
+
ConfidentialGate: () => ConfidentialGate,
|
|
23
34
|
ProtectGateway: () => ProtectGateway,
|
|
35
|
+
ReceiptPropagator: () => ReceiptPropagator,
|
|
36
|
+
anchorToRekor: () => anchorToRekor,
|
|
24
37
|
buildDecisionContext: () => buildDecisionContext,
|
|
25
38
|
checkRateLimit: () => checkRateLimit,
|
|
26
39
|
collectSignedReceipts: () => collectSignedReceipts,
|
|
40
|
+
computeCalibration: () => computeCalibration,
|
|
41
|
+
confidentialInference: () => confidentialInference,
|
|
42
|
+
createApprovalChallenge: () => createApprovalChallenge,
|
|
43
|
+
createApprovalReceiptPayload: () => createApprovalReceiptPayload,
|
|
44
|
+
createAttestationField: () => createAttestationField,
|
|
27
45
|
createAuditBundle: () => createAuditBundle,
|
|
46
|
+
createC2PAManifest: () => createC2PAManifest,
|
|
47
|
+
createDisclosurePackage: () => createDisclosurePackage,
|
|
48
|
+
createEvidenceAttestation: () => createEvidenceAttestation,
|
|
49
|
+
createLogAnchorField: () => createLogAnchorField,
|
|
50
|
+
createReceiptChannel: () => createReceiptChannel,
|
|
51
|
+
createSandbox: () => createSandbox,
|
|
52
|
+
destroySandbox: () => destroySandbox,
|
|
53
|
+
ed25519ToDIDKey: () => ed25519ToDIDKey,
|
|
28
54
|
evaluateTier: () => evaluateTier,
|
|
55
|
+
exportC2PAManifestJSON: () => exportC2PAManifestJSON,
|
|
56
|
+
exportJSONL: () => exportJSONL,
|
|
29
57
|
formatReportMarkdown: () => formatReportMarkdown,
|
|
30
58
|
formatSimulation: () => formatSimulation,
|
|
59
|
+
generateC2PACommand: () => generateC2PACommand,
|
|
60
|
+
generateDatasetCard: () => generateDatasetCard,
|
|
61
|
+
generateHFMetadata: () => generateHFMetadata,
|
|
31
62
|
generateReport: () => generateReport,
|
|
63
|
+
generateSafetyTranscript: () => generateSafetyTranscript,
|
|
32
64
|
getSignerInfo: () => getSignerInfo,
|
|
33
65
|
getToolPolicy: () => getToolPolicy,
|
|
66
|
+
hashReceipt: () => hashReceipt,
|
|
67
|
+
hashResponseBody: () => hashResponseBody,
|
|
34
68
|
initSigning: () => initSigning,
|
|
35
69
|
isAgentId: () => isAgentId,
|
|
36
70
|
isDisclosureMode: () => isDisclosureMode,
|
|
@@ -39,25 +73,42 @@ __export(index_exports, {
|
|
|
39
73
|
isSigningEnabled: () => isSigningEnabled,
|
|
40
74
|
listCredentialLabels: () => listCredentialLabels,
|
|
41
75
|
loadPolicy: () => loadPolicy,
|
|
76
|
+
manifestToVC: () => manifestToVC,
|
|
42
77
|
meetsMinTier: () => meetsMinTier,
|
|
43
78
|
parseLogFile: () => parseLogFile,
|
|
79
|
+
parseNotificationConfigFromEnv: () => parseNotificationConfigFromEnv,
|
|
44
80
|
parseRateLimit: () => parseRateLimit,
|
|
45
81
|
queryExternalPDP: () => queryExternalPDP,
|
|
82
|
+
receiptToVP: () => receiptToVP,
|
|
83
|
+
receiptsToHFRows: () => receiptsToHFRows,
|
|
84
|
+
redactFields: () => redactFields,
|
|
46
85
|
resolveCredential: () => resolveCredential,
|
|
86
|
+
revealField: () => revealField,
|
|
87
|
+
runInSandbox: () => runInSandbox,
|
|
88
|
+
sendApprovalNotification: () => sendApprovalNotification,
|
|
47
89
|
signDecision: () => signDecision,
|
|
48
90
|
simulate: () => simulate,
|
|
91
|
+
toCredentialRequestOptions: () => toCredentialRequestOptions,
|
|
92
|
+
toManifoldFormat: () => toManifoldFormat,
|
|
93
|
+
toMetaculusFormat: () => toMetaculusFormat,
|
|
49
94
|
validateCredentials: () => validateCredentials,
|
|
50
95
|
validateEvidenceReceipt: () => validateEvidenceReceipt,
|
|
51
|
-
validateManifest: () => validateManifest
|
|
96
|
+
validateManifest: () => validateManifest,
|
|
97
|
+
verifyActaC2PAAssertions: () => verifyActaC2PAAssertions,
|
|
98
|
+
verifyAllCommitments: () => verifyAllCommitments,
|
|
99
|
+
verifyApprovalAssertion: () => verifyApprovalAssertion,
|
|
100
|
+
verifyCommitment: () => verifyCommitment,
|
|
101
|
+
verifyEvidenceAttestation: () => verifyEvidenceAttestation,
|
|
102
|
+
verifyRekorAnchor: () => verifyRekorAnchor
|
|
52
103
|
});
|
|
53
104
|
module.exports = __toCommonJS(index_exports);
|
|
54
105
|
|
|
55
106
|
// src/gateway.ts
|
|
56
107
|
var import_node_child_process = require("child_process");
|
|
57
|
-
var
|
|
108
|
+
var import_node_crypto3 = require("crypto");
|
|
58
109
|
var import_node_readline = require("readline");
|
|
59
|
-
var
|
|
60
|
-
var
|
|
110
|
+
var import_node_fs6 = require("fs");
|
|
111
|
+
var import_node_path4 = require("path");
|
|
61
112
|
|
|
62
113
|
// src/policy.ts
|
|
63
114
|
var import_node_crypto = require("crypto");
|
|
@@ -625,10 +676,334 @@ function buildDecisionContext(toolName, tier, opts) {
|
|
|
625
676
|
};
|
|
626
677
|
}
|
|
627
678
|
|
|
628
|
-
// src/
|
|
629
|
-
var
|
|
679
|
+
// src/cedar-evaluator.ts
|
|
680
|
+
var import_node_crypto2 = require("crypto");
|
|
630
681
|
var import_node_fs4 = require("fs");
|
|
631
682
|
var import_node_path2 = require("path");
|
|
683
|
+
var cedarWasm = null;
|
|
684
|
+
var loadAttempted = false;
|
|
685
|
+
async function ensureCedarWasm() {
|
|
686
|
+
if (cedarWasm) return true;
|
|
687
|
+
if (loadAttempted) return false;
|
|
688
|
+
loadAttempted = true;
|
|
689
|
+
try {
|
|
690
|
+
const moduleName = "@cedar-policy/cedar-wasm";
|
|
691
|
+
cedarWasm = await import(
|
|
692
|
+
/* @vite-ignore */
|
|
693
|
+
moduleName
|
|
694
|
+
);
|
|
695
|
+
return true;
|
|
696
|
+
} catch {
|
|
697
|
+
return false;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
function buildEntities(req) {
|
|
701
|
+
const agentId = req.agentId || req.tier;
|
|
702
|
+
return [
|
|
703
|
+
{
|
|
704
|
+
uid: { type: "Agent", id: agentId },
|
|
705
|
+
attrs: {
|
|
706
|
+
tier: req.tier,
|
|
707
|
+
...req.agentId ? { agent_id: req.agentId } : {}
|
|
708
|
+
},
|
|
709
|
+
parents: []
|
|
710
|
+
},
|
|
711
|
+
{
|
|
712
|
+
uid: { type: "Tool", id: req.tool },
|
|
713
|
+
attrs: {},
|
|
714
|
+
parents: []
|
|
715
|
+
}
|
|
716
|
+
];
|
|
717
|
+
}
|
|
718
|
+
async function evaluateCedar(policySet, req) {
|
|
719
|
+
const available = await ensureCedarWasm();
|
|
720
|
+
if (!available) {
|
|
721
|
+
return {
|
|
722
|
+
allowed: true,
|
|
723
|
+
reason: "cedar_wasm_not_available",
|
|
724
|
+
metadata: { fallback: true }
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
try {
|
|
728
|
+
const agentId = req.agentId || req.tier;
|
|
729
|
+
const authRequest = {
|
|
730
|
+
principal: { type: "Agent", id: agentId },
|
|
731
|
+
action: { type: "Action", id: "MCP::Tool::call" },
|
|
732
|
+
resource: { type: "Tool", id: req.tool },
|
|
733
|
+
context: {
|
|
734
|
+
tier: req.tier,
|
|
735
|
+
...req.context || {}
|
|
736
|
+
}
|
|
737
|
+
};
|
|
738
|
+
const entities = buildEntities(req);
|
|
739
|
+
let result;
|
|
740
|
+
if (typeof cedarWasm.isAuthorized === "function") {
|
|
741
|
+
result = cedarWasm.isAuthorized({
|
|
742
|
+
policies: policySet.source,
|
|
743
|
+
entities,
|
|
744
|
+
principal: authRequest.principal,
|
|
745
|
+
action: authRequest.action,
|
|
746
|
+
resource: authRequest.resource,
|
|
747
|
+
context: authRequest.context,
|
|
748
|
+
schema: null
|
|
749
|
+
// No schema enforcement — Cedar still evaluates correctly
|
|
750
|
+
});
|
|
751
|
+
} else if (typeof cedarWasm.checkAuthorization === "function") {
|
|
752
|
+
result = cedarWasm.checkAuthorization(
|
|
753
|
+
policySet.source,
|
|
754
|
+
JSON.stringify(entities),
|
|
755
|
+
JSON.stringify(authRequest)
|
|
756
|
+
);
|
|
757
|
+
} else {
|
|
758
|
+
const cedarEngine = cedarWasm.default || cedarWasm;
|
|
759
|
+
if (typeof cedarEngine.isAuthorized === "function") {
|
|
760
|
+
result = cedarEngine.isAuthorized({
|
|
761
|
+
policies: policySet.source,
|
|
762
|
+
entities,
|
|
763
|
+
principal: authRequest.principal,
|
|
764
|
+
action: authRequest.action,
|
|
765
|
+
resource: authRequest.resource,
|
|
766
|
+
context: authRequest.context,
|
|
767
|
+
schema: null
|
|
768
|
+
});
|
|
769
|
+
} else {
|
|
770
|
+
return {
|
|
771
|
+
allowed: true,
|
|
772
|
+
reason: "cedar_wasm_api_unsupported",
|
|
773
|
+
metadata: { fallback: true, exports: Object.keys(cedarWasm) }
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
const decision = parseWasmResult(result);
|
|
778
|
+
return {
|
|
779
|
+
allowed: decision.allowed,
|
|
780
|
+
reason: decision.allowed ? void 0 : `cedar_deny${decision.diagnostics ? ": " + decision.diagnostics : ""}`,
|
|
781
|
+
metadata: {
|
|
782
|
+
policy_digest: policySet.digest,
|
|
783
|
+
...decision.matchedPolicies ? { matched_policies: decision.matchedPolicies } : {}
|
|
784
|
+
}
|
|
785
|
+
};
|
|
786
|
+
} catch (err) {
|
|
787
|
+
return {
|
|
788
|
+
allowed: true,
|
|
789
|
+
reason: `cedar_eval_error: ${err instanceof Error ? err.message : "unknown"}`,
|
|
790
|
+
metadata: { fallback: true, error: true }
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
function parseWasmResult(result) {
|
|
795
|
+
if (!result) {
|
|
796
|
+
return { allowed: true, diagnostics: "null result from Cedar WASM" };
|
|
797
|
+
}
|
|
798
|
+
if (result.type === "allow" || result.type === "Allow") {
|
|
799
|
+
return { allowed: true };
|
|
800
|
+
}
|
|
801
|
+
if (result.type === "deny" || result.type === "Deny") {
|
|
802
|
+
return {
|
|
803
|
+
allowed: false,
|
|
804
|
+
diagnostics: result.diagnostics ? JSON.stringify(result.diagnostics) : void 0,
|
|
805
|
+
matchedPolicies: result.diagnostics?.reasons
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
if (result.decision === "Allow") {
|
|
809
|
+
return { allowed: true };
|
|
810
|
+
}
|
|
811
|
+
if (result.decision === "Deny") {
|
|
812
|
+
return {
|
|
813
|
+
allowed: false,
|
|
814
|
+
diagnostics: result.diagnostics ? JSON.stringify(result.diagnostics) : void 0
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
if (typeof result === "boolean") {
|
|
818
|
+
return { allowed: result };
|
|
819
|
+
}
|
|
820
|
+
return { allowed: true, diagnostics: `unknown result format: ${JSON.stringify(result)}` };
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// src/notifications.ts
|
|
824
|
+
async function sendApprovalNotification(config, notification) {
|
|
825
|
+
const promises = [];
|
|
826
|
+
if (config.sms) {
|
|
827
|
+
promises.push(sendSms(config.sms, notification));
|
|
828
|
+
}
|
|
829
|
+
if (config.webhook) {
|
|
830
|
+
promises.push(sendWebhook(config.webhook, notification));
|
|
831
|
+
}
|
|
832
|
+
if (config.email) {
|
|
833
|
+
promises.push(sendEmail(config.email, notification));
|
|
834
|
+
}
|
|
835
|
+
const results = await Promise.allSettled(promises);
|
|
836
|
+
for (const result of results) {
|
|
837
|
+
if (result.status === "rejected") {
|
|
838
|
+
console.error(`[protect-mcp] Notification failed: ${result.reason}`);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
async function sendSms(config, notification) {
|
|
843
|
+
const body = [
|
|
844
|
+
`\u{1F512} Approval Required`,
|
|
845
|
+
`Tool: ${notification.toolName}`,
|
|
846
|
+
notification.agentId ? `Agent: ${notification.agentId}` : null,
|
|
847
|
+
`Reason: ${notification.reason}`,
|
|
848
|
+
notification.approveUrl ? `Approve: ${notification.approveUrl}` : null,
|
|
849
|
+
notification.traceUrl ? `Trace: ${notification.traceUrl}` : null
|
|
850
|
+
].filter(Boolean).join("\n");
|
|
851
|
+
const params = new URLSearchParams({
|
|
852
|
+
To: config.to,
|
|
853
|
+
From: config.from,
|
|
854
|
+
Body: body
|
|
855
|
+
});
|
|
856
|
+
const response = await fetch(
|
|
857
|
+
`https://api.twilio.com/2010-04-01/Accounts/${config.accountSid}/Messages.json`,
|
|
858
|
+
{
|
|
859
|
+
method: "POST",
|
|
860
|
+
headers: {
|
|
861
|
+
Authorization: `Basic ${Buffer.from(`${config.accountSid}:${config.authToken}`).toString("base64")}`,
|
|
862
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
863
|
+
},
|
|
864
|
+
body: params.toString()
|
|
865
|
+
}
|
|
866
|
+
);
|
|
867
|
+
if (!response.ok) {
|
|
868
|
+
throw new Error(`Twilio SMS failed: ${response.status} ${await response.text()}`);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
async function sendWebhook(config, notification) {
|
|
872
|
+
let payload;
|
|
873
|
+
if (config.template === "slack") {
|
|
874
|
+
payload = {
|
|
875
|
+
blocks: [
|
|
876
|
+
{
|
|
877
|
+
type: "header",
|
|
878
|
+
text: { type: "plain_text", text: "\u{1F512} Agent Approval Required" }
|
|
879
|
+
},
|
|
880
|
+
{
|
|
881
|
+
type: "section",
|
|
882
|
+
fields: [
|
|
883
|
+
{ type: "mrkdwn", text: `*Tool:*
|
|
884
|
+
\`${notification.toolName}\`` },
|
|
885
|
+
{ type: "mrkdwn", text: `*Agent:*
|
|
886
|
+
${notification.agentId || "unknown"}` },
|
|
887
|
+
{ type: "mrkdwn", text: `*Policy:*
|
|
888
|
+
${notification.policyName || "default"}` },
|
|
889
|
+
{ type: "mrkdwn", text: `*Time:*
|
|
890
|
+
${notification.timestamp}` }
|
|
891
|
+
]
|
|
892
|
+
},
|
|
893
|
+
{
|
|
894
|
+
type: "section",
|
|
895
|
+
text: { type: "mrkdwn", text: `*Reason:* ${notification.reason}` }
|
|
896
|
+
},
|
|
897
|
+
...notification.approveUrl || notification.traceUrl ? [
|
|
898
|
+
{
|
|
899
|
+
type: "actions",
|
|
900
|
+
elements: [
|
|
901
|
+
...notification.approveUrl ? [{ type: "button", text: { type: "plain_text", text: "\u2705 Approve" }, url: notification.approveUrl, style: "primary" }] : [],
|
|
902
|
+
...notification.traceUrl ? [{ type: "button", text: { type: "plain_text", text: "\u{1F50D} View Trace" }, url: notification.traceUrl }] : []
|
|
903
|
+
]
|
|
904
|
+
}
|
|
905
|
+
] : []
|
|
906
|
+
]
|
|
907
|
+
};
|
|
908
|
+
} else if (config.template === "pagerduty") {
|
|
909
|
+
payload = {
|
|
910
|
+
routing_key: config.headers?.["X-Routing-Key"] || "",
|
|
911
|
+
event_action: "trigger",
|
|
912
|
+
payload: {
|
|
913
|
+
summary: `Agent approval required: ${notification.toolName}`,
|
|
914
|
+
source: "protect-mcp",
|
|
915
|
+
severity: "warning",
|
|
916
|
+
custom_details: {
|
|
917
|
+
tool: notification.toolName,
|
|
918
|
+
agent: notification.agentId,
|
|
919
|
+
policy: notification.policyName,
|
|
920
|
+
reason: notification.reason,
|
|
921
|
+
trace_url: notification.traceUrl,
|
|
922
|
+
approve_url: notification.approveUrl
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
};
|
|
926
|
+
} else {
|
|
927
|
+
payload = notification;
|
|
928
|
+
}
|
|
929
|
+
const response = await fetch(config.url, {
|
|
930
|
+
method: config.method || "POST",
|
|
931
|
+
headers: {
|
|
932
|
+
"Content-Type": "application/json",
|
|
933
|
+
...config.headers
|
|
934
|
+
},
|
|
935
|
+
body: JSON.stringify(payload)
|
|
936
|
+
});
|
|
937
|
+
if (!response.ok) {
|
|
938
|
+
throw new Error(`Webhook failed: ${response.status}`);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
async function sendEmail(config, notification) {
|
|
942
|
+
if (!config.resendApiKey) {
|
|
943
|
+
console.warn("[protect-mcp] Email notification skipped: no resendApiKey configured");
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
const html = `
|
|
947
|
+
<div style="font-family: monospace; padding: 20px; background: #0d1117; color: #c9d1d9; border-radius: 8px;">
|
|
948
|
+
<h2 style="color: #10b981;">\u{1F512} Agent Approval Required</h2>
|
|
949
|
+
<table style="font-size: 14px; margin: 16px 0;">
|
|
950
|
+
<tr><td style="color: #8b949e; padding: 4px 16px 4px 0;">Tool:</td><td>${notification.toolName}</td></tr>
|
|
951
|
+
<tr><td style="color: #8b949e; padding: 4px 16px 4px 0;">Agent:</td><td>${notification.agentId || "unknown"}</td></tr>
|
|
952
|
+
<tr><td style="color: #8b949e; padding: 4px 16px 4px 0;">Reason:</td><td>${notification.reason}</td></tr>
|
|
953
|
+
<tr><td style="color: #8b949e; padding: 4px 16px 4px 0;">Time:</td><td>${notification.timestamp}</td></tr>
|
|
954
|
+
</table>
|
|
955
|
+
${notification.approveUrl ? `<a href="${notification.approveUrl}" style="background: #10b981; color: white; padding: 8px 16px; border-radius: 6px; text-decoration: none; margin-right: 8px;">\u2705 Approve</a>` : ""}
|
|
956
|
+
${notification.traceUrl ? `<a href="${notification.traceUrl}" style="background: #1f2937; color: #c9d1d9; padding: 8px 16px; border-radius: 6px; text-decoration: none; border: 1px solid #374151;">\u{1F50D} View Trace</a>` : ""}
|
|
957
|
+
</div>
|
|
958
|
+
`;
|
|
959
|
+
const response = await fetch("https://api.resend.com/emails", {
|
|
960
|
+
method: "POST",
|
|
961
|
+
headers: {
|
|
962
|
+
Authorization: `Bearer ${config.resendApiKey}`,
|
|
963
|
+
"Content-Type": "application/json"
|
|
964
|
+
},
|
|
965
|
+
body: JSON.stringify({
|
|
966
|
+
from: "ScopeBlind <noreply@scopeblind.com>",
|
|
967
|
+
to: config.to,
|
|
968
|
+
subject: `\u{1F512} Approval required: ${notification.toolName}`,
|
|
969
|
+
html
|
|
970
|
+
})
|
|
971
|
+
});
|
|
972
|
+
if (!response.ok) {
|
|
973
|
+
throw new Error(`Resend email failed: ${response.status}`);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
function parseNotificationConfigFromEnv() {
|
|
977
|
+
const config = {};
|
|
978
|
+
let hasConfig = false;
|
|
979
|
+
const smsTo = process.env.SCOPEBLIND_SMS_TO;
|
|
980
|
+
const twilioSid = process.env.TWILIO_ACCOUNT_SID;
|
|
981
|
+
const twilioToken = process.env.TWILIO_AUTH_TOKEN;
|
|
982
|
+
const twilioFrom = process.env.TWILIO_FROM_NUMBER;
|
|
983
|
+
if (smsTo && twilioSid && twilioToken && twilioFrom) {
|
|
984
|
+
config.sms = { accountSid: twilioSid, authToken: twilioToken, from: twilioFrom, to: smsTo };
|
|
985
|
+
hasConfig = true;
|
|
986
|
+
}
|
|
987
|
+
const webhookUrl = process.env.SCOPEBLIND_WEBHOOK_URL;
|
|
988
|
+
if (webhookUrl) {
|
|
989
|
+
config.webhook = {
|
|
990
|
+
url: webhookUrl,
|
|
991
|
+
template: process.env.SCOPEBLIND_WEBHOOK_TEMPLATE || "custom"
|
|
992
|
+
};
|
|
993
|
+
hasConfig = true;
|
|
994
|
+
}
|
|
995
|
+
const emailTo = process.env.SCOPEBLIND_EMAIL_TO;
|
|
996
|
+
if (emailTo) {
|
|
997
|
+
config.email = { to: emailTo, resendApiKey: process.env.RESEND_API_KEY };
|
|
998
|
+
hasConfig = true;
|
|
999
|
+
}
|
|
1000
|
+
return hasConfig ? config : null;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// src/http-server.ts
|
|
1004
|
+
var import_node_http = require("http");
|
|
1005
|
+
var import_node_fs5 = require("fs");
|
|
1006
|
+
var import_node_path3 = require("path");
|
|
632
1007
|
var LOG_FILE = ".protect-mcp-log.jsonl";
|
|
633
1008
|
var MAX_RECEIPTS = 100;
|
|
634
1009
|
var ReceiptBuffer = class {
|
|
@@ -721,13 +1096,13 @@ function handleHealth(res, startTime, config) {
|
|
|
721
1096
|
}));
|
|
722
1097
|
}
|
|
723
1098
|
function handleStatus(res, logDir) {
|
|
724
|
-
const logPath = (0,
|
|
725
|
-
if (!(0,
|
|
1099
|
+
const logPath = (0, import_node_path3.join)(logDir, LOG_FILE);
|
|
1100
|
+
if (!(0, import_node_fs5.existsSync)(logPath)) {
|
|
726
1101
|
res.writeHead(200);
|
|
727
1102
|
res.end(JSON.stringify({ entries: 0, message: "no log file yet" }));
|
|
728
1103
|
return;
|
|
729
1104
|
}
|
|
730
|
-
const raw = (0,
|
|
1105
|
+
const raw = (0, import_node_fs5.readFileSync)(logPath, "utf-8");
|
|
731
1106
|
const lines = raw.trim().split("\n").filter(Boolean);
|
|
732
1107
|
const entries = [];
|
|
733
1108
|
for (const line of lines) {
|
|
@@ -865,18 +1240,30 @@ var ProtectGateway = class {
|
|
|
865
1240
|
/** Approval grants keyed by request_id (scoped to the specific action that was requested) */
|
|
866
1241
|
approvalStore = /* @__PURE__ */ new Map();
|
|
867
1242
|
/** Random nonce generated at startup — required for approval endpoint authentication */
|
|
868
|
-
approvalNonce = (0,
|
|
1243
|
+
approvalNonce = (0, import_node_crypto3.randomBytes)(16).toString("hex");
|
|
869
1244
|
currentTier = "unknown";
|
|
870
1245
|
admissionResult = null;
|
|
1246
|
+
/** Notification config for approval gates (SMS, webhook, email) */
|
|
1247
|
+
notificationConfig = null;
|
|
871
1248
|
/** HTTP transport mode: pending response resolvers keyed by JSON-RPC id */
|
|
872
1249
|
pendingResponses = /* @__PURE__ */ new Map();
|
|
873
1250
|
httpMode = false;
|
|
1251
|
+
/** Loaded Cedar policy set (when policy_engine is "cedar") */
|
|
1252
|
+
cedarPolicySet = null;
|
|
874
1253
|
constructor(config) {
|
|
875
1254
|
this.config = config;
|
|
876
|
-
this.logFilePath = (0,
|
|
877
|
-
this.receiptFilePath = (0,
|
|
1255
|
+
this.logFilePath = (0, import_node_path4.join)(process.cwd(), LOG_FILE2);
|
|
1256
|
+
this.receiptFilePath = (0, import_node_path4.join)(process.cwd(), RECEIPTS_FILE);
|
|
878
1257
|
this.evidenceStore = new EvidenceStore();
|
|
879
1258
|
this.receiptBuffer = new ReceiptBuffer();
|
|
1259
|
+
this.notificationConfig = parseNotificationConfigFromEnv();
|
|
1260
|
+
}
|
|
1261
|
+
/**
|
|
1262
|
+
* Set the Cedar policy set for local evaluation.
|
|
1263
|
+
* Called during CLI startup when --cedar flag is used.
|
|
1264
|
+
*/
|
|
1265
|
+
setCedarPolicies(policySet) {
|
|
1266
|
+
this.cedarPolicySet = policySet;
|
|
880
1267
|
}
|
|
881
1268
|
async start() {
|
|
882
1269
|
const { command, args, verbose } = this.config;
|
|
@@ -1007,7 +1394,7 @@ var ProtectGateway = class {
|
|
|
1007
1394
|
}
|
|
1008
1395
|
async interceptToolCall(request) {
|
|
1009
1396
|
const toolName = request.params?.name || "unknown";
|
|
1010
|
-
const requestId = (0,
|
|
1397
|
+
const requestId = (0, import_node_crypto3.randomUUID)().slice(0, 12);
|
|
1011
1398
|
const mode = this.config.enforce ? "enforce" : "shadow";
|
|
1012
1399
|
let resolvedAgentKid = this.admissionResult?.agent_id;
|
|
1013
1400
|
let effectiveToolPolicy;
|
|
@@ -1045,6 +1432,27 @@ var ProtectGateway = class {
|
|
|
1045
1432
|
}
|
|
1046
1433
|
}
|
|
1047
1434
|
}
|
|
1435
|
+
if (this.config.policy?.policy_engine === "cedar" && this.cedarPolicySet) {
|
|
1436
|
+
try {
|
|
1437
|
+
const cedarDecision = await evaluateCedar(this.cedarPolicySet, {
|
|
1438
|
+
tool: toolName,
|
|
1439
|
+
tier: this.currentTier,
|
|
1440
|
+
agentId: this.admissionResult?.agent_id
|
|
1441
|
+
});
|
|
1442
|
+
if (!cedarDecision.allowed) {
|
|
1443
|
+
const reason = cedarDecision.reason || "cedar_deny";
|
|
1444
|
+
this.emitDecisionLog({ tool: toolName, decision: "deny", reason_code: reason, request_id: requestId, tier: this.currentTier, credential_ref: credentialRef });
|
|
1445
|
+
if (this.config.enforce) {
|
|
1446
|
+
return this.makeErrorResponse(request.id, -32600, `Tool "${toolName}" denied by Cedar policy`);
|
|
1447
|
+
}
|
|
1448
|
+
return null;
|
|
1449
|
+
}
|
|
1450
|
+
this.emitDecisionLog({ tool: toolName, decision: "allow", reason_code: "cedar_allow", request_id: requestId, tier: this.currentTier, credential_ref: credentialRef });
|
|
1451
|
+
return null;
|
|
1452
|
+
} catch (err) {
|
|
1453
|
+
if (this.config.verbose) this.log(`Cedar evaluation error: ${err instanceof Error ? err.message : err}`);
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1048
1456
|
if (this.config.policy?.external && (this.config.policy.policy_engine === "external" || this.config.policy.policy_engine === "hybrid")) {
|
|
1049
1457
|
try {
|
|
1050
1458
|
const ctx = buildDecisionContext(toolName, this.currentTier, {
|
|
@@ -1092,6 +1500,20 @@ var ProtectGateway = class {
|
|
|
1092
1500
|
return null;
|
|
1093
1501
|
}
|
|
1094
1502
|
this.emitDecisionLog({ tool: toolName, decision: "require_approval", reason_code: "requires_human_approval", request_id: requestId, tier: this.currentTier, credential_ref: credentialRef });
|
|
1503
|
+
if (this.notificationConfig) {
|
|
1504
|
+
sendApprovalNotification(this.notificationConfig, {
|
|
1505
|
+
requestId,
|
|
1506
|
+
toolName,
|
|
1507
|
+
agentId: this.admissionResult?.agent_id,
|
|
1508
|
+
policyName: "default",
|
|
1509
|
+
reason: `Policy requires human approval for "${toolName}"`,
|
|
1510
|
+
traceUrl: `https://scopeblind.com/trace`,
|
|
1511
|
+
approveUrl: void 0,
|
|
1512
|
+
// Approve URL provided when HTTP transport is active
|
|
1513
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1514
|
+
}).catch(() => {
|
|
1515
|
+
});
|
|
1516
|
+
}
|
|
1095
1517
|
if (this.config.enforce) {
|
|
1096
1518
|
return {
|
|
1097
1519
|
jsonrpc: "2.0",
|
|
@@ -1139,8 +1561,19 @@ var ProtectGateway = class {
|
|
|
1139
1561
|
}
|
|
1140
1562
|
return policy.rate_limit;
|
|
1141
1563
|
}
|
|
1564
|
+
/**
|
|
1565
|
+
* Emit a decision log entry with OTel-compatible trace IDs and optional
|
|
1566
|
+
* signed receipt generation.
|
|
1567
|
+
*
|
|
1568
|
+
* @patent Patent-protected construction — decision receipts with configurable
|
|
1569
|
+
* disclosure and issuer-blind properties. Covered by Apache 2.0 patent grant
|
|
1570
|
+
* for users of this code. Clean-room reimplementation requires a patent license.
|
|
1571
|
+
* @see {@link https://datatracker.ietf.org/doc/draft-farley-acta-signed-receipts/}
|
|
1572
|
+
*/
|
|
1142
1573
|
emitDecisionLog(entry) {
|
|
1143
1574
|
const mode = this.config.enforce ? "enforce" : "shadow";
|
|
1575
|
+
const otelTraceId = entry.otel_trace_id || (0, import_node_crypto3.randomBytes)(16).toString("hex");
|
|
1576
|
+
const otelSpanId = entry.otel_span_id || (0, import_node_crypto3.randomBytes)(8).toString("hex");
|
|
1144
1577
|
const log = {
|
|
1145
1578
|
v: 2,
|
|
1146
1579
|
tool: entry.tool || "unknown",
|
|
@@ -1148,17 +1581,19 @@ var ProtectGateway = class {
|
|
|
1148
1581
|
reason_code: entry.reason_code || "default_allow",
|
|
1149
1582
|
policy_digest: this.config.policyDigest,
|
|
1150
1583
|
policy_engine: this.config.policy?.policy_engine || "built-in",
|
|
1151
|
-
request_id: entry.request_id || (0,
|
|
1584
|
+
request_id: entry.request_id || (0, import_node_crypto3.randomUUID)().slice(0, 12),
|
|
1152
1585
|
timestamp: Date.now(),
|
|
1153
1586
|
mode,
|
|
1154
1587
|
...entry.rate_limit_remaining !== void 0 && { rate_limit_remaining: entry.rate_limit_remaining },
|
|
1155
1588
|
...entry.tier && { tier: entry.tier },
|
|
1156
|
-
...entry.credential_ref && { credential_ref: entry.credential_ref }
|
|
1589
|
+
...entry.credential_ref && { credential_ref: entry.credential_ref },
|
|
1590
|
+
otel_trace_id: otelTraceId,
|
|
1591
|
+
otel_span_id: otelSpanId
|
|
1157
1592
|
};
|
|
1158
1593
|
process.stderr.write(`[PROTECT_MCP] ${JSON.stringify(log)}
|
|
1159
1594
|
`);
|
|
1160
1595
|
try {
|
|
1161
|
-
(0,
|
|
1596
|
+
(0, import_node_fs6.appendFileSync)(this.logFilePath, JSON.stringify(log) + "\n");
|
|
1162
1597
|
} catch {
|
|
1163
1598
|
}
|
|
1164
1599
|
if (isSigningEnabled()) {
|
|
@@ -1167,7 +1602,7 @@ var ProtectGateway = class {
|
|
|
1167
1602
|
process.stderr.write(`[PROTECT_MCP_RECEIPT] ${signed.signed}
|
|
1168
1603
|
`);
|
|
1169
1604
|
try {
|
|
1170
|
-
(0,
|
|
1605
|
+
(0, import_node_fs6.appendFileSync)(this.receiptFilePath, signed.signed + "\n");
|
|
1171
1606
|
} catch {
|
|
1172
1607
|
}
|
|
1173
1608
|
this.receiptBuffer.add(log.request_id, signed.signed);
|
|
@@ -1357,9 +1792,9 @@ function collectSignedReceipts(logs) {
|
|
|
1357
1792
|
}
|
|
1358
1793
|
|
|
1359
1794
|
// src/simulate.ts
|
|
1360
|
-
var
|
|
1795
|
+
var import_node_fs7 = require("fs");
|
|
1361
1796
|
function parseLogFile(path) {
|
|
1362
|
-
const raw = (0,
|
|
1797
|
+
const raw = (0, import_node_fs7.readFileSync)(path, "utf-8");
|
|
1363
1798
|
const entries = [];
|
|
1364
1799
|
for (const line of raw.split("\n")) {
|
|
1365
1800
|
const trimmed = line.trim();
|
|
@@ -1488,13 +1923,13 @@ function formatSimulation(summary) {
|
|
|
1488
1923
|
}
|
|
1489
1924
|
|
|
1490
1925
|
// src/report.ts
|
|
1491
|
-
var
|
|
1926
|
+
var import_node_fs8 = require("fs");
|
|
1492
1927
|
function generateReport(logPath, receiptPath, periodDays) {
|
|
1493
1928
|
const now = /* @__PURE__ */ new Date();
|
|
1494
1929
|
const from = new Date(now.getTime() - periodDays * 864e5);
|
|
1495
1930
|
const entries = [];
|
|
1496
|
-
if ((0,
|
|
1497
|
-
const raw = (0,
|
|
1931
|
+
if ((0, import_node_fs8.existsSync)(logPath)) {
|
|
1932
|
+
const raw = (0, import_node_fs8.readFileSync)(logPath, "utf-8");
|
|
1498
1933
|
for (const line of raw.split("\n")) {
|
|
1499
1934
|
const trimmed = line.trim();
|
|
1500
1935
|
if (!trimmed) continue;
|
|
@@ -1514,8 +1949,8 @@ function generateReport(logPath, receiptPath, periodDays) {
|
|
|
1514
1949
|
let receiptsSigned = 0;
|
|
1515
1950
|
let signerKid = "";
|
|
1516
1951
|
let signerIssuer = "";
|
|
1517
|
-
if ((0,
|
|
1518
|
-
const raw = (0,
|
|
1952
|
+
if ((0, import_node_fs8.existsSync)(receiptPath)) {
|
|
1953
|
+
const raw = (0, import_node_fs8.readFileSync)(receiptPath, "utf-8");
|
|
1519
1954
|
for (const line of raw.split("\n")) {
|
|
1520
1955
|
const trimmed = line.trim();
|
|
1521
1956
|
if (!trimmed) continue;
|
|
@@ -1802,19 +2237,1270 @@ function validateEvidenceReceipt(receipt) {
|
|
|
1802
2237
|
}
|
|
1803
2238
|
return errors;
|
|
1804
2239
|
}
|
|
2240
|
+
|
|
2241
|
+
// src/rekor-anchor.ts
|
|
2242
|
+
var import_node_crypto4 = require("crypto");
|
|
2243
|
+
var REKOR_API = "https://rekor.sigstore.dev/api/v1";
|
|
2244
|
+
async function anchorToRekor(receiptHash, signature, publicKeyPem) {
|
|
2245
|
+
const entry = {
|
|
2246
|
+
apiVersion: "0.0.1",
|
|
2247
|
+
kind: "hashedrekord",
|
|
2248
|
+
spec: {
|
|
2249
|
+
data: {
|
|
2250
|
+
hash: {
|
|
2251
|
+
algorithm: "sha256",
|
|
2252
|
+
value: receiptHash
|
|
2253
|
+
}
|
|
2254
|
+
},
|
|
2255
|
+
signature: {
|
|
2256
|
+
content: signature,
|
|
2257
|
+
publicKey: {
|
|
2258
|
+
content: Buffer.from(publicKeyPem).toString("base64")
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
};
|
|
2263
|
+
const response = await fetch(`${REKOR_API}/log/entries`, {
|
|
2264
|
+
method: "POST",
|
|
2265
|
+
headers: { "Content-Type": "application/json" },
|
|
2266
|
+
body: JSON.stringify(entry)
|
|
2267
|
+
});
|
|
2268
|
+
if (!response.ok) {
|
|
2269
|
+
const errorText = await response.text();
|
|
2270
|
+
throw new Error(`Rekor anchoring failed: ${response.status} ${errorText}`);
|
|
2271
|
+
}
|
|
2272
|
+
const result = await response.json();
|
|
2273
|
+
const [uuid, data] = Object.entries(result)[0];
|
|
2274
|
+
return {
|
|
2275
|
+
logIndex: data.logIndex,
|
|
2276
|
+
uuid,
|
|
2277
|
+
integratedTime: new Date(data.integratedTime * 1e3).toISOString(),
|
|
2278
|
+
receiptHash,
|
|
2279
|
+
logID: data.logID,
|
|
2280
|
+
body: data.body
|
|
2281
|
+
};
|
|
2282
|
+
}
|
|
2283
|
+
async function verifyRekorAnchor(logIndex, expectedHash) {
|
|
2284
|
+
const response = await fetch(`${REKOR_API}/log/entries?logIndex=${logIndex}`);
|
|
2285
|
+
if (!response.ok) {
|
|
2286
|
+
return {
|
|
2287
|
+
valid: false,
|
|
2288
|
+
logIndex,
|
|
2289
|
+
integratedTime: "",
|
|
2290
|
+
receiptHashMatch: false
|
|
2291
|
+
};
|
|
2292
|
+
}
|
|
2293
|
+
const result = await response.json();
|
|
2294
|
+
const [, data] = Object.entries(result)[0];
|
|
2295
|
+
let receiptHashMatch = false;
|
|
2296
|
+
try {
|
|
2297
|
+
const bodyJson = JSON.parse(Buffer.from(data.body, "base64").toString());
|
|
2298
|
+
const hash = bodyJson?.spec?.data?.hash?.value;
|
|
2299
|
+
receiptHashMatch = hash === expectedHash;
|
|
2300
|
+
} catch {
|
|
2301
|
+
}
|
|
2302
|
+
return {
|
|
2303
|
+
valid: receiptHashMatch,
|
|
2304
|
+
logIndex,
|
|
2305
|
+
integratedTime: new Date(data.integratedTime * 1e3).toISOString(),
|
|
2306
|
+
receiptHashMatch
|
|
2307
|
+
};
|
|
2308
|
+
}
|
|
2309
|
+
function hashReceipt(receipt) {
|
|
2310
|
+
const canonical = JSON.stringify(receipt, Object.keys(receipt).sort());
|
|
2311
|
+
return (0, import_node_crypto4.createHash)("sha256").update(canonical).digest("hex");
|
|
2312
|
+
}
|
|
2313
|
+
function createLogAnchorField(anchor) {
|
|
2314
|
+
return {
|
|
2315
|
+
transparency_log: "rekor.sigstore.dev",
|
|
2316
|
+
log_index: anchor.logIndex,
|
|
2317
|
+
integrated_time: anchor.integratedTime,
|
|
2318
|
+
receipt_hash: anchor.receiptHash,
|
|
2319
|
+
verify_url: `https://search.sigstore.dev/?logIndex=${anchor.logIndex}`
|
|
2320
|
+
};
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2323
|
+
// src/selective-disclosure.ts
|
|
2324
|
+
var import_node_crypto5 = require("crypto");
|
|
2325
|
+
function redactFields(receipt, fieldsToRedact) {
|
|
2326
|
+
const redacted = JSON.parse(JSON.stringify(receipt));
|
|
2327
|
+
const salts = [];
|
|
2328
|
+
const redactedFields = [];
|
|
2329
|
+
const originalHash = hashObject(receipt);
|
|
2330
|
+
for (const fieldPath of fieldsToRedact) {
|
|
2331
|
+
const parts = fieldPath.split(".");
|
|
2332
|
+
let current = redacted;
|
|
2333
|
+
let parent = null;
|
|
2334
|
+
let lastKey = "";
|
|
2335
|
+
for (let i = 0; i < parts.length; i++) {
|
|
2336
|
+
const key = parts[i];
|
|
2337
|
+
if (i === parts.length - 1) {
|
|
2338
|
+
if (key in current) {
|
|
2339
|
+
const originalValue = current[key];
|
|
2340
|
+
const salt = (0, import_node_crypto5.randomBytes)(16).toString("hex");
|
|
2341
|
+
const commitment = computeCommitment(salt, originalValue);
|
|
2342
|
+
salts.push({ field: fieldPath, salt, originalValue });
|
|
2343
|
+
current[key] = `sha256(salt + ${typeof originalValue === "string" ? "..." : JSON.stringify(originalValue).slice(0, 20) + "..."})`;
|
|
2344
|
+
redactedFields.push(fieldPath);
|
|
2345
|
+
if (!redacted._commitments) {
|
|
2346
|
+
redacted._commitments = {};
|
|
2347
|
+
}
|
|
2348
|
+
redacted._commitments[fieldPath] = commitment;
|
|
2349
|
+
}
|
|
2350
|
+
} else {
|
|
2351
|
+
if (typeof current[key] === "object" && current[key] !== null) {
|
|
2352
|
+
parent = current;
|
|
2353
|
+
lastKey = key;
|
|
2354
|
+
current = current[key];
|
|
2355
|
+
} else {
|
|
2356
|
+
break;
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
return { redacted, salts, redactedFields, originalHash };
|
|
2362
|
+
}
|
|
2363
|
+
function revealField(redactedReceipt, salts, fieldPath) {
|
|
2364
|
+
const salt = salts.find((s) => s.field === fieldPath);
|
|
2365
|
+
if (!salt) {
|
|
2366
|
+
throw new Error(`No salt found for field: ${fieldPath}`);
|
|
2367
|
+
}
|
|
2368
|
+
const revealed = JSON.parse(JSON.stringify(redactedReceipt));
|
|
2369
|
+
const parts = fieldPath.split(".");
|
|
2370
|
+
let current = revealed;
|
|
2371
|
+
for (let i = 0; i < parts.length; i++) {
|
|
2372
|
+
const key = parts[i];
|
|
2373
|
+
if (i === parts.length - 1) {
|
|
2374
|
+
current[key] = salt.originalValue;
|
|
2375
|
+
} else {
|
|
2376
|
+
current = current[key];
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
return revealed;
|
|
2380
|
+
}
|
|
2381
|
+
function verifyCommitment(commitment, salt, value) {
|
|
2382
|
+
const expected = computeCommitment(salt, value);
|
|
2383
|
+
return commitment === expected;
|
|
2384
|
+
}
|
|
2385
|
+
function verifyAllCommitments(redactedReceipt, salts) {
|
|
2386
|
+
const commitments = redactedReceipt._commitments;
|
|
2387
|
+
if (!commitments) {
|
|
2388
|
+
return { valid: true, fields: {} };
|
|
2389
|
+
}
|
|
2390
|
+
const fields = {};
|
|
2391
|
+
let allValid = true;
|
|
2392
|
+
for (const salt of salts) {
|
|
2393
|
+
const commitment = commitments[salt.field];
|
|
2394
|
+
if (commitment) {
|
|
2395
|
+
const valid = verifyCommitment(commitment, salt.salt, salt.originalValue);
|
|
2396
|
+
fields[salt.field] = valid;
|
|
2397
|
+
if (!valid) allValid = false;
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
return { valid: allValid, fields };
|
|
2401
|
+
}
|
|
2402
|
+
function createDisclosurePackage(allSalts, fieldsToDisclose) {
|
|
2403
|
+
const disclosed = allSalts.filter((s) => fieldsToDisclose.includes(s.field)).map((s) => ({ field: s.field, salt: s.salt, value: s.originalValue }));
|
|
2404
|
+
return {
|
|
2405
|
+
version: "0.1",
|
|
2406
|
+
disclosed_fields: fieldsToDisclose,
|
|
2407
|
+
salts: disclosed
|
|
2408
|
+
};
|
|
2409
|
+
}
|
|
2410
|
+
function computeCommitment(salt, value) {
|
|
2411
|
+
const serialized = typeof value === "string" ? value : JSON.stringify(value);
|
|
2412
|
+
return (0, import_node_crypto5.createHash)("sha256").update(salt + serialized).digest("hex");
|
|
2413
|
+
}
|
|
2414
|
+
function hashObject(obj) {
|
|
2415
|
+
const canonical = JSON.stringify(obj, Object.keys(obj).sort());
|
|
2416
|
+
return (0, import_node_crypto5.createHash)("sha256").update(canonical).digest("hex");
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
// src/huggingface-export.ts
|
|
2420
|
+
function receiptsToHFRows(receipts) {
|
|
2421
|
+
return receipts.map((r) => {
|
|
2422
|
+
const raw = r;
|
|
2423
|
+
const payload = raw.payload || {};
|
|
2424
|
+
const edges = Array.isArray(raw.parent_receipts) ? raw.parent_receipts : [];
|
|
2425
|
+
return {
|
|
2426
|
+
receipt_id: String(raw.receipt_id || raw.id || ""),
|
|
2427
|
+
receipt_type: String(raw.receipt_type || raw.type || "unknown"),
|
|
2428
|
+
tool_name: payload.tool_name ? String(payload.tool_name) : null,
|
|
2429
|
+
decision: payload.decision ? String(payload.decision) : null,
|
|
2430
|
+
agent_id: payload.agent_id ? String(payload.agent_id) : raw.subject_id ? String(raw.subject_id) : null,
|
|
2431
|
+
issuer_id: String(raw.issuer_id || "unknown"),
|
|
2432
|
+
timestamp: String(raw.timestamp || raw.event_time || (/* @__PURE__ */ new Date()).toISOString()),
|
|
2433
|
+
policy_hash: payload.active_policy_hash ? String(payload.active_policy_hash) : null,
|
|
2434
|
+
edges,
|
|
2435
|
+
edge_count: edges.length,
|
|
2436
|
+
signature: raw.signature ? String(raw.signature) : null,
|
|
2437
|
+
signed: Boolean(raw.signature),
|
|
2438
|
+
context_hash: raw.context_hash ? String(raw.context_hash) : null,
|
|
2439
|
+
chain_id: raw.chain_id ? String(raw.chain_id) : null
|
|
2440
|
+
};
|
|
2441
|
+
});
|
|
2442
|
+
}
|
|
2443
|
+
function generateHFMetadata(rows, name) {
|
|
2444
|
+
const types = {};
|
|
2445
|
+
const decisions = {};
|
|
2446
|
+
const agents = /* @__PURE__ */ new Set();
|
|
2447
|
+
const tools = /* @__PURE__ */ new Set();
|
|
2448
|
+
let minTime = Infinity;
|
|
2449
|
+
let maxTime = -Infinity;
|
|
2450
|
+
for (const row of rows) {
|
|
2451
|
+
types[row.receipt_type] = (types[row.receipt_type] || 0) + 1;
|
|
2452
|
+
if (row.decision) decisions[row.decision] = (decisions[row.decision] || 0) + 1;
|
|
2453
|
+
if (row.agent_id) agents.add(row.agent_id);
|
|
2454
|
+
if (row.tool_name) tools.add(row.tool_name);
|
|
2455
|
+
const t = new Date(row.timestamp).getTime();
|
|
2456
|
+
if (t < minTime) minTime = t;
|
|
2457
|
+
if (t > maxTime) maxTime = t;
|
|
2458
|
+
}
|
|
2459
|
+
return {
|
|
2460
|
+
name: name || "scopeblind-acta-receipts",
|
|
2461
|
+
description: "Cryptographically signed decision receipts from AI agent tool calls. Each row is an Ed25519-signed receipt capturing a machine decision, its causal context, and policy evaluation result. Produced by protect-mcp and verified with @veritasacta/verify.",
|
|
2462
|
+
num_rows: rows.length,
|
|
2463
|
+
type_distribution: types,
|
|
2464
|
+
decision_distribution: decisions,
|
|
2465
|
+
time_range: {
|
|
2466
|
+
from: isFinite(minTime) ? new Date(minTime).toISOString() : "",
|
|
2467
|
+
to: isFinite(maxTime) ? new Date(maxTime).toISOString() : ""
|
|
2468
|
+
},
|
|
2469
|
+
unique_agents: agents.size,
|
|
2470
|
+
unique_tools: tools.size,
|
|
2471
|
+
exported_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2472
|
+
license: "MIT",
|
|
2473
|
+
tags: [
|
|
2474
|
+
"ai-safety",
|
|
2475
|
+
"agent-governance",
|
|
2476
|
+
"cryptographic-receipts",
|
|
2477
|
+
"veritas-acta",
|
|
2478
|
+
"scopeblind",
|
|
2479
|
+
"mcp",
|
|
2480
|
+
"ed25519",
|
|
2481
|
+
"causal-dag",
|
|
2482
|
+
"decision-evidence"
|
|
2483
|
+
]
|
|
2484
|
+
};
|
|
2485
|
+
}
|
|
2486
|
+
function exportJSONL(rows) {
|
|
2487
|
+
return rows.map((row) => JSON.stringify(row)).join("\n") + "\n";
|
|
2488
|
+
}
|
|
2489
|
+
function generateDatasetCard(metadata) {
|
|
2490
|
+
return `---
|
|
2491
|
+
license: mit
|
|
2492
|
+
task_categories:
|
|
2493
|
+
- text-classification
|
|
2494
|
+
tags:
|
|
2495
|
+
${metadata.tags.map((t) => ` - ${t}`).join("\n")}
|
|
2496
|
+
size_categories:
|
|
2497
|
+
- ${metadata.num_rows < 1e3 ? "n<1K" : metadata.num_rows < 1e4 ? "1K<n<10K" : "10K<n<100K"}
|
|
2498
|
+
---
|
|
2499
|
+
|
|
2500
|
+
# ${metadata.name}
|
|
2501
|
+
|
|
2502
|
+
${metadata.description}
|
|
2503
|
+
|
|
2504
|
+
## Dataset Structure
|
|
2505
|
+
|
|
2506
|
+
Each row is a cryptographically signed receipt representing a single machine decision.
|
|
2507
|
+
|
|
2508
|
+
| Field | Type | Description |
|
|
2509
|
+
|-------|------|-------------|
|
|
2510
|
+
| receipt_id | string | Unique receipt identifier (content-addressed hash) |
|
|
2511
|
+
| receipt_type | string | decision, execution, outcome, policy_load, observation, approval |
|
|
2512
|
+
| tool_name | string | MCP tool that was called |
|
|
2513
|
+
| decision | string | allow, deny, or null |
|
|
2514
|
+
| agent_id | string | Pseudonymous agent identifier |
|
|
2515
|
+
| timestamp | string | ISO 8601 timestamp |
|
|
2516
|
+
| policy_hash | string | SHA-256 hash of the active policy |
|
|
2517
|
+
| edges | array | Typed causal edges to parent receipts |
|
|
2518
|
+
| signature | string | Ed25519 signature (hex) |
|
|
2519
|
+
| signed | boolean | Whether the receipt has a valid signature |
|
|
2520
|
+
|
|
2521
|
+
## Statistics
|
|
2522
|
+
|
|
2523
|
+
- **Total receipts:** ${metadata.num_rows.toLocaleString()}
|
|
2524
|
+
- **Unique agents:** ${metadata.unique_agents}
|
|
2525
|
+
- **Unique tools:** ${metadata.unique_tools}
|
|
2526
|
+
- **Time range:** ${metadata.time_range.from} \u2192 ${metadata.time_range.to}
|
|
2527
|
+
|
|
2528
|
+
### Type distribution
|
|
2529
|
+
${Object.entries(metadata.type_distribution).map(([k, v]) => `- ${k}: ${v}`).join("\n")}
|
|
2530
|
+
|
|
2531
|
+
### Decision distribution
|
|
2532
|
+
${Object.entries(metadata.decision_distribution).map(([k, v]) => `- ${k}: ${v}`).join("\n")}
|
|
2533
|
+
|
|
2534
|
+
## Verification
|
|
2535
|
+
|
|
2536
|
+
Every receipt in this dataset can be independently verified:
|
|
2537
|
+
|
|
2538
|
+
\`\`\`bash
|
|
2539
|
+
npx @veritasacta/verify receipt.json
|
|
2540
|
+
\`\`\`
|
|
2541
|
+
|
|
2542
|
+
The verification is offline, MIT-licensed, and does not contact any server.
|
|
2543
|
+
|
|
2544
|
+
## Source
|
|
2545
|
+
|
|
2546
|
+
- Protocol: [Veritas Acta](https://veritasacta.com)
|
|
2547
|
+
- Gateway: [protect-mcp](https://npmjs.com/package/protect-mcp)
|
|
2548
|
+
- IETF Draft: [draft-farley-acta-signed-receipts](https://datatracker.ietf.org/doc/draft-farley-acta-signed-receipts/)
|
|
2549
|
+
|
|
2550
|
+
## License
|
|
2551
|
+
|
|
2552
|
+
MIT
|
|
2553
|
+
`;
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2556
|
+
// src/webauthn-approval.ts
|
|
2557
|
+
var import_node_crypto6 = require("crypto");
|
|
2558
|
+
function createApprovalChallenge(requestId, toolName, agentId, rpId = "scopeblind.com", timeoutSeconds = 300) {
|
|
2559
|
+
const challengeBytes = (0, import_node_crypto6.randomBytes)(32);
|
|
2560
|
+
const contextHash = (0, import_node_crypto6.createHash)("sha256").update(JSON.stringify({ requestId, toolName, agentId, timestamp: Date.now() })).digest("hex");
|
|
2561
|
+
return {
|
|
2562
|
+
challenge: base64urlEncode(challengeBytes),
|
|
2563
|
+
requestId,
|
|
2564
|
+
toolName,
|
|
2565
|
+
agentId,
|
|
2566
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2567
|
+
timeoutSeconds,
|
|
2568
|
+
rpId,
|
|
2569
|
+
contextHash
|
|
2570
|
+
};
|
|
2571
|
+
}
|
|
2572
|
+
function toCredentialRequestOptions(challenge, allowCredentials) {
|
|
2573
|
+
return {
|
|
2574
|
+
publicKey: {
|
|
2575
|
+
challenge: base64urlDecode(challenge.challenge).buffer,
|
|
2576
|
+
rpId: challenge.rpId,
|
|
2577
|
+
timeout: challenge.timeoutSeconds * 1e3,
|
|
2578
|
+
userVerification: "required",
|
|
2579
|
+
// Always require biometric
|
|
2580
|
+
...allowCredentials ? {
|
|
2581
|
+
allowCredentials: allowCredentials.map((c) => ({
|
|
2582
|
+
id: base64urlDecode(c.id).buffer,
|
|
2583
|
+
type: "public-key"
|
|
2584
|
+
}))
|
|
2585
|
+
} : {}
|
|
2586
|
+
}
|
|
2587
|
+
};
|
|
2588
|
+
}
|
|
2589
|
+
function verifyApprovalAssertion(challenge, assertion) {
|
|
2590
|
+
const createdAt = new Date(challenge.createdAt).getTime();
|
|
2591
|
+
const now = Date.now();
|
|
2592
|
+
if (now - createdAt > challenge.timeoutSeconds * 1e3) {
|
|
2593
|
+
return {
|
|
2594
|
+
valid: false,
|
|
2595
|
+
credentialId: assertion.credentialId,
|
|
2596
|
+
authenticatorType: "unknown",
|
|
2597
|
+
userVerified: false,
|
|
2598
|
+
signCount: 0,
|
|
2599
|
+
contextHash: challenge.contextHash,
|
|
2600
|
+
approvedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2601
|
+
};
|
|
2602
|
+
}
|
|
2603
|
+
const authData = base64urlDecode(assertion.authenticatorData);
|
|
2604
|
+
const flags = authData[32];
|
|
2605
|
+
const userPresent = !!(flags & 1);
|
|
2606
|
+
const userVerified = !!(flags & 4);
|
|
2607
|
+
const attestedCredData = !!(flags & 64);
|
|
2608
|
+
const signCount = authData.length >= 37 ? authData[33] << 24 | authData[34] << 16 | authData[35] << 8 | authData[36] : 0;
|
|
2609
|
+
let authenticatorType = "unknown";
|
|
2610
|
+
try {
|
|
2611
|
+
const clientData = JSON.parse(Buffer.from(base64urlDecode(assertion.clientDataJSON)).toString());
|
|
2612
|
+
if (clientData.type === "webauthn.get") {
|
|
2613
|
+
authenticatorType = "platform";
|
|
2614
|
+
}
|
|
2615
|
+
} catch {
|
|
2616
|
+
}
|
|
2617
|
+
return {
|
|
2618
|
+
valid: userPresent,
|
|
2619
|
+
// At minimum, user must be present
|
|
2620
|
+
credentialId: assertion.credentialId,
|
|
2621
|
+
authenticatorType,
|
|
2622
|
+
userVerified,
|
|
2623
|
+
signCount,
|
|
2624
|
+
contextHash: challenge.contextHash,
|
|
2625
|
+
approvedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2626
|
+
};
|
|
2627
|
+
}
|
|
2628
|
+
function createApprovalReceiptPayload(challenge, result) {
|
|
2629
|
+
return {
|
|
2630
|
+
type: "acta:approval",
|
|
2631
|
+
approval_method: "webauthn",
|
|
2632
|
+
tool_name: challenge.toolName,
|
|
2633
|
+
request_id: challenge.requestId,
|
|
2634
|
+
agent_id: challenge.agentId,
|
|
2635
|
+
authenticator_type: result.authenticatorType,
|
|
2636
|
+
user_verified: result.userVerified,
|
|
2637
|
+
context_hash: result.contextHash,
|
|
2638
|
+
approved_at: result.approvedAt,
|
|
2639
|
+
// Hash the credential ID for privacy — don't store the raw ID
|
|
2640
|
+
credential_id_hash: (0, import_node_crypto6.createHash)("sha256").update(result.credentialId).digest("hex").slice(0, 16)
|
|
2641
|
+
};
|
|
2642
|
+
}
|
|
2643
|
+
function base64urlEncode(buffer) {
|
|
2644
|
+
return Buffer.from(buffer).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
2645
|
+
}
|
|
2646
|
+
function base64urlDecode(str) {
|
|
2647
|
+
const base64 = str.replace(/-/g, "+").replace(/_/g, "/");
|
|
2648
|
+
const padded = base64 + "=".repeat((4 - base64.length % 4) % 4);
|
|
2649
|
+
return new Uint8Array(Buffer.from(padded, "base64"));
|
|
2650
|
+
}
|
|
2651
|
+
|
|
2652
|
+
// src/did-vc.ts
|
|
2653
|
+
function ed25519ToDIDKey(publicKeyHex) {
|
|
2654
|
+
const multicodecPrefix = Buffer.from([237, 1]);
|
|
2655
|
+
const publicKeyBytes = Buffer.from(publicKeyHex, "hex");
|
|
2656
|
+
const multicodecKey = Buffer.concat([multicodecPrefix, publicKeyBytes]);
|
|
2657
|
+
const base58 = base58btcEncode(multicodecKey);
|
|
2658
|
+
return `did:key:z${base58}`;
|
|
2659
|
+
}
|
|
2660
|
+
function manifestToVC(manifest) {
|
|
2661
|
+
const did = ed25519ToDIDKey(manifest.public_key);
|
|
2662
|
+
return {
|
|
2663
|
+
"@context": [
|
|
2664
|
+
"https://www.w3.org/2018/credentials/v1",
|
|
2665
|
+
"https://veritasacta.com/contexts/agent-manifest/v1"
|
|
2666
|
+
],
|
|
2667
|
+
type: ["VerifiableCredential", "AgentManifestCredential"],
|
|
2668
|
+
issuer: did,
|
|
2669
|
+
issuanceDate: manifest.created_at || (/* @__PURE__ */ new Date()).toISOString(),
|
|
2670
|
+
credentialSubject: {
|
|
2671
|
+
id: did,
|
|
2672
|
+
agentId: manifest.agent_id,
|
|
2673
|
+
displayName: manifest.display_name,
|
|
2674
|
+
capabilities: manifest.capabilities || [],
|
|
2675
|
+
policyDigest: manifest.policy_digest,
|
|
2676
|
+
publicKey: manifest.public_key
|
|
2677
|
+
},
|
|
2678
|
+
...manifest.signature ? {
|
|
2679
|
+
proof: {
|
|
2680
|
+
type: "Ed25519Signature2020",
|
|
2681
|
+
created: manifest.created_at || (/* @__PURE__ */ new Date()).toISOString(),
|
|
2682
|
+
verificationMethod: `${did}#key-1`,
|
|
2683
|
+
proofPurpose: "assertionMethod",
|
|
2684
|
+
proofValue: manifest.signature
|
|
2685
|
+
}
|
|
2686
|
+
} : {}
|
|
2687
|
+
};
|
|
2688
|
+
}
|
|
2689
|
+
function receiptToVP(receipt, issuerPublicKey) {
|
|
2690
|
+
const did = ed25519ToDIDKey(issuerPublicKey);
|
|
2691
|
+
return {
|
|
2692
|
+
"@context": ["https://www.w3.org/2018/credentials/v1"],
|
|
2693
|
+
type: ["VerifiablePresentation"],
|
|
2694
|
+
holder: did,
|
|
2695
|
+
verifiableCredential: [{
|
|
2696
|
+
"@context": [
|
|
2697
|
+
"https://www.w3.org/2018/credentials/v1",
|
|
2698
|
+
"https://veritasacta.com/contexts/decision-receipt/v1"
|
|
2699
|
+
],
|
|
2700
|
+
type: ["VerifiableCredential", "DecisionReceiptCredential"],
|
|
2701
|
+
issuer: did,
|
|
2702
|
+
issuanceDate: receipt.event_time || (/* @__PURE__ */ new Date()).toISOString(),
|
|
2703
|
+
credentialSubject: {
|
|
2704
|
+
receiptId: receipt.receipt_id,
|
|
2705
|
+
receiptType: receipt.receipt_type,
|
|
2706
|
+
toolName: receipt.payload?.tool_name,
|
|
2707
|
+
decision: receipt.payload?.decision
|
|
2708
|
+
}
|
|
2709
|
+
}]
|
|
2710
|
+
};
|
|
2711
|
+
}
|
|
2712
|
+
function base58btcEncode(buffer) {
|
|
2713
|
+
const ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
2714
|
+
let num = BigInt("0x" + buffer.toString("hex"));
|
|
2715
|
+
let result = "";
|
|
2716
|
+
while (num > 0n) {
|
|
2717
|
+
result = ALPHABET[Number(num % 58n)] + result;
|
|
2718
|
+
num = num / 58n;
|
|
2719
|
+
}
|
|
2720
|
+
for (const byte of buffer) {
|
|
2721
|
+
if (byte === 0) result = "1" + result;
|
|
2722
|
+
else break;
|
|
2723
|
+
}
|
|
2724
|
+
return result;
|
|
2725
|
+
}
|
|
2726
|
+
|
|
2727
|
+
// src/sandbox.ts
|
|
2728
|
+
async function createSandbox(config) {
|
|
2729
|
+
const runtime = config.runtime || (config.apiKey || process.env.E2B_API_KEY ? "e2b" : "docker");
|
|
2730
|
+
if (runtime === "e2b") {
|
|
2731
|
+
return createE2BSandbox(config);
|
|
2732
|
+
}
|
|
2733
|
+
return createDockerSandbox(config);
|
|
2734
|
+
}
|
|
2735
|
+
async function runInSandbox(sandbox, toolCall, policy) {
|
|
2736
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
2737
|
+
const decision = evaluatePolicy(toolCall.tool, policy);
|
|
2738
|
+
const receipt = {
|
|
2739
|
+
tool: toolCall.tool,
|
|
2740
|
+
decision,
|
|
2741
|
+
executed: decision === "allow",
|
|
2742
|
+
timestamp
|
|
2743
|
+
};
|
|
2744
|
+
if (decision === "allow") {
|
|
2745
|
+
try {
|
|
2746
|
+
const result = await executeInSandbox(sandbox, toolCall);
|
|
2747
|
+
receipt.result = result;
|
|
2748
|
+
receipt.executed = true;
|
|
2749
|
+
} catch (err) {
|
|
2750
|
+
receipt.result = {
|
|
2751
|
+
success: false,
|
|
2752
|
+
output: "",
|
|
2753
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2754
|
+
durationMs: 0
|
|
2755
|
+
};
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
sandbox.receipts.push(receipt);
|
|
2759
|
+
return receipt;
|
|
2760
|
+
}
|
|
2761
|
+
function generateSafetyTranscript(sandbox, template) {
|
|
2762
|
+
const receipts = sandbox.receipts;
|
|
2763
|
+
const allowed = receipts.filter((r) => r.decision === "allow").length;
|
|
2764
|
+
const denied = receipts.filter((r) => r.decision === "deny").length;
|
|
2765
|
+
const requireApproval = receipts.filter((r) => r.decision === "require_approval").length;
|
|
2766
|
+
const executed = receipts.filter((r) => r.executed && r.result);
|
|
2767
|
+
const successful = executed.filter((r) => r.result?.success);
|
|
2768
|
+
const denyScore = denied > 0 ? 40 : allowed > 0 ? 20 : 40;
|
|
2769
|
+
const successRate = executed.length > 0 ? successful.length / executed.length : 1;
|
|
2770
|
+
const successScore = 30 * successRate;
|
|
2771
|
+
const approvalScore = requireApproval === 0 ? 30 : 15;
|
|
2772
|
+
const safetyScore = Math.round(denyScore + successScore + approvalScore);
|
|
2773
|
+
return {
|
|
2774
|
+
sandboxId: sandbox.id,
|
|
2775
|
+
template,
|
|
2776
|
+
totalCalls: receipts.length,
|
|
2777
|
+
allowed,
|
|
2778
|
+
denied,
|
|
2779
|
+
requireApproval,
|
|
2780
|
+
successRate,
|
|
2781
|
+
receipts,
|
|
2782
|
+
durationMs: 0,
|
|
2783
|
+
// Would be calculated from first/last receipt timestamps
|
|
2784
|
+
evaluatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2785
|
+
safetyScore: Math.min(100, Math.max(0, safetyScore))
|
|
2786
|
+
};
|
|
2787
|
+
}
|
|
2788
|
+
async function destroySandbox(sandbox) {
|
|
2789
|
+
sandbox.status = "destroyed";
|
|
2790
|
+
if (sandbox.runtime === "docker") {
|
|
2791
|
+
try {
|
|
2792
|
+
const { execSync } = await import("child_process");
|
|
2793
|
+
execSync(`docker rm -f ${sandbox.id} 2>/dev/null`, { stdio: "pipe" });
|
|
2794
|
+
} catch {
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2798
|
+
async function createE2BSandbox(config) {
|
|
2799
|
+
const apiKey = config.apiKey || process.env.E2B_API_KEY;
|
|
2800
|
+
if (!apiKey) {
|
|
2801
|
+
throw new Error("E2B_API_KEY not set. Get one at https://e2b.dev");
|
|
2802
|
+
}
|
|
2803
|
+
const response = await fetch("https://api.e2b.dev/sandboxes", {
|
|
2804
|
+
method: "POST",
|
|
2805
|
+
headers: {
|
|
2806
|
+
"Content-Type": "application/json",
|
|
2807
|
+
"X-API-Key": apiKey
|
|
2808
|
+
},
|
|
2809
|
+
body: JSON.stringify({
|
|
2810
|
+
templateID: config.template,
|
|
2811
|
+
timeout: config.timeoutSeconds || 300
|
|
2812
|
+
})
|
|
2813
|
+
});
|
|
2814
|
+
if (!response.ok) {
|
|
2815
|
+
throw new Error(`E2B sandbox creation failed: ${response.status}`);
|
|
2816
|
+
}
|
|
2817
|
+
const data = await response.json();
|
|
2818
|
+
return {
|
|
2819
|
+
id: data.sandboxID,
|
|
2820
|
+
runtime: "e2b",
|
|
2821
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2822
|
+
status: "running",
|
|
2823
|
+
receipts: []
|
|
2824
|
+
};
|
|
2825
|
+
}
|
|
2826
|
+
async function createDockerSandbox(config) {
|
|
2827
|
+
const { execSync } = await import("child_process");
|
|
2828
|
+
const { randomUUID: randomUUID3 } = await import("crypto");
|
|
2829
|
+
const id = `scopeblind-sandbox-${randomUUID3().slice(0, 8)}`;
|
|
2830
|
+
const image = config.template.includes(":") ? config.template : `node:${config.template.replace("node-", "")}`;
|
|
2831
|
+
const memoryFlag = config.memoryMB ? `--memory=${config.memoryMB}m` : "";
|
|
2832
|
+
const timeout = config.timeoutSeconds || 300;
|
|
2833
|
+
try {
|
|
2834
|
+
execSync(
|
|
2835
|
+
`docker run -d --name ${id} ${memoryFlag} --network=none --stop-timeout=${timeout} ${image} sleep ${timeout}`,
|
|
2836
|
+
{ stdio: "pipe" }
|
|
2837
|
+
);
|
|
2838
|
+
} catch (err) {
|
|
2839
|
+
throw new Error(`Docker sandbox creation failed: ${err instanceof Error ? err.message : err}`);
|
|
2840
|
+
}
|
|
2841
|
+
return {
|
|
2842
|
+
id,
|
|
2843
|
+
runtime: "docker",
|
|
2844
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2845
|
+
status: "running",
|
|
2846
|
+
receipts: []
|
|
2847
|
+
};
|
|
2848
|
+
}
|
|
2849
|
+
async function executeInSandbox(sandbox, toolCall) {
|
|
2850
|
+
const start = Date.now();
|
|
2851
|
+
if (sandbox.runtime === "docker") {
|
|
2852
|
+
const { execSync } = await import("child_process");
|
|
2853
|
+
try {
|
|
2854
|
+
const command = toolCall.args.command || `echo "Tool: ${toolCall.tool}"`;
|
|
2855
|
+
const output = execSync(
|
|
2856
|
+
`docker exec ${sandbox.id} sh -c '${command.replace(/'/g, "'\\''")}'`,
|
|
2857
|
+
{ stdio: "pipe", timeout: 3e4 }
|
|
2858
|
+
).toString();
|
|
2859
|
+
return {
|
|
2860
|
+
success: true,
|
|
2861
|
+
output: output.trim(),
|
|
2862
|
+
durationMs: Date.now() - start,
|
|
2863
|
+
exitCode: 0
|
|
2864
|
+
};
|
|
2865
|
+
} catch (err) {
|
|
2866
|
+
const execErr = err;
|
|
2867
|
+
return {
|
|
2868
|
+
success: false,
|
|
2869
|
+
output: "",
|
|
2870
|
+
error: execErr.stderr?.toString() || String(err),
|
|
2871
|
+
durationMs: Date.now() - start,
|
|
2872
|
+
exitCode: execErr.status || 1
|
|
2873
|
+
};
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
return {
|
|
2877
|
+
success: true,
|
|
2878
|
+
output: `[E2B] Executed ${toolCall.tool} in sandbox ${sandbox.id}`,
|
|
2879
|
+
durationMs: Date.now() - start
|
|
2880
|
+
};
|
|
2881
|
+
}
|
|
2882
|
+
function evaluatePolicy(tool, policy) {
|
|
2883
|
+
if (!policy) return "allow";
|
|
2884
|
+
const tools = policy.tools;
|
|
2885
|
+
if (!tools) return "allow";
|
|
2886
|
+
const toolPolicy = tools[tool] || tools["*"];
|
|
2887
|
+
if (!toolPolicy) return "allow";
|
|
2888
|
+
if (toolPolicy.block) return "deny";
|
|
2889
|
+
if (toolPolicy.require_approval) return "require_approval";
|
|
2890
|
+
return "allow";
|
|
2891
|
+
}
|
|
2892
|
+
|
|
2893
|
+
// src/evidence-authenticity.ts
|
|
2894
|
+
var import_node_crypto7 = require("crypto");
|
|
2895
|
+
async function createEvidenceAttestation(input) {
|
|
2896
|
+
const tlsNotaryAvailable = await isTLSNotaryAvailable();
|
|
2897
|
+
if (tlsNotaryAvailable) {
|
|
2898
|
+
return createTLSNotaryAttestation(input);
|
|
2899
|
+
}
|
|
2900
|
+
return {
|
|
2901
|
+
version: "0.1-beta",
|
|
2902
|
+
method: "self-reported",
|
|
2903
|
+
url: input.url,
|
|
2904
|
+
httpMethod: input.httpMethod || "GET",
|
|
2905
|
+
responseHash: input.responseHash,
|
|
2906
|
+
statusCode: input.statusCode || 200,
|
|
2907
|
+
fetchedAt: input.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
|
|
2908
|
+
verified: false,
|
|
2909
|
+
verificationNote: "Self-reported attestation. No third-party verification. TLSNotary integration planned for Q3 2026."
|
|
2910
|
+
};
|
|
2911
|
+
}
|
|
2912
|
+
async function verifyEvidenceAttestation(attestation) {
|
|
2913
|
+
switch (attestation.method) {
|
|
2914
|
+
case "self-reported":
|
|
2915
|
+
return {
|
|
2916
|
+
valid: false,
|
|
2917
|
+
method: "self-reported",
|
|
2918
|
+
note: "Self-reported attestation cannot be independently verified. The response hash is included for integrity checking if the original data is available."
|
|
2919
|
+
};
|
|
2920
|
+
case "tlsnotary":
|
|
2921
|
+
if (!attestation.notaryPublicKey || !attestation.notarySignature) {
|
|
2922
|
+
return {
|
|
2923
|
+
valid: false,
|
|
2924
|
+
method: "tlsnotary",
|
|
2925
|
+
note: "TLSNotary attestation is missing notary public key or signature."
|
|
2926
|
+
};
|
|
2927
|
+
}
|
|
2928
|
+
return {
|
|
2929
|
+
valid: false,
|
|
2930
|
+
method: "tlsnotary",
|
|
2931
|
+
note: "TLSNotary verification not yet implemented. Attestation format is correct but signature cannot be checked."
|
|
2932
|
+
};
|
|
2933
|
+
case "oracle":
|
|
2934
|
+
return {
|
|
2935
|
+
valid: attestation.verified,
|
|
2936
|
+
method: "oracle",
|
|
2937
|
+
note: attestation.verified ? "Attestation verified by oracle service." : "Oracle verification pending or failed."
|
|
2938
|
+
};
|
|
2939
|
+
case "witness":
|
|
2940
|
+
return {
|
|
2941
|
+
valid: attestation.verified,
|
|
2942
|
+
method: "witness",
|
|
2943
|
+
note: attestation.verified ? "Attestation witnessed by independent third party." : "Witness verification pending."
|
|
2944
|
+
};
|
|
2945
|
+
default:
|
|
2946
|
+
return {
|
|
2947
|
+
valid: false,
|
|
2948
|
+
method: "unknown",
|
|
2949
|
+
note: "Unknown attestation method."
|
|
2950
|
+
};
|
|
2951
|
+
}
|
|
2952
|
+
}
|
|
2953
|
+
function hashResponseBody(body) {
|
|
2954
|
+
return (0, import_node_crypto7.createHash)("sha256").update(typeof body === "string" ? body : body).digest("hex");
|
|
2955
|
+
}
|
|
2956
|
+
function createAttestationField(attestation) {
|
|
2957
|
+
return {
|
|
2958
|
+
evidence_authenticity: {
|
|
2959
|
+
version: attestation.version,
|
|
2960
|
+
method: attestation.method,
|
|
2961
|
+
url_hash: (0, import_node_crypto7.createHash)("sha256").update(attestation.url).digest("hex").slice(0, 16),
|
|
2962
|
+
response_hash: attestation.responseHash,
|
|
2963
|
+
fetched_at: attestation.fetchedAt,
|
|
2964
|
+
verified: attestation.verified,
|
|
2965
|
+
note: attestation.verificationNote
|
|
2966
|
+
}
|
|
2967
|
+
};
|
|
2968
|
+
}
|
|
2969
|
+
async function isTLSNotaryAvailable() {
|
|
2970
|
+
try {
|
|
2971
|
+
await import("tlsn-js");
|
|
2972
|
+
return true;
|
|
2973
|
+
} catch {
|
|
2974
|
+
return false;
|
|
2975
|
+
}
|
|
2976
|
+
}
|
|
2977
|
+
async function createTLSNotaryAttestation(input) {
|
|
2978
|
+
return {
|
|
2979
|
+
version: "0.1-beta",
|
|
2980
|
+
method: "tlsnotary",
|
|
2981
|
+
url: input.url,
|
|
2982
|
+
httpMethod: input.httpMethod || "GET",
|
|
2983
|
+
responseHash: input.responseHash,
|
|
2984
|
+
statusCode: input.statusCode || 200,
|
|
2985
|
+
fetchedAt: input.timestamp || (/* @__PURE__ */ new Date()).toISOString(),
|
|
2986
|
+
verified: false,
|
|
2987
|
+
verificationNote: "TLSNotary SDK integration in progress. Attestation format is stable; verification will be enabled in a future release."
|
|
2988
|
+
};
|
|
2989
|
+
}
|
|
2990
|
+
|
|
2991
|
+
// src/c2pa-credentials.ts
|
|
2992
|
+
var import_node_crypto8 = require("crypto");
|
|
2993
|
+
function createC2PAManifest(receipts, options) {
|
|
2994
|
+
const generator = options.generator || "protect-mcp";
|
|
2995
|
+
const version = options.version || "0.3.3";
|
|
2996
|
+
const decisions = receipts.filter(
|
|
2997
|
+
(r) => r.receipt_type?.includes("decision") || r.type?.includes("decision")
|
|
2998
|
+
);
|
|
2999
|
+
const allows = decisions.filter(
|
|
3000
|
+
(r) => r.payload?.decision === "allow"
|
|
3001
|
+
);
|
|
3002
|
+
const denies = decisions.filter(
|
|
3003
|
+
(r) => r.payload?.decision === "deny"
|
|
3004
|
+
);
|
|
3005
|
+
const receiptHashes = receipts.map(
|
|
3006
|
+
(r) => (0, import_node_crypto8.createHash)("sha256").update(JSON.stringify(r)).digest("hex")
|
|
3007
|
+
);
|
|
3008
|
+
const merkleRoot = computeMerkleRoot(receiptHashes);
|
|
3009
|
+
const assertions = [
|
|
3010
|
+
// Acta decision provenance — the core assertion
|
|
3011
|
+
{
|
|
3012
|
+
label: "acta.decision-provenance",
|
|
3013
|
+
data: {
|
|
3014
|
+
protocol: "veritas-acta",
|
|
3015
|
+
protocol_version: "0.1",
|
|
3016
|
+
ietf_draft: "draft-farley-acta-signed-receipts-00",
|
|
3017
|
+
receipt_count: receipts.length,
|
|
3018
|
+
decision_count: decisions.length,
|
|
3019
|
+
allows: allows.length,
|
|
3020
|
+
denies: denies.length,
|
|
3021
|
+
merkle_root: merkleRoot,
|
|
3022
|
+
signing_algorithm: "Ed25519",
|
|
3023
|
+
canonicalization: "JCS (RFC 8785)",
|
|
3024
|
+
verifier: "npx @veritasacta/verify",
|
|
3025
|
+
verify_url: "https://scopeblind.com/verify",
|
|
3026
|
+
trace_url: "https://scopeblind.com/trace"
|
|
3027
|
+
}
|
|
3028
|
+
},
|
|
3029
|
+
// Policy compliance assertion
|
|
3030
|
+
{
|
|
3031
|
+
label: "acta.policy-compliance",
|
|
3032
|
+
data: {
|
|
3033
|
+
policy_violations: denies.length,
|
|
3034
|
+
total_decisions: decisions.length,
|
|
3035
|
+
compliance_rate: decisions.length > 0 ? (allows.length / decisions.length * 100).toFixed(1) + "%" : "N/A",
|
|
3036
|
+
policy_engine: "Cedar + JSON",
|
|
3037
|
+
human_approvals: receipts.filter(
|
|
3038
|
+
(r) => r.receipt_type?.includes("approval") || r.type?.includes("approval")
|
|
3039
|
+
).length
|
|
3040
|
+
}
|
|
3041
|
+
},
|
|
3042
|
+
// Standard C2PA actions
|
|
3043
|
+
{
|
|
3044
|
+
label: "c2pa.actions",
|
|
3045
|
+
data: {
|
|
3046
|
+
actions: [
|
|
3047
|
+
{
|
|
3048
|
+
action: "c2pa.created",
|
|
3049
|
+
when: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3050
|
+
softwareAgent: `${generator}/${version}`,
|
|
3051
|
+
parameters: {
|
|
3052
|
+
description: "Content generated by AI agent with ScopeBlind governance"
|
|
3053
|
+
}
|
|
3054
|
+
}
|
|
3055
|
+
]
|
|
3056
|
+
}
|
|
3057
|
+
}
|
|
3058
|
+
];
|
|
3059
|
+
if (options.includeFullReceipts) {
|
|
3060
|
+
assertions.push({
|
|
3061
|
+
label: "acta.receipt-chain",
|
|
3062
|
+
data: {
|
|
3063
|
+
receipts: receipts.map((r) => ({
|
|
3064
|
+
id: r.receipt_id || r.id,
|
|
3065
|
+
type: r.receipt_type || r.type,
|
|
3066
|
+
tool: r.payload?.tool_name,
|
|
3067
|
+
decision: r.payload?.decision,
|
|
3068
|
+
timestamp: r.timestamp || r.event_time
|
|
3069
|
+
}))
|
|
3070
|
+
}
|
|
3071
|
+
});
|
|
3072
|
+
} else {
|
|
3073
|
+
assertions.push({
|
|
3074
|
+
label: "acta.receipt-chain",
|
|
3075
|
+
data: {
|
|
3076
|
+
receipt_hashes: receiptHashes,
|
|
3077
|
+
merkle_root: merkleRoot,
|
|
3078
|
+
note: "Full receipts available via verify URL. Hashes provided for integrity verification."
|
|
3079
|
+
},
|
|
3080
|
+
is_hash: true
|
|
3081
|
+
});
|
|
3082
|
+
}
|
|
3083
|
+
if (options.additionalAssertions) {
|
|
3084
|
+
assertions.push(...options.additionalAssertions);
|
|
3085
|
+
}
|
|
3086
|
+
return {
|
|
3087
|
+
claim_generator: `${generator}/${version}`,
|
|
3088
|
+
claim_generator_info: [
|
|
3089
|
+
{
|
|
3090
|
+
name: generator,
|
|
3091
|
+
version
|
|
3092
|
+
}
|
|
3093
|
+
],
|
|
3094
|
+
title: options.title,
|
|
3095
|
+
assertions
|
|
3096
|
+
};
|
|
3097
|
+
}
|
|
3098
|
+
function exportC2PAManifestJSON(manifest) {
|
|
3099
|
+
return JSON.stringify(manifest, null, 2);
|
|
3100
|
+
}
|
|
3101
|
+
function generateC2PACommand(manifestPath, inputPath, outputPath) {
|
|
3102
|
+
return `c2patool ${inputPath} -m ${manifestPath} -o ${outputPath}`;
|
|
3103
|
+
}
|
|
3104
|
+
function verifyActaC2PAAssertions(c2paManifestJson) {
|
|
3105
|
+
try {
|
|
3106
|
+
const manifest = JSON.parse(c2paManifestJson);
|
|
3107
|
+
const assertions = manifest.assertions || [];
|
|
3108
|
+
const provenanceAssertion = assertions.find(
|
|
3109
|
+
(a) => a.label === "acta.decision-provenance"
|
|
3110
|
+
);
|
|
3111
|
+
const complianceAssertion = assertions.find(
|
|
3112
|
+
(a) => a.label === "acta.policy-compliance"
|
|
3113
|
+
);
|
|
3114
|
+
if (!provenanceAssertion) {
|
|
3115
|
+
return {
|
|
3116
|
+
hasActaProvenance: false,
|
|
3117
|
+
receiptCount: 0,
|
|
3118
|
+
merkleRoot: null,
|
|
3119
|
+
complianceRate: null,
|
|
3120
|
+
verifyUrl: null
|
|
3121
|
+
};
|
|
3122
|
+
}
|
|
3123
|
+
return {
|
|
3124
|
+
hasActaProvenance: true,
|
|
3125
|
+
receiptCount: provenanceAssertion.data.receipt_count || 0,
|
|
3126
|
+
merkleRoot: provenanceAssertion.data.merkle_root || null,
|
|
3127
|
+
complianceRate: complianceAssertion ? complianceAssertion.data.compliance_rate : null,
|
|
3128
|
+
verifyUrl: provenanceAssertion.data.verify_url || null
|
|
3129
|
+
};
|
|
3130
|
+
} catch {
|
|
3131
|
+
return {
|
|
3132
|
+
hasActaProvenance: false,
|
|
3133
|
+
receiptCount: 0,
|
|
3134
|
+
merkleRoot: null,
|
|
3135
|
+
complianceRate: null,
|
|
3136
|
+
verifyUrl: null
|
|
3137
|
+
};
|
|
3138
|
+
}
|
|
3139
|
+
}
|
|
3140
|
+
function computeMerkleRoot(hashes) {
|
|
3141
|
+
if (hashes.length === 0) return "";
|
|
3142
|
+
if (hashes.length === 1) return hashes[0];
|
|
3143
|
+
const nextLevel = [];
|
|
3144
|
+
for (let i = 0; i < hashes.length; i += 2) {
|
|
3145
|
+
const left = hashes[i];
|
|
3146
|
+
const right = i + 1 < hashes.length ? hashes[i + 1] : left;
|
|
3147
|
+
nextLevel.push(
|
|
3148
|
+
(0, import_node_crypto8.createHash)("sha256").update(left + right).digest("hex")
|
|
3149
|
+
);
|
|
3150
|
+
}
|
|
3151
|
+
return computeMerkleRoot(nextLevel);
|
|
3152
|
+
}
|
|
3153
|
+
|
|
3154
|
+
// src/prediction-bridge.ts
|
|
3155
|
+
function computeCalibration(predictions, resolutions) {
|
|
3156
|
+
let totalSquaredError = 0;
|
|
3157
|
+
let resolved = 0;
|
|
3158
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
3159
|
+
for (const pred of predictions) {
|
|
3160
|
+
const resolution = resolutions.get(pred.receipt_id);
|
|
3161
|
+
if (!resolution || resolution.payload.resolution_value === "ambiguous") continue;
|
|
3162
|
+
resolved++;
|
|
3163
|
+
const actual = resolution.payload.resolution_value === "true" ? 1 : 0;
|
|
3164
|
+
const error = (pred.payload.probability - actual) ** 2;
|
|
3165
|
+
totalSquaredError += error;
|
|
3166
|
+
const bucketKey = `${Math.floor(pred.payload.probability * 10) / 10}-${Math.ceil(pred.payload.probability * 10) / 10}`;
|
|
3167
|
+
const bucket = buckets.get(bucketKey) || { sum: 0, actual: 0, count: 0 };
|
|
3168
|
+
bucket.sum += pred.payload.probability;
|
|
3169
|
+
bucket.actual += actual;
|
|
3170
|
+
bucket.count++;
|
|
3171
|
+
buckets.set(bucketKey, bucket);
|
|
3172
|
+
}
|
|
3173
|
+
return {
|
|
3174
|
+
total_predictions: predictions.length,
|
|
3175
|
+
resolved,
|
|
3176
|
+
brier_score: resolved > 0 ? totalSquaredError / resolved : 0,
|
|
3177
|
+
calibration_buckets: Array.from(buckets.entries()).map(([bucket, data]) => ({
|
|
3178
|
+
bucket,
|
|
3179
|
+
predicted_probability: data.sum / data.count,
|
|
3180
|
+
actual_frequency: data.actual / data.count,
|
|
3181
|
+
count: data.count
|
|
3182
|
+
}))
|
|
3183
|
+
};
|
|
3184
|
+
}
|
|
3185
|
+
function toMetaculusFormat(prediction) {
|
|
3186
|
+
return {
|
|
3187
|
+
prediction_value: prediction.payload.probability,
|
|
3188
|
+
acta_receipt_id: prediction.receipt_id,
|
|
3189
|
+
acta_signature: prediction.signature
|
|
3190
|
+
};
|
|
3191
|
+
}
|
|
3192
|
+
function toManifoldFormat(prediction) {
|
|
3193
|
+
return {
|
|
3194
|
+
probability: prediction.payload.probability,
|
|
3195
|
+
acta_receipt_id: prediction.receipt_id,
|
|
3196
|
+
acta_signature: prediction.signature
|
|
3197
|
+
};
|
|
3198
|
+
}
|
|
3199
|
+
|
|
3200
|
+
// src/agent-exchange.ts
|
|
3201
|
+
var import_node_crypto9 = require("crypto");
|
|
3202
|
+
var ReceiptPropagator = class {
|
|
3203
|
+
issuer;
|
|
3204
|
+
signer;
|
|
3205
|
+
receipts = /* @__PURE__ */ new Map();
|
|
3206
|
+
delegationCallCounts = /* @__PURE__ */ new Map();
|
|
3207
|
+
constructor(config) {
|
|
3208
|
+
this.issuer = config.issuer;
|
|
3209
|
+
this.signer = config.signer;
|
|
3210
|
+
}
|
|
3211
|
+
/**
|
|
3212
|
+
* Create a delegation receipt authorizing another agent to use specific tools.
|
|
3213
|
+
*
|
|
3214
|
+
* @patent Patent-protected construction — delegated signing with receipt chain
|
|
3215
|
+
* propagation. Covered by Apache 2.0 patent grant for users of this code.
|
|
3216
|
+
* Clean-room reimplementation requires a patent license.
|
|
3217
|
+
* @see {@link https://datatracker.ietf.org/doc/draft-farley-acta-signed-receipts/}
|
|
3218
|
+
*/
|
|
3219
|
+
delegate(delegateId, options) {
|
|
3220
|
+
const now = /* @__PURE__ */ new Date();
|
|
3221
|
+
const receipt = {
|
|
3222
|
+
receipt_id: `del_${(0, import_node_crypto9.randomUUID)().slice(0, 12)}`,
|
|
3223
|
+
receipt_type: "delegation",
|
|
3224
|
+
issuer_id: this.issuer,
|
|
3225
|
+
event_time: now.toISOString(),
|
|
3226
|
+
payload: {
|
|
3227
|
+
delegate_id: delegateId,
|
|
3228
|
+
authorized_tools: options.tools,
|
|
3229
|
+
scope: options.scope,
|
|
3230
|
+
ttl: options.ttl,
|
|
3231
|
+
expires_at: new Date(now.getTime() + options.ttl * 1e3).toISOString(),
|
|
3232
|
+
max_calls: options.maxCalls,
|
|
3233
|
+
allow_subdelegation: options.allowSubdelegation ?? false
|
|
3234
|
+
},
|
|
3235
|
+
parent_receipts: options.parentReceipts || []
|
|
3236
|
+
};
|
|
3237
|
+
if (this.signer) {
|
|
3238
|
+
const signed = this.signer(receipt);
|
|
3239
|
+
Object.assign(receipt, signed);
|
|
3240
|
+
}
|
|
3241
|
+
this.receipts.set(receipt.receipt_id, receipt);
|
|
3242
|
+
this.delegationCallCounts.set(receipt.receipt_id, 0);
|
|
3243
|
+
return receipt;
|
|
3244
|
+
}
|
|
3245
|
+
/**
|
|
3246
|
+
* Wrap a tool call with a receipt that references the delegation.
|
|
3247
|
+
* Validates the delegation is still valid (not expired, within call limit,
|
|
3248
|
+
* tool is authorized).
|
|
3249
|
+
*
|
|
3250
|
+
* @patent Patent-protected construction — delegated signing with receipt chain
|
|
3251
|
+
* propagation. Covered by Apache 2.0 patent grant for users of this code.
|
|
3252
|
+
* Clean-room reimplementation requires a patent license.
|
|
3253
|
+
* @see {@link https://datatracker.ietf.org/doc/draft-farley-acta-signed-receipts/}
|
|
3254
|
+
*/
|
|
3255
|
+
wrapAction(toolName, options) {
|
|
3256
|
+
const delegation = this.receipts.get(options.delegation_receipt);
|
|
3257
|
+
let decision = "allow";
|
|
3258
|
+
if (!delegation) {
|
|
3259
|
+
decision = "deny";
|
|
3260
|
+
} else if (delegation.receipt_type !== "delegation") {
|
|
3261
|
+
decision = "deny";
|
|
3262
|
+
} else {
|
|
3263
|
+
if (new Date(delegation.payload.expires_at) < /* @__PURE__ */ new Date()) {
|
|
3264
|
+
decision = "deny";
|
|
3265
|
+
}
|
|
3266
|
+
if (!delegation.payload.authorized_tools.includes(toolName) && !delegation.payload.authorized_tools.includes("*")) {
|
|
3267
|
+
decision = "deny";
|
|
3268
|
+
}
|
|
3269
|
+
if (delegation.payload.max_calls !== void 0) {
|
|
3270
|
+
const count = this.delegationCallCounts.get(options.delegation_receipt) || 0;
|
|
3271
|
+
if (count >= delegation.payload.max_calls) {
|
|
3272
|
+
decision = "deny";
|
|
3273
|
+
}
|
|
3274
|
+
}
|
|
3275
|
+
}
|
|
3276
|
+
const currentCount = this.delegationCallCounts.get(options.delegation_receipt) || 0;
|
|
3277
|
+
this.delegationCallCounts.set(options.delegation_receipt, currentCount + 1);
|
|
3278
|
+
const receipt = {
|
|
3279
|
+
receipt_id: `act_${(0, import_node_crypto9.randomUUID)().slice(0, 12)}`,
|
|
3280
|
+
receipt_type: "execution",
|
|
3281
|
+
issuer_id: this.issuer,
|
|
3282
|
+
event_time: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3283
|
+
payload: {
|
|
3284
|
+
tool_name: toolName,
|
|
3285
|
+
decision,
|
|
3286
|
+
delegation_receipt: options.delegation_receipt,
|
|
3287
|
+
scope: delegation?.payload.scope || "unknown",
|
|
3288
|
+
call_index: currentCount + 1
|
|
3289
|
+
},
|
|
3290
|
+
parent_receipts: [options.delegation_receipt]
|
|
3291
|
+
};
|
|
3292
|
+
if (this.signer) {
|
|
3293
|
+
const signed = this.signer(receipt);
|
|
3294
|
+
Object.assign(receipt, signed);
|
|
3295
|
+
}
|
|
3296
|
+
this.receipts.set(receipt.receipt_id, receipt);
|
|
3297
|
+
return receipt;
|
|
3298
|
+
}
|
|
3299
|
+
/**
|
|
3300
|
+
* Trace the full receipt chain from a given receipt back to the root delegation.
|
|
3301
|
+
*
|
|
3302
|
+
* @patent Patent-protected construction — delegated signing with receipt chain
|
|
3303
|
+
* propagation. Covered by Apache 2.0 patent grant for users of this code.
|
|
3304
|
+
* Clean-room reimplementation requires a patent license.
|
|
3305
|
+
* @see {@link https://datatracker.ietf.org/doc/draft-farley-acta-signed-receipts/}
|
|
3306
|
+
*/
|
|
3307
|
+
traceChain(receiptId) {
|
|
3308
|
+
const chain = [];
|
|
3309
|
+
const visited = /* @__PURE__ */ new Set();
|
|
3310
|
+
const walk = (id) => {
|
|
3311
|
+
if (visited.has(id)) return;
|
|
3312
|
+
visited.add(id);
|
|
3313
|
+
const receipt = this.receipts.get(id);
|
|
3314
|
+
if (!receipt) return;
|
|
3315
|
+
for (const parentId of receipt.parent_receipts) {
|
|
3316
|
+
walk(parentId);
|
|
3317
|
+
}
|
|
3318
|
+
chain.push(receipt);
|
|
3319
|
+
};
|
|
3320
|
+
walk(receiptId);
|
|
3321
|
+
return chain;
|
|
3322
|
+
}
|
|
3323
|
+
/**
|
|
3324
|
+
* Export all receipts as a JSON array (for verification, archival, or Trace visualization).
|
|
3325
|
+
*/
|
|
3326
|
+
exportAll() {
|
|
3327
|
+
return Array.from(this.receipts.values());
|
|
3328
|
+
}
|
|
3329
|
+
/**
|
|
3330
|
+
* Validate that a delegation chain is intact and all signatures verify.
|
|
3331
|
+
*
|
|
3332
|
+
* @patent Patent-protected construction — delegated signing with receipt chain
|
|
3333
|
+
* propagation. Covered by Apache 2.0 patent grant for users of this code.
|
|
3334
|
+
* Clean-room reimplementation requires a patent license.
|
|
3335
|
+
* @see {@link https://datatracker.ietf.org/doc/draft-farley-acta-signed-receipts/}
|
|
3336
|
+
*/
|
|
3337
|
+
validateChain(receiptId) {
|
|
3338
|
+
const chain = this.traceChain(receiptId);
|
|
3339
|
+
const issues = [];
|
|
3340
|
+
if (chain.length === 0) {
|
|
3341
|
+
return { valid: false, chain_length: 0, issues: ["Receipt not found"] };
|
|
3342
|
+
}
|
|
3343
|
+
let sawAction = false;
|
|
3344
|
+
for (const receipt of chain) {
|
|
3345
|
+
if (receipt.receipt_type === "delegation" && sawAction) {
|
|
3346
|
+
issues.push(`Delegation ${receipt.receipt_id} appears after action in chain`);
|
|
3347
|
+
}
|
|
3348
|
+
if (receipt.receipt_type === "execution") sawAction = true;
|
|
3349
|
+
}
|
|
3350
|
+
for (const receipt of chain) {
|
|
3351
|
+
for (const parentId of receipt.parent_receipts) {
|
|
3352
|
+
if (!this.receipts.has(parentId)) {
|
|
3353
|
+
issues.push(`Missing parent receipt: ${parentId}`);
|
|
3354
|
+
}
|
|
3355
|
+
}
|
|
3356
|
+
}
|
|
3357
|
+
return {
|
|
3358
|
+
valid: issues.length === 0,
|
|
3359
|
+
chain_length: chain.length,
|
|
3360
|
+
issues
|
|
3361
|
+
};
|
|
3362
|
+
}
|
|
3363
|
+
};
|
|
3364
|
+
function createReceiptChannel(orchestratorId) {
|
|
3365
|
+
const propagator = new ReceiptPropagator({ issuer: orchestratorId });
|
|
3366
|
+
return {
|
|
3367
|
+
propagator,
|
|
3368
|
+
async withDelegation(delegateId, tools, fn, options) {
|
|
3369
|
+
const delegation = propagator.delegate(delegateId, {
|
|
3370
|
+
tools,
|
|
3371
|
+
scope: options?.scope || `task-${(0, import_node_crypto9.randomUUID)().slice(0, 8)}`,
|
|
3372
|
+
ttl: options?.ttl || 3600,
|
|
3373
|
+
maxCalls: options?.maxCalls
|
|
3374
|
+
});
|
|
3375
|
+
const result = await fn({ delegation, propagator });
|
|
3376
|
+
return {
|
|
3377
|
+
result,
|
|
3378
|
+
delegation,
|
|
3379
|
+
chain: propagator.exportAll()
|
|
3380
|
+
};
|
|
3381
|
+
}
|
|
3382
|
+
};
|
|
3383
|
+
}
|
|
3384
|
+
|
|
3385
|
+
// src/confidential.ts
|
|
3386
|
+
var ConfidentialGate = class {
|
|
3387
|
+
config;
|
|
3388
|
+
constructor(config) {
|
|
3389
|
+
this.config = config;
|
|
3390
|
+
}
|
|
3391
|
+
/**
|
|
3392
|
+
* Evaluate an attestation document and determine the resulting trust tier.
|
|
3393
|
+
*/
|
|
3394
|
+
evaluateAttestation(doc) {
|
|
3395
|
+
if (!this.config.accepted_providers.includes(doc.provider)) {
|
|
3396
|
+
return {
|
|
3397
|
+
accepted: false,
|
|
3398
|
+
tier: "unknown",
|
|
3399
|
+
provider: doc.provider,
|
|
3400
|
+
reason: `Provider ${doc.provider} not in accepted list: ${this.config.accepted_providers.join(", ")}`
|
|
3401
|
+
};
|
|
3402
|
+
}
|
|
3403
|
+
if (this.config.max_attestation_age) {
|
|
3404
|
+
const age = (Date.now() - new Date(doc.timestamp).getTime()) / 1e3;
|
|
3405
|
+
if (age > this.config.max_attestation_age) {
|
|
3406
|
+
return {
|
|
3407
|
+
accepted: false,
|
|
3408
|
+
tier: "unknown",
|
|
3409
|
+
provider: doc.provider,
|
|
3410
|
+
reason: `Attestation expired: age ${Math.floor(age)}s exceeds max ${this.config.max_attestation_age}s`
|
|
3411
|
+
};
|
|
3412
|
+
}
|
|
3413
|
+
}
|
|
3414
|
+
if (this.config.expected_measurements) {
|
|
3415
|
+
for (const [key, expected] of Object.entries(this.config.expected_measurements)) {
|
|
3416
|
+
const actual = doc.measurements[key];
|
|
3417
|
+
if (actual !== expected) {
|
|
3418
|
+
return {
|
|
3419
|
+
accepted: false,
|
|
3420
|
+
tier: "signed",
|
|
3421
|
+
provider: doc.provider,
|
|
3422
|
+
reason: `Measurement mismatch: ${key} expected ${expected}, got ${actual || "missing"}`
|
|
3423
|
+
};
|
|
3424
|
+
}
|
|
3425
|
+
}
|
|
3426
|
+
}
|
|
3427
|
+
return {
|
|
3428
|
+
accepted: true,
|
|
3429
|
+
tier: "privileged",
|
|
3430
|
+
provider: doc.provider,
|
|
3431
|
+
reason: `Attestation verified: ${doc.provider} enclave with valid measurements`
|
|
3432
|
+
};
|
|
3433
|
+
}
|
|
3434
|
+
/**
|
|
3435
|
+
* Check if an agent's current tier requires attestation.
|
|
3436
|
+
*/
|
|
3437
|
+
requiresAttestation(currentTier) {
|
|
3438
|
+
if (!this.config.require_attestation) return false;
|
|
3439
|
+
const tierOrder = ["unknown", "signed", "evidenced", "privileged"];
|
|
3440
|
+
const requiredIdx = tierOrder.indexOf(this.config.min_trust_tier);
|
|
3441
|
+
const currentIdx = tierOrder.indexOf(currentTier);
|
|
3442
|
+
return currentIdx >= requiredIdx;
|
|
3443
|
+
}
|
|
3444
|
+
/**
|
|
3445
|
+
* Generate an attestation receipt documenting the evaluation.
|
|
3446
|
+
*/
|
|
3447
|
+
toReceipt(result, agentId) {
|
|
3448
|
+
return {
|
|
3449
|
+
receipt_type: "attestation",
|
|
3450
|
+
issuer_id: "confidential-gate",
|
|
3451
|
+
event_time: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3452
|
+
payload: {
|
|
3453
|
+
agent_id: agentId,
|
|
3454
|
+
provider: result.provider,
|
|
3455
|
+
accepted: result.accepted,
|
|
3456
|
+
resulting_tier: result.tier,
|
|
3457
|
+
reason: result.reason
|
|
3458
|
+
}
|
|
3459
|
+
};
|
|
3460
|
+
}
|
|
3461
|
+
};
|
|
3462
|
+
async function confidentialInference(_prompt, _config) {
|
|
3463
|
+
throw new Error(
|
|
3464
|
+
"Confidential inference requires a TEE/HE provider SDK. See docs at scopeblind.com/docs/confidential for setup instructions. Supported providers: Gramine (local_tee), Zama Concrete ML (homomorphic), NVIDIA Confidential Computing (secure_enclave)."
|
|
3465
|
+
);
|
|
3466
|
+
}
|
|
1805
3467
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1806
3468
|
0 && (module.exports = {
|
|
3469
|
+
ConfidentialGate,
|
|
1807
3470
|
ProtectGateway,
|
|
3471
|
+
ReceiptPropagator,
|
|
3472
|
+
anchorToRekor,
|
|
1808
3473
|
buildDecisionContext,
|
|
1809
3474
|
checkRateLimit,
|
|
1810
3475
|
collectSignedReceipts,
|
|
3476
|
+
computeCalibration,
|
|
3477
|
+
confidentialInference,
|
|
3478
|
+
createApprovalChallenge,
|
|
3479
|
+
createApprovalReceiptPayload,
|
|
3480
|
+
createAttestationField,
|
|
1811
3481
|
createAuditBundle,
|
|
3482
|
+
createC2PAManifest,
|
|
3483
|
+
createDisclosurePackage,
|
|
3484
|
+
createEvidenceAttestation,
|
|
3485
|
+
createLogAnchorField,
|
|
3486
|
+
createReceiptChannel,
|
|
3487
|
+
createSandbox,
|
|
3488
|
+
destroySandbox,
|
|
3489
|
+
ed25519ToDIDKey,
|
|
1812
3490
|
evaluateTier,
|
|
3491
|
+
exportC2PAManifestJSON,
|
|
3492
|
+
exportJSONL,
|
|
1813
3493
|
formatReportMarkdown,
|
|
1814
3494
|
formatSimulation,
|
|
3495
|
+
generateC2PACommand,
|
|
3496
|
+
generateDatasetCard,
|
|
3497
|
+
generateHFMetadata,
|
|
1815
3498
|
generateReport,
|
|
3499
|
+
generateSafetyTranscript,
|
|
1816
3500
|
getSignerInfo,
|
|
1817
3501
|
getToolPolicy,
|
|
3502
|
+
hashReceipt,
|
|
3503
|
+
hashResponseBody,
|
|
1818
3504
|
initSigning,
|
|
1819
3505
|
isAgentId,
|
|
1820
3506
|
isDisclosureMode,
|
|
@@ -1823,14 +3509,31 @@ function validateEvidenceReceipt(receipt) {
|
|
|
1823
3509
|
isSigningEnabled,
|
|
1824
3510
|
listCredentialLabels,
|
|
1825
3511
|
loadPolicy,
|
|
3512
|
+
manifestToVC,
|
|
1826
3513
|
meetsMinTier,
|
|
1827
3514
|
parseLogFile,
|
|
3515
|
+
parseNotificationConfigFromEnv,
|
|
1828
3516
|
parseRateLimit,
|
|
1829
3517
|
queryExternalPDP,
|
|
3518
|
+
receiptToVP,
|
|
3519
|
+
receiptsToHFRows,
|
|
3520
|
+
redactFields,
|
|
1830
3521
|
resolveCredential,
|
|
3522
|
+
revealField,
|
|
3523
|
+
runInSandbox,
|
|
3524
|
+
sendApprovalNotification,
|
|
1831
3525
|
signDecision,
|
|
1832
3526
|
simulate,
|
|
3527
|
+
toCredentialRequestOptions,
|
|
3528
|
+
toManifoldFormat,
|
|
3529
|
+
toMetaculusFormat,
|
|
1833
3530
|
validateCredentials,
|
|
1834
3531
|
validateEvidenceReceipt,
|
|
1835
|
-
validateManifest
|
|
3532
|
+
validateManifest,
|
|
3533
|
+
verifyActaC2PAAssertions,
|
|
3534
|
+
verifyAllCommitments,
|
|
3535
|
+
verifyApprovalAssertion,
|
|
3536
|
+
verifyCommitment,
|
|
3537
|
+
verifyEvidenceAttestation,
|
|
3538
|
+
verifyRekorAnchor
|
|
1836
3539
|
});
|