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.
Files changed (107) hide show
  1. package/.claude-plugin/marketplace.json +85 -0
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.well-known/mcp/server-card.json +1 -1
  4. package/README.md +33 -5
  5. package/adapters/chatgpt/openapi.yaml +1707 -0
  6. package/adapters/claude/.mcp.json +2 -2
  7. package/adapters/codex/config.toml +2 -2
  8. package/adapters/mcp/server-stdio.js +1 -1
  9. package/adapters/opencode/opencode.json +1 -1
  10. package/bin/cli.js +63 -54
  11. package/bin/postinstall.js +23 -14
  12. package/config/post-deploy-marketing-pages.json +19 -0
  13. package/config/tessl-tiles.json +1 -3
  14. package/package.json +99 -229
  15. package/public/guide.html +0 -4
  16. package/public/index.html +28 -49
  17. package/public/learn.html +0 -8
  18. package/public/numbers.html +2 -2
  19. package/scripts/agent-readiness.js +0 -147
  20. package/scripts/bot-detection.js +61 -3
  21. package/scripts/cli-feedback.js +4 -14
  22. package/scripts/cli-schema.js +7 -0
  23. package/scripts/durability/step.js +3 -3
  24. package/scripts/feedback-loop.js +10 -32
  25. package/scripts/gates-engine.js +72 -1
  26. package/scripts/hook-runtime.js +2 -2
  27. package/scripts/license.js +10 -9
  28. package/scripts/llm-client.js +44 -260
  29. package/scripts/mcp-config.js +7 -1
  30. package/scripts/published-cli.js +0 -8
  31. package/scripts/seo-gsd.js +0 -5
  32. package/src/api/server.js +111 -35
  33. package/.claude/commands/dashboard.md +0 -15
  34. package/.claude/commands/thumbgate-blocked.md +0 -27
  35. package/.claude/commands/thumbgate-dashboard.md +0 -15
  36. package/.claude/commands/thumbgate-doctor.md +0 -30
  37. package/.claude/commands/thumbgate-guard.md +0 -36
  38. package/.claude/commands/thumbgate-protect.md +0 -30
  39. package/.claude/commands/thumbgate-rules.md +0 -30
  40. package/adapters/gcp/dfcx-webhook-gate.js +0 -295
  41. package/adapters/letta/README.md +0 -41
  42. package/adapters/letta/thumbgate-letta-adapter.js +0 -133
  43. package/adapters/policy-engine/ethicore-guardian-client.js +0 -68
  44. package/adapters/policy-engine/thumbgate-policy-engine-adapter.js +0 -260
  45. package/bench/observability-eval-suite.json +0 -26
  46. package/bin/dashboard-cli.js +0 -7
  47. package/commands/dashboard.md +0 -15
  48. package/commands/thumbgate-dashboard.md +0 -15
  49. package/config/builtin-lessons.json +0 -23
  50. package/public/about.html +0 -162
  51. package/public/agent-manager.html +0 -179
  52. package/public/agents-cost-savings.html +0 -153
  53. package/public/ai-malpractice-prevention.html +0 -818
  54. package/public/assets/brand/github-social-preview.png +0 -0
  55. package/public/assets/brand/thumbgate-icon-512.png +0 -0
  56. package/public/assets/brand/thumbgate-icon-pro-512.png +0 -0
  57. package/public/assets/brand/thumbgate-icon-team-512.png +0 -0
  58. package/public/assets/brand/thumbgate-logo-1200x360.png +0 -0
  59. package/public/assets/brand/thumbgate-logo-transparent.svg +0 -22
  60. package/public/assets/brand/thumbgate-mark-inline-v3.svg +0 -19
  61. package/public/assets/brand/thumbgate-mark-pro.svg +0 -23
  62. package/public/assets/brand/thumbgate-mark-team.svg +0 -26
  63. package/public/assets/brand/thumbgate-mark.svg +0 -21
  64. package/public/assets/brand/thumbgate-wordmark.svg +0 -20
  65. package/public/assets/claude-thumbgate-statusbar.svg +0 -8
  66. package/public/assets/codex-thumbgate-statusbar-test.svg +0 -9
  67. package/public/assets/legal-intake-control-flow.svg +0 -66
  68. package/public/brand/thumbgate-mark.svg +0 -19
  69. package/public/brand/thumbgate-og.svg +0 -16
  70. package/public/chatgpt-app.html +0 -330
  71. package/public/codex-enterprise.html +0 -123
  72. package/public/js/buyer-intent.js +0 -672
  73. package/public/pricing.html +0 -399
  74. package/scripts/action-receipts.js +0 -324
  75. package/scripts/activation-quickstart.js +0 -187
  76. package/scripts/agent-operations-planner.js +0 -621
  77. package/scripts/ai-component-inventory.js +0 -367
  78. package/scripts/async-eval-observability.js +0 -236
  79. package/scripts/audit.js +0 -65
  80. package/scripts/aws-blocks-guardrails.js +0 -228
  81. package/scripts/bot-detector.js +0 -50
  82. package/scripts/classifier-routing.js +0 -130
  83. package/scripts/dashboard-chat.js +0 -332
  84. package/scripts/feedback-aggregate.js +0 -281
  85. package/scripts/feedback-sanitizer.js +0 -105
  86. package/scripts/hob-pack.js +0 -591
  87. package/scripts/hook-stop-anti-claim.js +0 -301
  88. package/scripts/install-shim.js +0 -87
  89. package/scripts/mcp-oauth.js +0 -293
  90. package/scripts/noop-detect.js +0 -285
  91. package/scripts/omlx-smoke.js +0 -192
  92. package/scripts/parallel-workflow-orchestrator.js +0 -293
  93. package/scripts/plan-gate.js +0 -243
  94. package/scripts/plausible-domain-config.js +0 -99
  95. package/scripts/qa-scenario-planner.js +0 -136
  96. package/scripts/repeat-metric.js +0 -137
  97. package/scripts/secret-fixture-tokens.js +0 -61
  98. package/scripts/secret-redaction.js +0 -166
  99. package/scripts/self-harness-optimizer.js +0 -141
  100. package/scripts/self-healing-check.js +0 -193
  101. package/scripts/silent-failure-cluster.js +0 -531
  102. package/scripts/statusline-cache-read.js +0 -57
  103. package/scripts/sync-telemetry-from-prod.js +0 -374
  104. package/scripts/tool-contract-validator.js +0 -76
  105. package/scripts/trajectory-scorer.js +0 -63
  106. package/scripts/verify-marketing-pages-deployed.js +0 -195
  107. 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
- };
@@ -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
- };