thumbgate 1.27.17 → 1.27.19
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/.claude-plugin/marketplace.json +85 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +33 -5
- package/adapters/chatgpt/openapi.yaml +1707 -0
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/codex/config.toml +2 -2
- package/adapters/mcp/server-stdio.js +1 -1
- package/adapters/opencode/opencode.json +1 -1
- package/bin/cli.js +63 -54
- package/bin/postinstall.js +23 -14
- package/config/post-deploy-marketing-pages.json +19 -0
- package/config/tessl-tiles.json +1 -3
- package/package.json +99 -229
- package/public/guide.html +0 -4
- package/public/index.html +28 -49
- package/public/learn.html +0 -8
- package/public/numbers.html +2 -2
- package/scripts/agent-readiness.js +0 -147
- package/scripts/bot-detection.js +61 -3
- package/scripts/cli-feedback.js +4 -14
- package/scripts/cli-schema.js +7 -0
- package/scripts/durability/step.js +3 -3
- package/scripts/feedback-loop.js +10 -32
- package/scripts/gates-engine.js +72 -1
- package/scripts/hook-runtime.js +2 -2
- package/scripts/license.js +10 -9
- package/scripts/llm-client.js +44 -260
- package/scripts/mcp-config.js +7 -1
- package/scripts/published-cli.js +0 -8
- package/scripts/seo-gsd.js +0 -5
- package/src/api/server.js +111 -35
- package/.claude/commands/dashboard.md +0 -15
- package/.claude/commands/thumbgate-blocked.md +0 -27
- package/.claude/commands/thumbgate-dashboard.md +0 -15
- package/.claude/commands/thumbgate-doctor.md +0 -30
- package/.claude/commands/thumbgate-guard.md +0 -36
- package/.claude/commands/thumbgate-protect.md +0 -30
- package/.claude/commands/thumbgate-rules.md +0 -30
- package/adapters/gcp/dfcx-webhook-gate.js +0 -295
- package/adapters/letta/README.md +0 -41
- package/adapters/letta/thumbgate-letta-adapter.js +0 -133
- package/adapters/policy-engine/ethicore-guardian-client.js +0 -68
- package/adapters/policy-engine/thumbgate-policy-engine-adapter.js +0 -260
- package/bench/observability-eval-suite.json +0 -26
- package/bin/dashboard-cli.js +0 -7
- package/commands/dashboard.md +0 -15
- package/commands/thumbgate-dashboard.md +0 -15
- package/config/builtin-lessons.json +0 -23
- package/public/about.html +0 -162
- package/public/agent-manager.html +0 -179
- package/public/agents-cost-savings.html +0 -153
- package/public/ai-malpractice-prevention.html +0 -818
- package/public/assets/brand/github-social-preview.png +0 -0
- package/public/assets/brand/thumbgate-icon-512.png +0 -0
- package/public/assets/brand/thumbgate-icon-pro-512.png +0 -0
- package/public/assets/brand/thumbgate-icon-team-512.png +0 -0
- package/public/assets/brand/thumbgate-logo-1200x360.png +0 -0
- package/public/assets/brand/thumbgate-logo-transparent.svg +0 -22
- package/public/assets/brand/thumbgate-mark-inline-v3.svg +0 -19
- package/public/assets/brand/thumbgate-mark-pro.svg +0 -23
- package/public/assets/brand/thumbgate-mark-team.svg +0 -26
- package/public/assets/brand/thumbgate-mark.svg +0 -21
- package/public/assets/brand/thumbgate-wordmark.svg +0 -20
- package/public/assets/claude-thumbgate-statusbar.svg +0 -8
- package/public/assets/codex-thumbgate-statusbar-test.svg +0 -9
- package/public/assets/legal-intake-control-flow.svg +0 -66
- package/public/brand/thumbgate-mark.svg +0 -19
- package/public/brand/thumbgate-og.svg +0 -16
- package/public/chatgpt-app.html +0 -330
- package/public/codex-enterprise.html +0 -123
- package/public/js/buyer-intent.js +0 -672
- package/public/pricing.html +0 -399
- package/scripts/action-receipts.js +0 -324
- package/scripts/activation-quickstart.js +0 -187
- package/scripts/agent-operations-planner.js +0 -621
- package/scripts/ai-component-inventory.js +0 -367
- package/scripts/async-eval-observability.js +0 -236
- package/scripts/audit.js +0 -65
- package/scripts/aws-blocks-guardrails.js +0 -228
- package/scripts/bot-detector.js +0 -50
- package/scripts/classifier-routing.js +0 -130
- package/scripts/dashboard-chat.js +0 -332
- package/scripts/feedback-aggregate.js +0 -281
- package/scripts/feedback-sanitizer.js +0 -105
- package/scripts/hob-pack.js +0 -591
- package/scripts/hook-stop-anti-claim.js +0 -301
- package/scripts/install-shim.js +0 -87
- package/scripts/mcp-oauth.js +0 -293
- package/scripts/noop-detect.js +0 -285
- package/scripts/omlx-smoke.js +0 -192
- package/scripts/parallel-workflow-orchestrator.js +0 -293
- package/scripts/plan-gate.js +0 -243
- package/scripts/plausible-domain-config.js +0 -99
- package/scripts/qa-scenario-planner.js +0 -136
- package/scripts/repeat-metric.js +0 -137
- package/scripts/secret-fixture-tokens.js +0 -61
- package/scripts/secret-redaction.js +0 -166
- package/scripts/self-harness-optimizer.js +0 -141
- package/scripts/self-healing-check.js +0 -193
- package/scripts/silent-failure-cluster.js +0 -531
- package/scripts/statusline-cache-read.js +0 -57
- package/scripts/sync-telemetry-from-prod.js +0 -374
- package/scripts/tool-contract-validator.js +0 -76
- package/scripts/trajectory-scorer.js +0 -63
- package/scripts/verify-marketing-pages-deployed.js +0 -195
- package/scripts/visitor-journey.js +0 -172
|
@@ -1,295 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// adapters/gcp/dfcx-webhook-gate.js
|
|
4
|
-
// -----------------------------------------------------------------------------
|
|
5
|
-
// ThumbGate Enterprise — Dialogflow CX fulfillment webhook guardrail.
|
|
6
|
-
//
|
|
7
|
-
// Routes a Dialogflow CX (DFCX) fulfillment request through ThumbGate's
|
|
8
|
-
// pre-action gate engine BEFORE the real fulfillment side-effect runs (DB/CRM/
|
|
9
|
-
// billing write). If a configured policy gate denies the action, or the action
|
|
10
|
-
// is a known same-session repeat, the side-effect is blocked and a safe DFCX
|
|
11
|
-
// WebhookResponse is returned instead of executing it.
|
|
12
|
-
//
|
|
13
|
-
// Design: ThumbGate is a *guard in front of* the customer's existing fulfillment
|
|
14
|
-
// function — it decides whether that function is allowed to run. It does not
|
|
15
|
-
// replace it, mutate Playbooks, or call any Google API itself.
|
|
16
|
-
//
|
|
17
|
-
// This enterprise adapter is also listed in package.json "files" because
|
|
18
|
-
// src/api/server.js loads it for the local enterprise Dialogflow dashboard routes.
|
|
19
|
-
// The same module can still be deployed as Cloud Run / Cloud Functions middleware.
|
|
20
|
-
// -----------------------------------------------------------------------------
|
|
21
|
-
|
|
22
|
-
const path = require('path');
|
|
23
|
-
|
|
24
|
-
const REPO_ROOT = path.join(__dirname, '..', '..');
|
|
25
|
-
const gates = require(path.join(REPO_ROOT, 'scripts', 'gates-engine'));
|
|
26
|
-
|
|
27
|
-
// Risk scorer is optional: it needs a trained model on disk. Degrade to null.
|
|
28
|
-
let riskScorer = null;
|
|
29
|
-
try {
|
|
30
|
-
riskScorer = require(path.join(REPO_ROOT, 'scripts', 'risk-scorer'));
|
|
31
|
-
} catch (_) {
|
|
32
|
-
riskScorer = null;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Deterministic stringify so the same parameter object always yields the same
|
|
36
|
-
// action id (used for same-session repeat detection).
|
|
37
|
-
function stableStringify(value) {
|
|
38
|
-
if (value === null || typeof value !== 'object') return JSON.stringify(value);
|
|
39
|
-
if (Array.isArray(value)) return '[' + value.map(stableStringify).join(',') + ']';
|
|
40
|
-
const keys = Object.keys(value).sort();
|
|
41
|
-
return '{' + keys.map((k) => JSON.stringify(k) + ':' + stableStringify(value[k])).join(',') + '}';
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Map a DFCX WebhookRequest into a ThumbGate (toolName, toolInput) action.
|
|
45
|
-
// DFCX fulfillment tag -> toolName ("dfcx:<tag>"); session parameters -> toolInput.
|
|
46
|
-
// Supports both camelCase (standard DFCX) and snake_case (legacy/internal) formatting.
|
|
47
|
-
function mapDfcxToAction(reqBody) {
|
|
48
|
-
const body = reqBody || {};
|
|
49
|
-
const fulfillmentInfo = body.fulfillmentInfo || body.fulfillment_info || {};
|
|
50
|
-
const tag = fulfillmentInfo.tag || 'unknown';
|
|
51
|
-
const sessionInfo = body.sessionInfo || body.session_info || {};
|
|
52
|
-
const params = sessionInfo.parameters || {};
|
|
53
|
-
const sessionId = sessionInfo.session || '';
|
|
54
|
-
return {
|
|
55
|
-
tag,
|
|
56
|
-
toolName: 'dfcx:' + tag,
|
|
57
|
-
toolInput: params,
|
|
58
|
-
sessionId,
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// A DFCX webhook is fully untrusted (internet-facing), unlike a local coding
|
|
63
|
-
// agent. These allowlists reject anything that could carry shell/path
|
|
64
|
-
// metacharacters before the action ever reaches the gate engine.
|
|
65
|
-
const SAFE_TOKEN = /^[A-Za-z0-9._\s-]{1,64}$/; // fulfillment tags, parameter names
|
|
66
|
-
const SAFE_VALUE = /^[^`\;|&<>]{0,2048}$/; // parameter string values (allow standard punctuation, block command injection)
|
|
67
|
-
|
|
68
|
-
// Evaluate whether a DFCX fulfillment should be allowed to execute.
|
|
69
|
-
// Returns { allowed, decision, gate, message, severity, repeat, risk, action }.
|
|
70
|
-
function evaluateDfcxFulfillment(reqBody, opts = {}) {
|
|
71
|
-
const raw = mapDfcxToAction(reqBody);
|
|
72
|
-
|
|
73
|
-
// 0) Validate the untrusted webhook input and rebuild a SAFE action inline,
|
|
74
|
-
// before any value reaches the gate engine. Block on any unsafe token/value
|
|
75
|
-
// so attacker-controlled input cannot reach a path/command sink downstream.
|
|
76
|
-
const blockedUnsafe = (reason) => ({
|
|
77
|
-
allowed: false,
|
|
78
|
-
decision: 'deny',
|
|
79
|
-
gate: 'dfcx-unsafe-input',
|
|
80
|
-
message: 'The request contained unsafe input and was blocked.',
|
|
81
|
-
severity: 'critical',
|
|
82
|
-
repeat: false,
|
|
83
|
-
risk: null,
|
|
84
|
-
action: { tag: String(raw.tag), toolName: 'dfcx:unsafe', toolInput: {}, sessionId: raw.sessionId },
|
|
85
|
-
reason,
|
|
86
|
-
});
|
|
87
|
-
const tag = String(raw.tag);
|
|
88
|
-
if (!SAFE_TOKEN.test(tag)) return blockedUnsafe('unsafe fulfillment tag');
|
|
89
|
-
const toolName = 'dfcx:' + tag;
|
|
90
|
-
const toolInput = {};
|
|
91
|
-
const rawParams = raw.toolInput && typeof raw.toolInput === 'object' ? raw.toolInput : {};
|
|
92
|
-
for (const key of Object.keys(rawParams)) {
|
|
93
|
-
if (!SAFE_TOKEN.test(key)) return blockedUnsafe('unsafe parameter name');
|
|
94
|
-
const value = rawParams[key];
|
|
95
|
-
if (typeof value === 'string') {
|
|
96
|
-
if (!SAFE_VALUE.test(value)) return blockedUnsafe('unsafe parameter value');
|
|
97
|
-
toolInput[key] = value;
|
|
98
|
-
} else if (typeof value === 'number' || typeof value === 'boolean' || value === null) {
|
|
99
|
-
toolInput[key] = value;
|
|
100
|
-
}
|
|
101
|
-
// non-scalar values are intentionally dropped (never forwarded downstream).
|
|
102
|
-
}
|
|
103
|
-
const action = { tag, toolName, toolInput, sessionId: raw.sessionId };
|
|
104
|
-
|
|
105
|
-
// 1) Configured policy gates. The pilot configures DFCX-relevant gates (e.g.
|
|
106
|
-
// "block dfcx:process-refund when amount > limit and not approved"). With no
|
|
107
|
-
// custom config this is simply a no-op (allow).
|
|
108
|
-
let gateResult = null;
|
|
109
|
-
try {
|
|
110
|
-
gateResult = gates.evaluateGates(toolName, toolInput, opts.configPath);
|
|
111
|
-
} catch (_) {
|
|
112
|
-
gateResult = null;
|
|
113
|
-
}
|
|
114
|
-
const denied = Boolean(gateResult && gateResult.decision === 'deny');
|
|
115
|
-
|
|
116
|
-
// 2) Same-session repeat detection — works with zero custom config.
|
|
117
|
-
const actionId = action.toolName + ':' + stableStringify(action.toolInput);
|
|
118
|
-
let repeat = false;
|
|
119
|
-
if (typeof gates.hasAction === 'function') {
|
|
120
|
-
try { repeat = Boolean(gates.hasAction(actionId)); } catch (_) { repeat = false; }
|
|
121
|
-
}
|
|
122
|
-
if (typeof gates.trackAction === 'function') {
|
|
123
|
-
try { gates.trackAction(actionId, { source: 'dfcx', tag: action.tag }); } catch (_) { /* non-fatal */ }
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// 3) Optional risk score (best-effort; null when no model is trained).
|
|
127
|
-
let risk = null;
|
|
128
|
-
if (riskScorer && typeof riskScorer.predictRisk === 'function') {
|
|
129
|
-
try {
|
|
130
|
-
const candidate = typeof riskScorer.buildRiskCandidate === 'function'
|
|
131
|
-
? riskScorer.buildRiskCandidate({ toolName: action.toolName, toolInput: action.toolInput })
|
|
132
|
-
: { toolName: action.toolName, toolInput: action.toolInput };
|
|
133
|
-
const r = riskScorer.predictRisk(candidate);
|
|
134
|
-
risk = typeof r === 'number' ? r : (r && typeof r.risk === 'number' ? r.risk : null);
|
|
135
|
-
} catch (_) {
|
|
136
|
-
risk = null;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const blockOnRepeat = opts.blockOnRepeat !== false && repeat;
|
|
141
|
-
const allowed = !denied && !blockOnRepeat;
|
|
142
|
-
|
|
143
|
-
return {
|
|
144
|
-
allowed,
|
|
145
|
-
decision: allowed ? 'allow' : 'deny',
|
|
146
|
-
gate: denied ? gateResult.gate : (blockOnRepeat ? 'dfcx-repeat-action' : null),
|
|
147
|
-
message: denied
|
|
148
|
-
? gateResult.message
|
|
149
|
-
: (blockOnRepeat ? 'This action was already attempted in this session and is blocked as a repeat.' : null),
|
|
150
|
-
severity: denied ? gateResult.severity : (blockOnRepeat ? 'high' : null),
|
|
151
|
-
repeat,
|
|
152
|
-
risk,
|
|
153
|
-
action,
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Build a DFCX WebhookResponse that safely halts the turn without side-effects.
|
|
158
|
-
// Supports both camelCase (standard DFCX) and snake_case (legacy/internal) formatting.
|
|
159
|
-
function buildBlockResponse(evaluation, opts = {}) {
|
|
160
|
-
const message = opts.blockedMessage
|
|
161
|
-
|| 'This request was held by a safety policy and was not completed. A team member may follow up.';
|
|
162
|
-
const payload = {
|
|
163
|
-
fulfillment_response: { messages: [{ text: { text: [message] } }] },
|
|
164
|
-
fulfillmentResponse: { messages: [{ text: { text: [message] } }] },
|
|
165
|
-
session_info: {
|
|
166
|
-
parameters: {
|
|
167
|
-
thumbgate_blocked: true,
|
|
168
|
-
thumbgate_gate: evaluation.gate || null,
|
|
169
|
-
thumbgate_severity: evaluation.severity || null,
|
|
170
|
-
},
|
|
171
|
-
},
|
|
172
|
-
sessionInfo: {
|
|
173
|
-
parameters: {
|
|
174
|
-
thumbgate_blocked: true,
|
|
175
|
-
thumbgate_gate: evaluation.gate || null,
|
|
176
|
-
thumbgate_severity: evaluation.severity || null,
|
|
177
|
-
},
|
|
178
|
-
},
|
|
179
|
-
};
|
|
180
|
-
return payload;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Annotate an allowed (passed-through) response so downstream flows can observe
|
|
184
|
-
// that ThumbGate evaluated and permitted the turn. Never throws on odd shapes.
|
|
185
|
-
// Populates both camelCase and snake_case variants to ensure compatibility.
|
|
186
|
-
function annotateAllowed(response, evaluation) {
|
|
187
|
-
const base = response && typeof response === 'object' ? response : {};
|
|
188
|
-
|
|
189
|
-
const sessionInfo = base.sessionInfo || base.session_info || {};
|
|
190
|
-
const params = sessionInfo.parameters && typeof sessionInfo.parameters === 'object' ? sessionInfo.parameters : {};
|
|
191
|
-
|
|
192
|
-
const updatedParams = Object.assign({}, params, {
|
|
193
|
-
thumbgate_blocked: false,
|
|
194
|
-
thumbgate_risk: evaluation.risk,
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
const updatedSessionInfo = Object.assign({}, sessionInfo, {
|
|
198
|
-
parameters: updatedParams,
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
const updated = Object.assign({}, base, {
|
|
202
|
-
session_info: updatedSessionInfo,
|
|
203
|
-
sessionInfo: updatedSessionInfo,
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
if (base.fulfillment_response) {
|
|
207
|
-
updated.fulfillmentResponse = base.fulfillment_response;
|
|
208
|
-
} else if (base.fulfillmentResponse) {
|
|
209
|
-
updated.fulfillment_response = base.fulfillmentResponse;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
return updated;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// Guard a DFCX webhook: run the gate; only invoke the real fulfillment when
|
|
216
|
-
// allowed. `fulfill(reqBody) -> WebhookResponse` is the customer's existing
|
|
217
|
-
// fulfillment function. Returns { blocked, response, evaluation }.
|
|
218
|
-
async function guardDfcxWebhook(reqBody, fulfill, opts = {}) {
|
|
219
|
-
const evaluation = evaluateDfcxFulfillment(reqBody, opts);
|
|
220
|
-
if (!evaluation.allowed) {
|
|
221
|
-
return { blocked: true, response: buildBlockResponse(evaluation, opts), evaluation };
|
|
222
|
-
}
|
|
223
|
-
const fulfilled = typeof fulfill === 'function'
|
|
224
|
-
? await fulfill(reqBody)
|
|
225
|
-
: { fulfillment_response: { messages: [] } };
|
|
226
|
-
return { blocked: false, response: annotateAllowed(fulfilled, evaluation), evaluation };
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// Reject bodies larger than this. A DFCX WebhookRequest is small (a few KB); an
|
|
230
|
-
// unbounded reader on an internet-facing endpoint is a memory-exhaustion vector.
|
|
231
|
-
const MAX_BODY_BYTES = 1024 * 1024; // 1 MiB
|
|
232
|
-
|
|
233
|
-
// Read a JSON request body from a Node IncomingMessage stream, with a hard size
|
|
234
|
-
// cap so a malicious/misconfigured caller cannot exhaust memory.
|
|
235
|
-
function readJsonBody(req) {
|
|
236
|
-
return new Promise((resolve, reject) => {
|
|
237
|
-
let raw = '';
|
|
238
|
-
let bytes = 0;
|
|
239
|
-
let aborted = false;
|
|
240
|
-
req.on('data', (chunk) => {
|
|
241
|
-
if (aborted) return;
|
|
242
|
-
bytes += chunk.length;
|
|
243
|
-
if (bytes > MAX_BODY_BYTES) {
|
|
244
|
-
aborted = true;
|
|
245
|
-
const err = new Error('request body exceeds ' + MAX_BODY_BYTES + ' bytes');
|
|
246
|
-
err.statusCode = 413;
|
|
247
|
-
try { req.destroy(); } catch (_) { /* ignore */ }
|
|
248
|
-
return reject(err);
|
|
249
|
-
}
|
|
250
|
-
raw += chunk;
|
|
251
|
-
});
|
|
252
|
-
req.on('end', () => {
|
|
253
|
-
if (aborted) return;
|
|
254
|
-
if (!raw) return resolve({});
|
|
255
|
-
try { resolve(JSON.parse(raw)); } catch (e) { reject(e); }
|
|
256
|
-
});
|
|
257
|
-
req.on('error', reject);
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// Plain Node HTTP handler for Cloud Run / Cloud Functions (no framework dep).
|
|
262
|
-
function createHttpHandler(fulfill, opts = {}) {
|
|
263
|
-
return async function handler(req, res) {
|
|
264
|
-
try {
|
|
265
|
-
const body = req && req.body && typeof req.body === 'object'
|
|
266
|
-
? req.body
|
|
267
|
-
: await readJsonBody(req);
|
|
268
|
-
const { response, evaluation } = await guardDfcxWebhook(body, fulfill, opts);
|
|
269
|
-
if (typeof opts.onDecision === 'function') {
|
|
270
|
-
try { opts.onDecision(evaluation); } catch (_) { /* observability must not break the turn */ }
|
|
271
|
-
}
|
|
272
|
-
res.statusCode = 200;
|
|
273
|
-
res.setHeader('content-type', 'application/json');
|
|
274
|
-
res.end(JSON.stringify(response));
|
|
275
|
-
} catch (err) {
|
|
276
|
-
// Log internally for operators; never leak error/stack details to the
|
|
277
|
-
// external caller (Dialogflow CX / the open internet).
|
|
278
|
-
try { console.error('thumbgate-dfcx-gate error:', err); } catch (_) { /* ignore */ }
|
|
279
|
-
res.statusCode = 500;
|
|
280
|
-
res.setHeader('content-type', 'application/json');
|
|
281
|
-
res.end(JSON.stringify({ error: 'thumbgate-dfcx-gate-failure' }));
|
|
282
|
-
}
|
|
283
|
-
};
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
module.exports = {
|
|
287
|
-
mapDfcxToAction,
|
|
288
|
-
evaluateDfcxFulfillment,
|
|
289
|
-
buildBlockResponse,
|
|
290
|
-
annotateAllowed,
|
|
291
|
-
guardDfcxWebhook,
|
|
292
|
-
createHttpHandler,
|
|
293
|
-
// exposed for tests
|
|
294
|
-
stableStringify,
|
|
295
|
-
};
|
package/adapters/letta/README.md
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
# Letta Adapter
|
|
2
|
-
|
|
3
|
-
Letta is a stateful-agent runtime with persistent memory, MCP tools, custom server tools, and client-side tool execution. ThumbGate should not compete with that memory surface. ThumbGate should sit underneath it as the deterministic local enforcement layer.
|
|
4
|
-
|
|
5
|
-
## Integration Pattern
|
|
6
|
-
|
|
7
|
-
Use `thumbgate-letta-adapter.js` to wrap Letta tool execution:
|
|
8
|
-
|
|
9
|
-
1. Letta agent proposes a tool call.
|
|
10
|
-
2. The wrapper normalizes the Letta event into ThumbGate's provider-action schema.
|
|
11
|
-
3. Your app calls ThumbGate's local gate-check transport.
|
|
12
|
-
4. If ThumbGate blocks or requires approval, the tool executor is never called.
|
|
13
|
-
5. If allowed, the original Letta tool executes normally.
|
|
14
|
-
|
|
15
|
-
This works for the two practical Letta surfaces:
|
|
16
|
-
|
|
17
|
-
- **MCP tools**: Letta forwards the tool call through the Letta server to an MCP server. Wrap the forwarding boundary or expose the downstream tool through a ThumbGate-guarded MCP server.
|
|
18
|
-
- **Client tools**: Letta asks your client application to execute a tool locally. Wrap the client tool handler with `createLettaToolGuard`.
|
|
19
|
-
|
|
20
|
-
## Example
|
|
21
|
-
|
|
22
|
-
```js
|
|
23
|
-
const { createLettaToolGuard } = require('./thumbgate-letta-adapter');
|
|
24
|
-
|
|
25
|
-
const guardedTool = createLettaToolGuard({
|
|
26
|
-
gateCheck: async (normalizedAction) => thumbgateGateCheck(normalizedAction),
|
|
27
|
-
executeTool: async (lettaEvent) => runOriginalTool(lettaEvent),
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
await guardedTool({
|
|
31
|
-
agentId: 'agent-123',
|
|
32
|
-
clientTool: {
|
|
33
|
-
name: 'shell',
|
|
34
|
-
input: { command: 'git push --force origin main' },
|
|
35
|
-
},
|
|
36
|
-
});
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
## Positioning
|
|
40
|
-
|
|
41
|
-
Letta helps agents remember and operate with state. ThumbGate helps teams decide what those agents are allowed to do next. The moat is adapter coverage: ThumbGate works under memory-first runtimes instead of asking buyers to replace them.
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
const { normalizeProviderAction } = require('../../scripts/provider-action-normalizer');
|
|
5
|
-
|
|
6
|
-
function asObject(value) {
|
|
7
|
-
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
function firstString(...values) {
|
|
11
|
-
for (const value of values) {
|
|
12
|
-
const text = String(value || '').trim();
|
|
13
|
-
if (text) return text;
|
|
14
|
-
}
|
|
15
|
-
return '';
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function extractLettaToolCall(input = {}) {
|
|
19
|
-
const event = asObject(input);
|
|
20
|
-
const toolCall = asObject(event.toolCall || event.tool_call || event.lettaToolCall);
|
|
21
|
-
const functionCall = asObject(toolCall.function || event.function);
|
|
22
|
-
const mcp = asObject(event.mcp || event.mcpToolCall);
|
|
23
|
-
const clientTool = asObject(event.clientTool || event.client_tool);
|
|
24
|
-
const params = asObject(event.params);
|
|
25
|
-
|
|
26
|
-
const name = firstString(
|
|
27
|
-
event.toolName,
|
|
28
|
-
event.name,
|
|
29
|
-
toolCall.name,
|
|
30
|
-
functionCall.name,
|
|
31
|
-
clientTool.name,
|
|
32
|
-
mcp.name,
|
|
33
|
-
params.name
|
|
34
|
-
);
|
|
35
|
-
const args = asObject(
|
|
36
|
-
event.arguments
|
|
37
|
-
|| event.args
|
|
38
|
-
|| event.input
|
|
39
|
-
|| toolCall.arguments
|
|
40
|
-
|| toolCall.input
|
|
41
|
-
|| functionCall.arguments
|
|
42
|
-
|| clientTool.arguments
|
|
43
|
-
|| clientTool.input
|
|
44
|
-
|| mcp.arguments
|
|
45
|
-
|| params.arguments
|
|
46
|
-
);
|
|
47
|
-
|
|
48
|
-
return {
|
|
49
|
-
id: firstString(event.id, event.toolCallId, event.tool_call_id, toolCall.id, clientTool.id),
|
|
50
|
-
name,
|
|
51
|
-
arguments: args,
|
|
52
|
-
server: firstString(event.mcpServer, mcp.server, params.server),
|
|
53
|
-
surface: firstString(event.surface, event.executionSurface, clientTool.name ? 'client-tool' : mcp.name || params.name ? 'mcp-tool' : 'server-tool'),
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function normalizeLettaAction(input = {}) {
|
|
58
|
-
const event = asObject(input);
|
|
59
|
-
const toolCall = extractLettaToolCall(event);
|
|
60
|
-
|
|
61
|
-
const normalized = normalizeProviderAction({
|
|
62
|
-
...event,
|
|
63
|
-
provider: 'letta',
|
|
64
|
-
toolCall: {
|
|
65
|
-
id: toolCall.id,
|
|
66
|
-
name: toolCall.name,
|
|
67
|
-
input: toolCall.arguments,
|
|
68
|
-
},
|
|
69
|
-
toolName: toolCall.name,
|
|
70
|
-
input: toolCall.arguments,
|
|
71
|
-
mcpServer: toolCall.server,
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
return {
|
|
75
|
-
...normalized,
|
|
76
|
-
provider: 'letta',
|
|
77
|
-
agentRuntime: 'letta',
|
|
78
|
-
letta: {
|
|
79
|
-
agentId: firstString(event.agentId, event.agent_id),
|
|
80
|
-
messageId: firstString(event.messageId, event.message_id),
|
|
81
|
-
surface: toolCall.surface,
|
|
82
|
-
toolCallId: toolCall.id,
|
|
83
|
-
},
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function normalizeGateDecision(decision = {}) {
|
|
88
|
-
const value = asObject(decision);
|
|
89
|
-
const raw = firstString(value.decision, value.mode, value.action, value.status).toLowerCase();
|
|
90
|
-
const blocked = value.allowed === false
|
|
91
|
-
|| value.accepted === false
|
|
92
|
-
|| value.blocked === true
|
|
93
|
-
|| ['block', 'deny', 'denied', 'reject', 'rejected'].includes(raw);
|
|
94
|
-
const approvalRequired = value.requiresApproval === true || raw === 'approve';
|
|
95
|
-
return {
|
|
96
|
-
allowed: !blocked && !approvalRequired,
|
|
97
|
-
blocked,
|
|
98
|
-
approvalRequired,
|
|
99
|
-
reason: firstString(value.reason, value.message, Array.isArray(value.reasons) ? value.reasons.join('; ') : ''),
|
|
100
|
-
raw: value,
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function createLettaToolGuard({ gateCheck, executeTool, onDecision } = {}) {
|
|
105
|
-
if (typeof gateCheck !== 'function') {
|
|
106
|
-
throw new TypeError('createLettaToolGuard requires a gateCheck function');
|
|
107
|
-
}
|
|
108
|
-
if (typeof executeTool !== 'function') {
|
|
109
|
-
throw new TypeError('createLettaToolGuard requires an executeTool function');
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
return async function guardedLettaTool(input = {}) {
|
|
113
|
-
const normalizedAction = normalizeLettaAction(input);
|
|
114
|
-
const decision = normalizeGateDecision(await gateCheck(normalizedAction));
|
|
115
|
-
if (typeof onDecision === 'function') {
|
|
116
|
-
await onDecision({ normalizedAction, decision });
|
|
117
|
-
}
|
|
118
|
-
if (!decision.allowed) {
|
|
119
|
-
const error = new Error(decision.reason || 'ThumbGate blocked this Letta tool call before execution.');
|
|
120
|
-
error.code = decision.approvalRequired ? 'THUMBGATE_APPROVAL_REQUIRED' : 'THUMBGATE_BLOCKED';
|
|
121
|
-
error.thumbgate = { normalizedAction, decision };
|
|
122
|
-
throw error;
|
|
123
|
-
}
|
|
124
|
-
return executeTool(input, { normalizedAction, decision });
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
module.exports = {
|
|
129
|
-
createLettaToolGuard,
|
|
130
|
-
extractLettaToolCall,
|
|
131
|
-
normalizeGateDecision,
|
|
132
|
-
normalizeLettaAction,
|
|
133
|
-
};
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
const DEFAULT_ENDPOINT = 'https://api.oraclestechnologies.com/v1/guardian/analyze';
|
|
5
|
-
|
|
6
|
-
function requireApiKey(env = process.env) {
|
|
7
|
-
const key = env.ETHICORE_API_KEY || env.GUARDIAN_API_KEY || env.ORACLES_GUARDIAN_API_KEY;
|
|
8
|
-
if (!key) {
|
|
9
|
-
throw new Error('ETHICORE_API_KEY env var is required');
|
|
10
|
-
}
|
|
11
|
-
return key;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
async function analyzeText(text, options = {}) {
|
|
15
|
-
if (!String(text || '').trim()) {
|
|
16
|
-
throw new Error('analyzeText requires text');
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const env = options.env || process.env;
|
|
20
|
-
const endpoint = options.endpoint || env.ETHICORE_GUARDIAN_ENDPOINT || DEFAULT_ENDPOINT;
|
|
21
|
-
const apiKey = options.apiKey || requireApiKey(env);
|
|
22
|
-
const fetchImpl = options.fetch || fetch;
|
|
23
|
-
|
|
24
|
-
const response = await fetchImpl(endpoint, {
|
|
25
|
-
method: 'POST',
|
|
26
|
-
headers: {
|
|
27
|
-
Authorization: `Bearer ${apiKey}`,
|
|
28
|
-
'Content-Type': 'application/json',
|
|
29
|
-
},
|
|
30
|
-
body: JSON.stringify({ text }),
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
const bodyText = await response.text();
|
|
34
|
-
let body = bodyText;
|
|
35
|
-
try {
|
|
36
|
-
body = bodyText ? JSON.parse(bodyText) : {};
|
|
37
|
-
} catch {
|
|
38
|
-
// Keep non-JSON body for diagnostics.
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
if (!response.ok) {
|
|
42
|
-
throw new Error(`Ethicore Guardian API ${response.status}: ${typeof body === 'string' ? body : JSON.stringify(body)}`);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return body;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function createEthicorePolicyCheck(options = {}) {
|
|
49
|
-
return async function ethicorePolicyCheck(action = {}) {
|
|
50
|
-
const toolText = [
|
|
51
|
-
action.toolName,
|
|
52
|
-
action.actionType,
|
|
53
|
-
action.command,
|
|
54
|
-
action.path,
|
|
55
|
-
action.url,
|
|
56
|
-
action.input ? JSON.stringify(action.input) : '',
|
|
57
|
-
].filter(Boolean).join('\n');
|
|
58
|
-
|
|
59
|
-
return analyzeText(toolText || JSON.stringify(action), options);
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
module.exports = {
|
|
64
|
-
DEFAULT_ENDPOINT,
|
|
65
|
-
analyzeText,
|
|
66
|
-
createEthicorePolicyCheck,
|
|
67
|
-
requireApiKey,
|
|
68
|
-
};
|