thumbgate 1.35.0 → 1.37.2
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/.agents/skills/cobble-hot-store-compare-not-clone/SKILL.md +80 -0
- package/.agents/skills/colab-compute-honesty-not-clone/SKILL.md +70 -0
- package/.agents/skills/cyberstrike-compare-not-clone/SKILL.md +36 -0
- package/.agents/skills/gitlab-sandbox-allowlist-not-trust/SKILL.md +77 -0
- package/.agents/skills/jit-harness-compare-not-clone/SKILL.md +34 -0
- package/.agents/skills/openui-catalog-compose-honesty/SKILL.md +64 -0
- package/.agents/skills/token-shunt-honesty-not-clone/SKILL.md +67 -0
- package/.agents/skills/typesafe-typed-questions-not-clone/SKILL.md +82 -0
- package/.agents/skills/zvec-grep-compare-not-clone/SKILL.md +34 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.well-known/llms.txt +1 -0
- package/.well-known/mcp/server-card.json +1 -1
- package/CONTRIBUTING.md +95 -0
- package/README.md +195 -632
- package/THIRD_PARTY_NOTICES.md +89 -0
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/forge/forge.yaml +3 -3
- package/adapters/future-agi/.mcp.json +8 -0
- package/adapters/future-agi/FUTURE_AGI.md +23 -0
- package/adapters/future-agi/config.toml +3 -0
- package/adapters/future-agi/future-agi-bridge.js +9 -0
- package/adapters/future-agi/opencode.json +8 -0
- package/adapters/herdr/herdr-plugin.toml +18 -0
- package/adapters/mcp/server-stdio.js +238 -25
- package/adapters/opencode/opencode.json +1 -1
- package/adapters/workos/WORKOS.md +52 -0
- package/bin/cli.js +465 -3
- package/bin/futureagi-bridge +9 -0
- package/config/gate-templates.json +629 -4
- package/config/gates/actor-critic-audit.json +34 -0
- package/config/gates/default.json +9 -3
- package/config/gates/five-walls-governance.json +34 -0
- package/config/gates/future-agi-guardrails.json +34 -0
- package/config/gates/radware-threat-defense-2026.json +61 -0
- package/config/gates/simatree-data-governance.json +33 -0
- package/config/mcp-allowlists.json +4 -0
- package/config/merge-quality-checks.json +6 -0
- package/config/model-candidates.json +312 -29
- package/config/model-tiers.json +18 -0
- package/config/post-deploy-marketing-pages.json +10 -0
- package/config/progressive/01-wire-only.json +11 -0
- package/config/progressive/02-dashboard-empty-ok.json +10 -0
- package/config/progressive/03-one-lesson.json +10 -0
- package/config/progressive/04-warn-fires.json +11 -0
- package/config/progressive/05-strict-optional.json +11 -0
- package/config/progressive/README.md +15 -0
- package/config/schemas/broker-execution-receipt.schema.json +139 -0
- package/config/schemas/provider-execution-attestation-v1.schema.json +58 -0
- package/conformance/provider-attestation/vectors.json +320 -0
- package/docs/specs/provider-execution-attestation-v1.md +69 -0
- package/openapi/openapi.yaml +15 -0
- package/package.json +408 -150
- package/public/about.html +2 -2
- package/public/ai-malpractice-prevention.html +7 -7
- package/public/blog/a-10-dollar-vps-is-not-a-computer.html +143 -0
- package/public/blog/a-receipt-is-not-world-state.html +388 -0
- package/public/blog/git-at-agent-scale.html +374 -0
- package/public/blog/no-llm-in-the-gate.html +133 -0
- package/public/blog.html +80 -0
- package/public/case-studies.html +16 -1
- package/public/compare.html +28 -0
- package/public/diagnostic.html +216 -7
- package/public/docs/connectors.html +39 -0
- package/public/federal.html +2 -2
- package/public/founders.html +639 -0
- package/public/index.html +87 -9
- package/public/install.html +8 -8
- package/public/learn.html +39 -0
- package/public/numbers.html +2 -2
- package/public/peter.html +310 -0
- package/public/platform-partners.html +119 -0
- package/public/pricing.html +24 -3
- package/public/privacy.html +117 -0
- package/public/pro.html +17 -0
- package/public/support.html +62 -0
- package/public/terms.html +130 -0
- package/public/third-party-notices.html +95 -0
- package/public/yt.html +351 -0
- package/scripts/action-receipts.js +133 -3
- package/scripts/adaptive-governance-arena.js +349 -0
- package/scripts/admin-override.js +205 -0
- package/scripts/agent-action-inventory.js +869 -0
- package/scripts/agent-audit-trace.js +42 -2
- package/scripts/agent-egress-policy.js +1117 -0
- package/scripts/agent-memory-lifecycle.js +141 -2
- package/scripts/agent-operations-planner.js +441 -1
- package/scripts/agent-readiness.js +68 -0
- package/scripts/agent-security-central.js +647 -0
- package/scripts/allowlist-bridge-honesty.js +417 -0
- package/scripts/async-job-runner.js +102 -11
- package/scripts/audit-trail.js +212 -0
- package/scripts/billing.js +1 -1
- package/scripts/broker-execution-receipts.js +719 -0
- package/scripts/budget-aware-gates-proof.js +423 -0
- package/scripts/claude-feedback-sync.js +29 -3
- package/scripts/claw-harness-production.js +237 -0
- package/scripts/cli-schema.js +203 -1
- package/scripts/cobble-hot-store-split.js +600 -0
- package/scripts/codex-runbook-flywheel.js +318 -0
- package/scripts/colab-compute-honesty.js +281 -0
- package/scripts/context-footprint.js +186 -0
- package/scripts/contextfs.js +143 -61
- package/scripts/dashboard.js +251 -32
- package/scripts/deepseek-v4-runtime-guardrails.js +72 -6
- package/scripts/docker-sandbox-planner.js +18 -0
- package/scripts/double-blind-eval-protocol.js +252 -0
- package/scripts/edotenv-rl-gateway.js +259 -0
- package/scripts/ensure-production-search-corpus.js +162 -0
- package/scripts/eval-holdout.js +311 -0
- package/scripts/feedback-aggregate.js +21 -2
- package/scripts/feedback-loop.js +87 -5
- package/scripts/feedback-quality.js +9 -0
- package/scripts/file-ledger-lock.js +4 -1
- package/scripts/financial-control-plane.js +41 -1
- package/scripts/find-dormant-requires.js +118 -0
- package/scripts/fs-utils.js +84 -8
- package/scripts/gates-engine.js +826 -68
- package/scripts/generate-case-study-outreach.js +24 -15
- package/scripts/git-at-scale.js +628 -0
- package/scripts/governance-conflict-audit.js +1650 -0
- package/scripts/governance-difficulty-curriculum.js +328 -0
- package/scripts/graphrag-retrieval.js +275 -0
- package/scripts/gurobi-optimizer.js +324 -0
- package/scripts/gurobi_optimizer.py +485 -0
- package/scripts/harness-selector.js +82 -1
- package/scripts/hidden-entry-points.js +284 -0
- package/scripts/human-escalation.js +199 -1
- package/scripts/hybrid-feedback-context.js +152 -19
- package/scripts/intent-governed-execution.js +602 -0
- package/scripts/intervention-policy.js +123 -20
- package/scripts/jit-harness-compose.js +628 -0
- package/scripts/jsonl-watcher.js +10 -0
- package/scripts/lesson-embedding-index.js +95 -12
- package/scripts/lesson-retrieval.js +105 -19
- package/scripts/local-model-profile.js +19 -2
- package/scripts/mailer/resend-mailer.js +1 -1
- package/scripts/matryoshka-embedding.js +235 -0
- package/scripts/mcp-oauth.js +42 -4
- package/scripts/mcp-session-handles.js +1016 -0
- package/scripts/mcp-wiring-doctor.js +314 -0
- package/scripts/memory-firewall.js +115 -2
- package/scripts/memory-scope-readiness.js +299 -0
- package/scripts/memory-vs-rag-route.js +161 -0
- package/scripts/model-tier-router.js +148 -21
- package/scripts/nvidia-specdecode-al-doctor.js +536 -0
- package/scripts/openui-catalog-compose-honesty.js +593 -0
- package/scripts/operational-integrity.js +19 -1
- package/scripts/override-audit.js +213 -0
- package/scripts/package-manager-honesty-doctor.js +458 -0
- package/scripts/pr-manager.js +63 -1
- package/scripts/prove-herdr-adapter.js +52 -0
- package/scripts/prove-memory-pyramid-and-symbolic-canvas.js +95 -0
- package/scripts/prove-workos.js +73 -0
- package/scripts/provider-attestation-conformance.js +192 -0
- package/scripts/provider-receipt-contract.js +136 -0
- package/scripts/qwen38-max-cost-optimizer.js +401 -0
- package/scripts/radware-threat-defense.js +280 -0
- package/scripts/rag-embedding-identity.js +221 -0
- package/scripts/rag-precision-guardrails.js +112 -2
- package/scripts/remote-feedback-capture.js +159 -0
- package/scripts/research-agent-harness.js +256 -0
- package/scripts/rsi-safety-hillclimb.js +200 -0
- package/scripts/rule-sprawl.js +188 -0
- package/scripts/schedule-manager.js +147 -0
- package/scripts/self-heal.js +8 -0
- package/scripts/session-lease.js +415 -0
- package/scripts/simatree-data-governance.js +347 -0
- package/scripts/slo-alert-engine.js +172 -7
- package/scripts/solver-parity.js +539 -0
- package/scripts/stealth-memory-injection-gate.js +333 -0
- package/scripts/switchyard-router.js +366 -0
- package/scripts/telemetry-analytics.js +84 -27
- package/scripts/temporal-decay-weighting.js +138 -0
- package/scripts/test-all.js +165 -0
- package/scripts/token-savings.js +42 -0
- package/scripts/token-shunt-honesty.js +502 -0
- package/scripts/tool-kpi-tracker.js +108 -5
- package/scripts/tool-registry.js +193 -5
- package/scripts/typesafe-typed-questions.js +813 -0
- package/scripts/universal-claim-evaluator.js +14 -2
- package/scripts/vector-store.js +279 -9
- package/scripts/workflow-notebook.js +391 -0
- package/scripts/workflow-sentinel.js +111 -12
- package/scripts/workos-production-guard.js +260 -0
- package/scripts/workspace-search-route.js +515 -0
- package/server.json +2 -2
- package/src/agent-identity-boundary.js +76 -0
- package/src/agent-retrieval-cache.js +155 -0
- package/src/alert-noise-ledger.js +502 -0
- package/src/api/server.js +724 -153
- package/src/git-fast-cache.js +220 -0
- package/src/git-wal-sync.js +156 -0
- package/src/hash-anchored-edit.js +82 -0
- package/src/hermes-platform-protocol.js +475 -0
- package/src/hermes-sync-plane.js +241 -0
- package/src/index.js +30 -1
- package/src/iso42001-compliance-guard.js +97 -0
- package/src/latency-budget.js +244 -0
- package/src/mcp-writeguard.js +316 -0
- package/src/miminions-adapter.js +106 -0
- package/src/pipeline-compass.js +104 -0
- package/src/ppl-alert-pipeline.js +284 -0
- package/src/rendezvous-router.js +90 -0
- package/src/security-questionnaire.js +195 -0
|
@@ -0,0 +1,719 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Broker-signed execution receipts.
|
|
6
|
+
*
|
|
7
|
+
* Design (ThumbGate side of the aigate split):
|
|
8
|
+
* - A credential-holding broker outside the agent trust boundary signs the
|
|
9
|
+
* receipt with an Ed25519 key the agent cannot reach.
|
|
10
|
+
* - ThumbGate verifies the signature, rejects agent-minted "proof", and can
|
|
11
|
+
* require a valid receipt before high-risk provider side effects.
|
|
12
|
+
* - ThumbGate does NOT store third-party provider credentials.
|
|
13
|
+
*
|
|
14
|
+
* Modes (THUMBGATE_BROKER_RECEIPT_MODE):
|
|
15
|
+
* - off — gate is a no-op
|
|
16
|
+
* - verify — if a receipt is attached, it must verify (default)
|
|
17
|
+
* - enforce — high-risk provider actions require a valid receipt
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const crypto = require('node:crypto');
|
|
21
|
+
const fs = require('node:fs');
|
|
22
|
+
const path = require('node:path');
|
|
23
|
+
const { getFeedbackPaths } = require('./feedback-paths');
|
|
24
|
+
const { validateToolContract } = require('./tool-contract-validator');
|
|
25
|
+
|
|
26
|
+
const SCHEMA_VERSION = 'broker-execution-receipt-v1';
|
|
27
|
+
const SCHEMA_PATH = path.join(__dirname, '..', 'config', 'schemas', 'broker-execution-receipt.schema.json');
|
|
28
|
+
// Agora-style honesty: a receipt records what the path produced, not the world.
|
|
29
|
+
const RECEIPT_PROOF_BOUNDARY = Object.freeze({
|
|
30
|
+
proves: [
|
|
31
|
+
'A credential-holding broker signed this decision',
|
|
32
|
+
'The bound principal, target, decision, and idempotency key',
|
|
33
|
+
'Hash-chain linkage when previousReceiptHash is present',
|
|
34
|
+
],
|
|
35
|
+
doesNotProve: [
|
|
36
|
+
'Independent world-state outcome outside the broker',
|
|
37
|
+
'That a public listing or health page is invocable',
|
|
38
|
+
'That a denied or unexecuted action ran',
|
|
39
|
+
'That a signed deny decision is an execution receipt',
|
|
40
|
+
],
|
|
41
|
+
});
|
|
42
|
+
const LEDGER_FILE = 'broker-execution-receipts.jsonl';
|
|
43
|
+
const PUBLIC_KEYS_FILE = 'broker-public-keys.json';
|
|
44
|
+
|
|
45
|
+
const BROKER_RECEIPT_SCHEMA = JSON.parse(fs.readFileSync(SCHEMA_PATH, 'utf8'));
|
|
46
|
+
|
|
47
|
+
// Provider-credentialed / external side-effect surfaces that must not run on
|
|
48
|
+
// agent-local prose alone when mode=enforce.
|
|
49
|
+
const HIGH_RISK_PROVIDER_PATTERNS = [
|
|
50
|
+
/\b(?:stripe|paypal|plaid|twilio|sendgrid|mailgun|postmark)\b/i,
|
|
51
|
+
/\b(?:openai|anthropic|openrouter|baseten)\b.{0,40}\b(?:key|token|billing|charge)\b/i,
|
|
52
|
+
/\bprovider[_ -]?credential/i,
|
|
53
|
+
/\bcredential[_ -]?vault/i,
|
|
54
|
+
/\b(?:send|dispatch)\s+(?:email|sms|wire|payout)\b/i,
|
|
55
|
+
/\b(?:create|capture|confirm)\s+(?:payment|charge|invoice|transfer)\b/i,
|
|
56
|
+
/\bbroker[_ -]?signed/i,
|
|
57
|
+
/\bprovider[_ -]?side[_ -]?effect\b/i,
|
|
58
|
+
/\bproviderEventId\b/i,
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
const HIGH_RISK_TOOL_NAMES = new Set([
|
|
62
|
+
'browser_click',
|
|
63
|
+
'browser_type',
|
|
64
|
+
'computer',
|
|
65
|
+
'computer_use',
|
|
66
|
+
'mcp__stripe',
|
|
67
|
+
'send_email',
|
|
68
|
+
'send_outreach',
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
function getLedgerPath(options = {}) {
|
|
72
|
+
if (options.ledgerPath) return path.resolve(options.ledgerPath);
|
|
73
|
+
return path.join(getFeedbackPaths(options).FEEDBACK_DIR, LEDGER_FILE);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function getPublicKeysPath(options = {}) {
|
|
77
|
+
if (options.publicKeysPath) return path.resolve(options.publicKeysPath);
|
|
78
|
+
return path.join(getFeedbackPaths(options).FEEDBACK_DIR, PUBLIC_KEYS_FILE);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function ensureDirFor(filePath) {
|
|
82
|
+
try {
|
|
83
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
84
|
+
} catch {
|
|
85
|
+
// write surfaces real errors
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function cleanString(value, max = 500) {
|
|
90
|
+
if (value === null || value === undefined) return '';
|
|
91
|
+
const text = String(value).trim();
|
|
92
|
+
if (!text) return '';
|
|
93
|
+
return text.length > max ? text.slice(0, max) : text;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function stableStringify(value) {
|
|
97
|
+
if (value === null || typeof value !== 'object') {
|
|
98
|
+
return JSON.stringify(value);
|
|
99
|
+
}
|
|
100
|
+
if (Array.isArray(value)) {
|
|
101
|
+
return `[${value.map((entry) => stableStringify(entry)).join(',')}]`;
|
|
102
|
+
}
|
|
103
|
+
const keys = Object.keys(value).sort((left, right) => left.localeCompare(right));
|
|
104
|
+
const parts = keys.map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`);
|
|
105
|
+
return `{${parts.join(',')}}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function sha256Hex(input) {
|
|
109
|
+
return crypto.createHash('sha256').update(String(input), 'utf8').digest('hex');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function publicKeyIdFromPem(publicKeyPem) {
|
|
113
|
+
return sha256Hex(String(publicKeyPem || '').replace(/\s+/g, '')).slice(0, 16);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function generateBrokerKeyPair() {
|
|
117
|
+
const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519');
|
|
118
|
+
const publicKeyPem = publicKey.export({ type: 'spki', format: 'pem' });
|
|
119
|
+
const privateKeyPem = privateKey.export({ type: 'pkcs8', format: 'pem' });
|
|
120
|
+
return {
|
|
121
|
+
publicKeyPem,
|
|
122
|
+
privateKeyPem,
|
|
123
|
+
publicKeyId: publicKeyIdFromPem(publicKeyPem),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Fields that are cryptographically bound (signed). Signature + receiptHash
|
|
129
|
+
* are computed after this object is frozen.
|
|
130
|
+
*/
|
|
131
|
+
function buildSignableBody(input = {}) {
|
|
132
|
+
const principal = input.principal && typeof input.principal === 'object'
|
|
133
|
+
? input.principal
|
|
134
|
+
: {};
|
|
135
|
+
const target = input.target && typeof input.target === 'object'
|
|
136
|
+
? input.target
|
|
137
|
+
: {};
|
|
138
|
+
const broker = input.broker && typeof input.broker === 'object'
|
|
139
|
+
? input.broker
|
|
140
|
+
: {};
|
|
141
|
+
|
|
142
|
+
const body = {
|
|
143
|
+
schemaVersion: SCHEMA_VERSION,
|
|
144
|
+
receiptId: cleanString(input.receiptId) || crypto.randomUUID(),
|
|
145
|
+
principal: {
|
|
146
|
+
id: cleanString(principal.id, 200),
|
|
147
|
+
kind: cleanString(principal.kind, 40) || 'agent',
|
|
148
|
+
},
|
|
149
|
+
target: {
|
|
150
|
+
provider: cleanString(target.provider, 120),
|
|
151
|
+
resource: cleanString(target.resource, 500) || undefined,
|
|
152
|
+
action: cleanString(target.action, 120),
|
|
153
|
+
},
|
|
154
|
+
decision: cleanString(input.decision, 40) || 'execute',
|
|
155
|
+
idempotencyKey: cleanString(input.idempotencyKey, 300),
|
|
156
|
+
providerEventId: input.providerEventId === null || input.providerEventId === undefined
|
|
157
|
+
? null
|
|
158
|
+
: cleanString(input.providerEventId, 300),
|
|
159
|
+
issuedAt: cleanString(input.issuedAt) || new Date().toISOString(),
|
|
160
|
+
broker: {
|
|
161
|
+
id: cleanString(broker.id, 200),
|
|
162
|
+
kind: 'broker',
|
|
163
|
+
},
|
|
164
|
+
previousReceiptHash: input.previousReceiptHash
|
|
165
|
+
? cleanString(input.previousReceiptHash, 64)
|
|
166
|
+
: null,
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
if (!body.target.resource) {
|
|
170
|
+
delete body.target.resource;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Metadata is signed when present so agents cannot attach extra claims post-hoc.
|
|
174
|
+
if (input.metadata && typeof input.metadata === 'object' && !Array.isArray(input.metadata)) {
|
|
175
|
+
body.metadata = input.metadata;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return body;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function computePayloadHash(signableBody) {
|
|
182
|
+
return sha256Hex(stableStringify(signableBody));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Issue a broker-signed receipt. Only call from a host that holds the broker
|
|
187
|
+
* private key (never from agent-controlled tool args alone).
|
|
188
|
+
*/
|
|
189
|
+
function issueBrokerReceipt(input = {}, options = {}) {
|
|
190
|
+
const privateKeyPem = cleanString(options.privateKeyPem || process.env.THUMBGATE_BROKER_SIGNING_KEY);
|
|
191
|
+
if (!privateKeyPem) {
|
|
192
|
+
const error = new Error('Broker signing key is not configured on the host');
|
|
193
|
+
error.code = 'THUMBGATE_BROKER_SIGNING_KEY_MISSING';
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const publicKeyPem = cleanString(options.publicKeyPem || process.env.THUMBGATE_BROKER_PUBLIC_KEY);
|
|
198
|
+
if (!publicKeyPem) {
|
|
199
|
+
const error = new Error('Broker public key is required to issue a receipt');
|
|
200
|
+
error.code = 'THUMBGATE_BROKER_PUBLIC_KEY_MISSING';
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const signable = buildSignableBody({
|
|
205
|
+
...input,
|
|
206
|
+
broker: {
|
|
207
|
+
id: cleanString(input.broker?.id || options.brokerId || 'thumbgate-dev-broker', 200),
|
|
208
|
+
kind: 'broker',
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
if (!signable.principal.id) {
|
|
213
|
+
const error = new Error('principal.id is required');
|
|
214
|
+
error.code = 'THUMBGATE_BROKER_RECEIPT_INVALID';
|
|
215
|
+
throw error;
|
|
216
|
+
}
|
|
217
|
+
if (!signable.target.provider || !signable.target.action) {
|
|
218
|
+
const error = new Error('target.provider and target.action are required');
|
|
219
|
+
error.code = 'THUMBGATE_BROKER_RECEIPT_INVALID';
|
|
220
|
+
throw error;
|
|
221
|
+
}
|
|
222
|
+
if (!signable.idempotencyKey) {
|
|
223
|
+
const error = new Error('idempotencyKey is required');
|
|
224
|
+
error.code = 'THUMBGATE_BROKER_RECEIPT_INVALID';
|
|
225
|
+
throw error;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const payloadHash = computePayloadHash(signable);
|
|
229
|
+
const privateKey = crypto.createPrivateKey(privateKeyPem);
|
|
230
|
+
const signatureValue = crypto.sign(null, Buffer.from(payloadHash, 'utf8'), privateKey)
|
|
231
|
+
.toString('base64');
|
|
232
|
+
|
|
233
|
+
const receipt = {
|
|
234
|
+
...signable,
|
|
235
|
+
payloadHash,
|
|
236
|
+
signature: {
|
|
237
|
+
alg: 'ed25519',
|
|
238
|
+
publicKeyId: cleanString(options.publicKeyId) || publicKeyIdFromPem(publicKeyPem),
|
|
239
|
+
value: signatureValue,
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
receipt.receiptHash = sha256Hex(stableStringify({
|
|
244
|
+
...receipt,
|
|
245
|
+
receiptHash: undefined,
|
|
246
|
+
}));
|
|
247
|
+
|
|
248
|
+
const validation = validateToolContract(BROKER_RECEIPT_SCHEMA, receipt);
|
|
249
|
+
if (!validation.valid) {
|
|
250
|
+
const error = new Error(`Invalid broker receipt: ${validation.errors.join('; ')}`);
|
|
251
|
+
error.code = 'THUMBGATE_BROKER_RECEIPT_INVALID';
|
|
252
|
+
error.details = validation.errors;
|
|
253
|
+
throw error;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return receipt;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function loadTrustedPublicKeys(options = {}) {
|
|
260
|
+
if (Array.isArray(options.trustedPublicKeys) && options.trustedPublicKeys.length > 0) {
|
|
261
|
+
return options.trustedPublicKeys.map((entry) => ({
|
|
262
|
+
publicKeyId: cleanString(entry.publicKeyId) || publicKeyIdFromPem(entry.publicKeyPem),
|
|
263
|
+
publicKeyPem: cleanString(entry.publicKeyPem),
|
|
264
|
+
brokerId: cleanString(entry.brokerId || ''),
|
|
265
|
+
})).filter((entry) => entry.publicKeyPem);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const fromEnv = cleanString(process.env.THUMBGATE_BROKER_PUBLIC_KEYS_JSON);
|
|
269
|
+
if (fromEnv) {
|
|
270
|
+
try {
|
|
271
|
+
const parsed = JSON.parse(fromEnv);
|
|
272
|
+
if (Array.isArray(parsed)) {
|
|
273
|
+
return loadTrustedPublicKeys({ trustedPublicKeys: parsed });
|
|
274
|
+
}
|
|
275
|
+
} catch {
|
|
276
|
+
// fall through
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const singlePem = cleanString(options.publicKeyPem || process.env.THUMBGATE_BROKER_PUBLIC_KEY);
|
|
281
|
+
if (singlePem) {
|
|
282
|
+
return [{
|
|
283
|
+
publicKeyId: publicKeyIdFromPem(singlePem),
|
|
284
|
+
publicKeyPem: singlePem,
|
|
285
|
+
brokerId: '',
|
|
286
|
+
}];
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
const raw = fs.readFileSync(getPublicKeysPath(options), 'utf8');
|
|
291
|
+
const parsed = JSON.parse(raw);
|
|
292
|
+
if (Array.isArray(parsed)) {
|
|
293
|
+
return loadTrustedPublicKeys({ trustedPublicKeys: parsed });
|
|
294
|
+
}
|
|
295
|
+
if (parsed && Array.isArray(parsed.keys)) {
|
|
296
|
+
return loadTrustedPublicKeys({ trustedPublicKeys: parsed.keys });
|
|
297
|
+
}
|
|
298
|
+
} catch {
|
|
299
|
+
// no keys on disk
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return [];
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function resolvePublicKeyPem(receipt, options = {}) {
|
|
306
|
+
const trusted = loadTrustedPublicKeys(options);
|
|
307
|
+
const keyId = cleanString(receipt?.signature?.publicKeyId);
|
|
308
|
+
if (keyId) {
|
|
309
|
+
const match = trusted.find((entry) => entry.publicKeyId === keyId);
|
|
310
|
+
if (match) return match.publicKeyPem;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (options.publicKeyPem) return cleanString(options.publicKeyPem);
|
|
314
|
+
if (trusted.length === 1) return trusted[0].publicKeyPem;
|
|
315
|
+
return '';
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function verifyBrokerReceipt(receipt, options = {}) {
|
|
319
|
+
const reasons = [];
|
|
320
|
+
if (!receipt || typeof receipt !== 'object') {
|
|
321
|
+
return { valid: false, reasons: ['receipt_missing'] };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const validation = validateToolContract(BROKER_RECEIPT_SCHEMA, receipt);
|
|
325
|
+
if (!validation.valid) {
|
|
326
|
+
return {
|
|
327
|
+
valid: false,
|
|
328
|
+
reasons: validation.errors.map((error) => `schema:${error}`),
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (receipt.broker?.kind !== 'broker') {
|
|
333
|
+
reasons.push('broker_kind_not_broker');
|
|
334
|
+
}
|
|
335
|
+
if (receipt.broker?.id && /agent/i.test(String(receipt.broker.id)) && options.rejectAgentBrokerId !== false) {
|
|
336
|
+
// Soft signal only when id contains "agent" without a trusted key match later.
|
|
337
|
+
reasons.push('broker_id_looks_like_agent');
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const signable = buildSignableBody(receipt);
|
|
341
|
+
// Preserve issuedAt/receiptId from receipt exactly for hash compare
|
|
342
|
+
signable.receiptId = receipt.receiptId;
|
|
343
|
+
signable.issuedAt = receipt.issuedAt;
|
|
344
|
+
signable.previousReceiptHash = receipt.previousReceiptHash || null;
|
|
345
|
+
if (receipt.target?.resource) {
|
|
346
|
+
signable.target.resource = receipt.target.resource;
|
|
347
|
+
} else {
|
|
348
|
+
delete signable.target.resource;
|
|
349
|
+
}
|
|
350
|
+
if (receipt.metadata && typeof receipt.metadata === 'object') {
|
|
351
|
+
signable.metadata = receipt.metadata;
|
|
352
|
+
} else {
|
|
353
|
+
delete signable.metadata;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const expectedPayloadHash = computePayloadHash(signable);
|
|
357
|
+
if (expectedPayloadHash !== receipt.payloadHash) {
|
|
358
|
+
reasons.push('payload_hash_mismatch');
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const publicKeyPem = resolvePublicKeyPem(receipt, options);
|
|
362
|
+
if (!publicKeyPem) {
|
|
363
|
+
reasons.push('trusted_public_key_missing');
|
|
364
|
+
} else {
|
|
365
|
+
try {
|
|
366
|
+
const publicKey = crypto.createPublicKey(publicKeyPem);
|
|
367
|
+
const ok = crypto.verify(
|
|
368
|
+
null,
|
|
369
|
+
Buffer.from(receipt.payloadHash, 'utf8'),
|
|
370
|
+
publicKey,
|
|
371
|
+
Buffer.from(receipt.signature.value, 'base64'),
|
|
372
|
+
);
|
|
373
|
+
if (!ok) {
|
|
374
|
+
reasons.push('signature_invalid');
|
|
375
|
+
} else {
|
|
376
|
+
// Valid signature clears the soft agent-id heuristic.
|
|
377
|
+
const idx = reasons.indexOf('broker_id_looks_like_agent');
|
|
378
|
+
if (idx >= 0) reasons.splice(idx, 1);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const expectedKeyId = publicKeyIdFromPem(publicKeyPem);
|
|
382
|
+
if (receipt.signature.publicKeyId !== expectedKeyId && options.requireExactKeyId !== false) {
|
|
383
|
+
const trusted = loadTrustedPublicKeys(options);
|
|
384
|
+
const known = trusted.some((entry) => entry.publicKeyId === receipt.signature.publicKeyId);
|
|
385
|
+
if (!known && trusted.length > 0) {
|
|
386
|
+
reasons.push('public_key_id_untrusted');
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
} catch {
|
|
390
|
+
reasons.push('public_key_unusable');
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const expectedReceiptHash = sha256Hex(stableStringify({
|
|
395
|
+
...receipt,
|
|
396
|
+
receiptHash: undefined,
|
|
397
|
+
}));
|
|
398
|
+
if (expectedReceiptHash !== receipt.receiptHash) {
|
|
399
|
+
reasons.push('receipt_hash_mismatch');
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Agent-local rehash is not broker proof: principal may be agent, but broker
|
|
403
|
+
// must not be agent-kind (already enforced by schema const).
|
|
404
|
+
if (options.rejectAgentSelfSign !== false && receipt.signature?.publicKeyId === 'agent') {
|
|
405
|
+
reasons.push('agent_self_sign_forbidden');
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return {
|
|
409
|
+
valid: reasons.length === 0,
|
|
410
|
+
reasons,
|
|
411
|
+
payloadHash: receipt.payloadHash,
|
|
412
|
+
receiptHash: receipt.receiptHash,
|
|
413
|
+
brokerId: receipt.broker?.id || null,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function appendReceiptToLedger(receipt, options = {}) {
|
|
418
|
+
const verification = verifyBrokerReceipt(receipt, options);
|
|
419
|
+
if (!verification.valid) {
|
|
420
|
+
const error = new Error(`Refusing to ledger invalid receipt: ${verification.reasons.join(', ')}`);
|
|
421
|
+
error.code = 'THUMBGATE_BROKER_RECEIPT_INVALID';
|
|
422
|
+
error.details = verification.reasons;
|
|
423
|
+
throw error;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const ledgerPath = getLedgerPath(options);
|
|
427
|
+
ensureDirFor(ledgerPath);
|
|
428
|
+
const existing = readReceiptLedger(options);
|
|
429
|
+
if (existing.length > 0) {
|
|
430
|
+
const head = existing[existing.length - 1];
|
|
431
|
+
// Non-genesis receipts must chain to the current head (no open forks).
|
|
432
|
+
if (!receipt.previousReceiptHash) {
|
|
433
|
+
const error = new Error('previousReceiptHash is required when the ledger is non-empty');
|
|
434
|
+
error.code = 'THUMBGATE_BROKER_CHAIN_REQUIRED';
|
|
435
|
+
throw error;
|
|
436
|
+
}
|
|
437
|
+
if (receipt.previousReceiptHash !== head.receiptHash) {
|
|
438
|
+
const error = new Error('previousReceiptHash does not match ledger head');
|
|
439
|
+
error.code = 'THUMBGATE_BROKER_CHAIN_BREAK';
|
|
440
|
+
throw error;
|
|
441
|
+
}
|
|
442
|
+
} else if (receipt.previousReceiptHash) {
|
|
443
|
+
const error = new Error('genesis receipt must not set previousReceiptHash');
|
|
444
|
+
error.code = 'THUMBGATE_BROKER_CHAIN_BREAK';
|
|
445
|
+
throw error;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
fs.appendFileSync(ledgerPath, `${JSON.stringify(receipt)}\n`, 'utf8');
|
|
449
|
+
return { recorded: true, receiptHash: receipt.receiptHash, count: existing.length + 1 };
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function readReceiptLedger(options = {}) {
|
|
453
|
+
const ledgerPath = getLedgerPath(options);
|
|
454
|
+
let raw;
|
|
455
|
+
try {
|
|
456
|
+
raw = fs.readFileSync(ledgerPath, 'utf8');
|
|
457
|
+
} catch {
|
|
458
|
+
return [];
|
|
459
|
+
}
|
|
460
|
+
const rows = [];
|
|
461
|
+
for (const line of raw.split('\n')) {
|
|
462
|
+
const trimmed = line.trim();
|
|
463
|
+
if (!trimmed) continue;
|
|
464
|
+
try {
|
|
465
|
+
rows.push(JSON.parse(trimmed));
|
|
466
|
+
} catch {
|
|
467
|
+
// skip corrupt line
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return rows;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function reconcileReceiptChain(options = {}) {
|
|
474
|
+
const rows = readReceiptLedger(options);
|
|
475
|
+
const issues = [];
|
|
476
|
+
let previousHash = null;
|
|
477
|
+
|
|
478
|
+
for (let index = 0; index < rows.length; index += 1) {
|
|
479
|
+
const receipt = rows[index];
|
|
480
|
+
const verification = verifyBrokerReceipt(receipt, options);
|
|
481
|
+
if (!verification.valid) {
|
|
482
|
+
issues.push({ index, receiptId: receipt.receiptId, reasons: verification.reasons });
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
if (index === 0) {
|
|
486
|
+
if (receipt.previousReceiptHash) {
|
|
487
|
+
issues.push({ index, receiptId: receipt.receiptId, reasons: ['unexpected_previous_hash_on_genesis'] });
|
|
488
|
+
}
|
|
489
|
+
} else if (receipt.previousReceiptHash !== previousHash) {
|
|
490
|
+
issues.push({ index, receiptId: receipt.receiptId, reasons: ['chain_break'] });
|
|
491
|
+
}
|
|
492
|
+
previousHash = receipt.receiptHash;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return {
|
|
496
|
+
ok: issues.length === 0,
|
|
497
|
+
count: rows.length,
|
|
498
|
+
issues,
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function getReceiptMode(options = {}) {
|
|
503
|
+
const raw = cleanString(options.mode || process.env.THUMBGATE_BROKER_RECEIPT_MODE || 'verify').toLowerCase();
|
|
504
|
+
if (raw === 'off' || raw === '0' || raw === 'false') return 'off';
|
|
505
|
+
if (raw === 'enforce' || raw === 'required' || raw === 'strict') return 'enforce';
|
|
506
|
+
return 'verify';
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function extractReceiptFromToolInput(toolInput = {}) {
|
|
510
|
+
if (!toolInput || typeof toolInput !== 'object') return null;
|
|
511
|
+
if (toolInput.brokerReceipt && typeof toolInput.brokerReceipt === 'object') {
|
|
512
|
+
return toolInput.brokerReceipt;
|
|
513
|
+
}
|
|
514
|
+
if (toolInput.broker_execution_receipt && typeof toolInput.broker_execution_receipt === 'object') {
|
|
515
|
+
return toolInput.broker_execution_receipt;
|
|
516
|
+
}
|
|
517
|
+
if (toolInput.receipt && toolInput.receipt.schemaVersion === SCHEMA_VERSION) {
|
|
518
|
+
return toolInput.receipt;
|
|
519
|
+
}
|
|
520
|
+
return null;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function serializeToolSurface(toolName, toolInput) {
|
|
524
|
+
let serialized = '';
|
|
525
|
+
try {
|
|
526
|
+
serialized = JSON.stringify(toolInput || {});
|
|
527
|
+
} catch {
|
|
528
|
+
serialized = String(toolInput || '');
|
|
529
|
+
}
|
|
530
|
+
return `${toolName || ''} ${serialized}`;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function isHighRiskProviderAction(toolName = '', toolInput = {}) {
|
|
534
|
+
const name = String(toolName || '');
|
|
535
|
+
if (HIGH_RISK_TOOL_NAMES.has(name)) return true;
|
|
536
|
+
if (/^mcp__/.test(name) && /stripe|paypal|twilio|sendgrid|mailgun/i.test(name)) return true;
|
|
537
|
+
|
|
538
|
+
const surface = serializeToolSurface(toolName, toolInput);
|
|
539
|
+
if (toolInput && toolInput.requiresBrokerReceipt === true) return true;
|
|
540
|
+
if (toolInput && toolInput.providerCredentialed === true) return true;
|
|
541
|
+
return HIGH_RISK_PROVIDER_PATTERNS.some((pattern) => pattern.test(surface));
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Bind a receipt's target to the tool call being gated so a valid signature for
|
|
546
|
+
* an unrelated action cannot unlock a different side effect.
|
|
547
|
+
*/
|
|
548
|
+
function receiptBindsToAction(receipt, toolName, toolInput = {}) {
|
|
549
|
+
const reasons = [];
|
|
550
|
+
if (!receipt || typeof receipt !== 'object') {
|
|
551
|
+
return { bound: false, reasons: ['receipt_missing'] };
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const target = receipt.target || {};
|
|
555
|
+
const provider = cleanString(target.provider).toLowerCase();
|
|
556
|
+
const action = cleanString(target.action).toLowerCase();
|
|
557
|
+
// Exclude the receipt itself from the surface so a forged target cannot
|
|
558
|
+
// self-match by being embedded in tool_input.brokerReceipt.
|
|
559
|
+
const surfaceInput = (toolInput && typeof toolInput === 'object')
|
|
560
|
+
? { ...toolInput }
|
|
561
|
+
: toolInput;
|
|
562
|
+
if (surfaceInput && typeof surfaceInput === 'object') {
|
|
563
|
+
delete surfaceInput.brokerReceipt;
|
|
564
|
+
delete surfaceInput.broker_execution_receipt;
|
|
565
|
+
if (surfaceInput.receipt && surfaceInput.receipt.schemaVersion === SCHEMA_VERSION) {
|
|
566
|
+
delete surfaceInput.receipt;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
const surface = serializeToolSurface(toolName, surfaceInput).toLowerCase();
|
|
570
|
+
const tool = String(toolName || '').toLowerCase();
|
|
571
|
+
|
|
572
|
+
if (provider && !surface.includes(provider) && !tool.includes(provider)) {
|
|
573
|
+
reasons.push('target_provider_mismatch');
|
|
574
|
+
}
|
|
575
|
+
if (action) {
|
|
576
|
+
// Prefer whole action phrase; fall back to distinctive tokens (len>=4) so
|
|
577
|
+
// short verbs like "create" alone cannot bind a receipt to unrelated tools.
|
|
578
|
+
const phraseMatched = surface.includes(action) || tool.includes(action);
|
|
579
|
+
const actionTokens = action.split(/[^a-z0-9]+/).filter((token) => token.length >= 4);
|
|
580
|
+
const tokenMatched = actionTokens.length > 0
|
|
581
|
+
&& actionTokens.every((token) => surface.includes(token) || tool.includes(token));
|
|
582
|
+
if (!phraseMatched && !tokenMatched) {
|
|
583
|
+
reasons.push('target_action_mismatch');
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Optional explicit binding fields from the broker.
|
|
588
|
+
const expectedTool = cleanString(receipt.metadata?.toolName || receipt.metadata?.tool_name);
|
|
589
|
+
if (expectedTool && expectedTool.toLowerCase() !== tool) {
|
|
590
|
+
reasons.push('metadata_tool_mismatch');
|
|
591
|
+
}
|
|
592
|
+
const expectedIdempotency = cleanString(
|
|
593
|
+
toolInput.idempotencyKey || toolInput.idempotency_key || '',
|
|
594
|
+
);
|
|
595
|
+
if (expectedIdempotency && expectedIdempotency !== cleanString(receipt.idempotencyKey)) {
|
|
596
|
+
reasons.push('idempotency_key_mismatch');
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
return { bound: reasons.length === 0, reasons };
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Pre-tool gate evaluation for broker receipts.
|
|
604
|
+
* Returns null when the gate does not apply or allows; otherwise a deny/warn result.
|
|
605
|
+
*/
|
|
606
|
+
function evaluateBrokerReceiptGate(toolName, toolInput = {}, options = {}) {
|
|
607
|
+
const mode = getReceiptMode(options);
|
|
608
|
+
if (mode === 'off') return null;
|
|
609
|
+
|
|
610
|
+
const receipt = extractReceiptFromToolInput(toolInput);
|
|
611
|
+
const highRisk = isHighRiskProviderAction(toolName, toolInput);
|
|
612
|
+
const trustedKeys = loadTrustedPublicKeys(options);
|
|
613
|
+
|
|
614
|
+
if (receipt) {
|
|
615
|
+
const verification = verifyBrokerReceipt(receipt, options);
|
|
616
|
+
if (!verification.valid) {
|
|
617
|
+
return {
|
|
618
|
+
decision: 'deny',
|
|
619
|
+
gate: 'broker-execution-receipt',
|
|
620
|
+
message: `Broker execution receipt failed verification (${verification.reasons.join(', ')}). `
|
|
621
|
+
+ 'A receipt an agent can rewrite is not evidence; only a credential-holding broker may sign.',
|
|
622
|
+
severity: 'critical',
|
|
623
|
+
reasons: verification.reasons,
|
|
624
|
+
source: 'broker-execution-receipts',
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
const binding = receiptBindsToAction(receipt, toolName, toolInput);
|
|
628
|
+
if (!binding.bound) {
|
|
629
|
+
return {
|
|
630
|
+
decision: 'deny',
|
|
631
|
+
gate: 'broker-execution-receipt',
|
|
632
|
+
message: `Broker execution receipt does not bind to this action (${binding.reasons.join(', ')}). `
|
|
633
|
+
+ 'A signature for a different target cannot unlock this tool call.',
|
|
634
|
+
severity: 'critical',
|
|
635
|
+
reasons: binding.reasons,
|
|
636
|
+
source: 'broker-execution-receipts',
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
return null;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
if (mode === 'enforce' && highRisk) {
|
|
643
|
+
if (trustedKeys.length === 0 && options.allowEnforceWithoutKeys !== true) {
|
|
644
|
+
// Fail closed when enforce is requested for high-risk work without keys:
|
|
645
|
+
// operators must install broker public keys first.
|
|
646
|
+
return {
|
|
647
|
+
decision: 'deny',
|
|
648
|
+
gate: 'broker-execution-receipt',
|
|
649
|
+
message: 'High-risk provider action requires a broker-signed execution receipt, '
|
|
650
|
+
+ 'but no trusted broker public keys are configured on the host.',
|
|
651
|
+
severity: 'critical',
|
|
652
|
+
reasons: ['receipt_required', 'trusted_public_key_missing'],
|
|
653
|
+
source: 'broker-execution-receipts',
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
return {
|
|
657
|
+
decision: 'deny',
|
|
658
|
+
gate: 'broker-execution-receipt',
|
|
659
|
+
message: 'High-risk provider action blocked: attach a valid broker-signed execution receipt '
|
|
660
|
+
+ '(tool_input.brokerReceipt). Agents cannot self-sign proof.',
|
|
661
|
+
severity: 'critical',
|
|
662
|
+
reasons: ['receipt_required'],
|
|
663
|
+
source: 'broker-execution-receipts',
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
return null;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function runCli(argv = process.argv.slice(2), options = {}) {
|
|
671
|
+
const command = argv[0] || 'help';
|
|
672
|
+
if (command === 'reconcile') {
|
|
673
|
+
const result = reconcileReceiptChain(options);
|
|
674
|
+
console.log(JSON.stringify(result, null, 2));
|
|
675
|
+
return result.ok ? 0 : 1;
|
|
676
|
+
}
|
|
677
|
+
if (command === 'verify') {
|
|
678
|
+
const file = argv[1];
|
|
679
|
+
if (!file) {
|
|
680
|
+
console.error('Usage: broker-execution-receipts.js verify <receipt.json>');
|
|
681
|
+
return 2;
|
|
682
|
+
}
|
|
683
|
+
const receipt = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
684
|
+
const result = verifyBrokerReceipt(receipt, options);
|
|
685
|
+
console.log(JSON.stringify(result, null, 2));
|
|
686
|
+
return result.valid ? 0 : 1;
|
|
687
|
+
}
|
|
688
|
+
console.log('Usage: broker-execution-receipts.js <reconcile|verify> [...]');
|
|
689
|
+
return 2;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(__filename)) {
|
|
693
|
+
process.exitCode = runCli();
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
module.exports = {
|
|
697
|
+
SCHEMA_VERSION,
|
|
698
|
+
RECEIPT_PROOF_BOUNDARY,
|
|
699
|
+
BROKER_RECEIPT_SCHEMA,
|
|
700
|
+
HIGH_RISK_PROVIDER_PATTERNS,
|
|
701
|
+
appendReceiptToLedger,
|
|
702
|
+
buildSignableBody,
|
|
703
|
+
computePayloadHash,
|
|
704
|
+
evaluateBrokerReceiptGate,
|
|
705
|
+
extractReceiptFromToolInput,
|
|
706
|
+
generateBrokerKeyPair,
|
|
707
|
+
receiptBindsToAction,
|
|
708
|
+
getLedgerPath,
|
|
709
|
+
getReceiptMode,
|
|
710
|
+
isHighRiskProviderAction,
|
|
711
|
+
issueBrokerReceipt,
|
|
712
|
+
loadTrustedPublicKeys,
|
|
713
|
+
publicKeyIdFromPem,
|
|
714
|
+
readReceiptLedger,
|
|
715
|
+
reconcileReceiptChain,
|
|
716
|
+
runCli,
|
|
717
|
+
stableStringify,
|
|
718
|
+
verifyBrokerReceipt,
|
|
719
|
+
};
|