thumbgate 1.27.19 → 1.27.20
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/commands/dashboard.md +15 -0
- package/.claude/commands/thumbgate-blocked.md +27 -0
- package/.claude/commands/thumbgate-dashboard.md +15 -0
- package/.claude/commands/thumbgate-doctor.md +30 -0
- package/.claude/commands/thumbgate-guard.md +36 -0
- package/.claude/commands/thumbgate-protect.md +30 -0
- package/.claude/commands/thumbgate-rules.md +30 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +0 -3
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/gcp/dfcx-webhook-gate.js +295 -0
- package/adapters/letta/README.md +41 -0
- package/adapters/letta/thumbgate-letta-adapter.js +133 -0
- package/adapters/mcp/server-stdio.js +1 -1
- package/adapters/opencode/opencode.json +1 -1
- package/adapters/policy-engine/ethicore-guardian-client.js +68 -0
- package/adapters/policy-engine/thumbgate-policy-engine-adapter.js +260 -0
- package/bench/observability-eval-suite.json +26 -0
- package/bin/cli.js +27 -1
- package/bin/dashboard-cli.js +7 -0
- package/bin/postinstall.js +14 -23
- package/commands/dashboard.md +15 -0
- package/commands/thumbgate-dashboard.md +15 -0
- package/package.json +225 -100
- package/public/about.html +162 -0
- package/public/agent-manager.html +179 -0
- package/public/agents-cost-savings.html +153 -0
- package/public/ai-malpractice-prevention.html +818 -0
- 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 +28 -0
- package/public/assets/brand/thumbgate-mark-inline-v3.svg +18 -0
- package/public/assets/brand/thumbgate-mark-pro.svg +23 -0
- package/public/assets/brand/thumbgate-mark-team.svg +26 -0
- package/public/assets/brand/thumbgate-mark.svg +21 -0
- package/public/assets/brand/thumbgate-wordmark.svg +20 -0
- package/public/assets/claude-thumbgate-statusbar.svg +8 -0
- package/public/assets/codex-thumbgate-statusbar-test.svg +9 -0
- package/public/assets/legal-intake-control-flow.svg +66 -0
- package/public/brand/thumbgate-mark.svg +19 -0
- package/public/brand/thumbgate-og.svg +16 -0
- package/public/chatgpt-app.html +330 -0
- package/public/codex-enterprise.html +123 -0
- package/public/diagnostic.html +345 -0
- package/public/index.html +2 -2
- package/public/install.html +193 -0
- package/public/js/buyer-intent.js +672 -0
- package/public/numbers.html +2 -2
- package/public/pricing.html +399 -0
- package/scripts/action-receipts.js +324 -0
- package/scripts/activation-quickstart.js +187 -0
- package/scripts/agent-operations-planner.js +621 -0
- package/scripts/ai-component-inventory.js +367 -0
- package/scripts/async-eval-observability.js +236 -0
- package/scripts/audit.js +65 -0
- package/scripts/aws-blocks-guardrails.js +272 -0
- package/scripts/classifier-routing.js +130 -0
- package/scripts/dashboard-chat.js +332 -0
- package/scripts/feedback-aggregate.js +281 -0
- package/scripts/feedback-sanitizer.js +105 -0
- package/scripts/hook-stop-anti-claim.js +301 -0
- package/scripts/install-shim.js +87 -0
- package/scripts/mcp-oauth.js +293 -0
- package/scripts/noop-detect.js +285 -0
- package/scripts/parallel-workflow-orchestrator.js +293 -0
- package/scripts/plan-gate.js +243 -0
- package/scripts/plausible-domain-config.js +99 -0
- package/scripts/qa-scenario-planner.js +136 -0
- package/scripts/repeat-metric.js +137 -0
- package/scripts/secret-fixture-tokens.js +61 -0
- package/scripts/secret-redaction.js +166 -0
- package/scripts/self-harness-optimizer.js +141 -0
- package/scripts/self-healing-check.js +193 -0
- package/scripts/self-protection.js +90 -0
- package/scripts/silent-failure-cluster.js +531 -0
- package/scripts/statusline-cache-read.js +57 -0
- package/scripts/sync-telemetry-from-prod.js +374 -0
- package/scripts/tool-contract-validator.js +76 -0
- package/scripts/trajectory-scorer.js +63 -0
- package/scripts/verify-marketing-pages-deployed.js +212 -0
- package/scripts/visitor-journey.js +172 -0
- package/.claude-plugin/marketplace.json +0 -85
- package/adapters/chatgpt/openapi.yaml +0 -1707
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dashboard
|
|
3
|
+
description: Open the local HTTP dashboard for the current project in your web browser.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Open Dashboard
|
|
7
|
+
|
|
8
|
+
Open the local HTTP dashboard for the current project in your web browser.
|
|
9
|
+
|
|
10
|
+
## Instructions
|
|
11
|
+
Execute the following command in the project directory to open the browser dashboard scoped to the current repository:
|
|
12
|
+
```bash
|
|
13
|
+
thumbgate-dashboard
|
|
14
|
+
```
|
|
15
|
+
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: thumbgate-blocked
|
|
3
|
+
description: Show what ThumbGate has actually blocked — gate enforcement stats and the full enforcement matrix. Use for "what has ThumbGate blocked", "show gate stats", "is enforcement working", "how many tokens did we save", "enforcement matrix".
|
|
4
|
+
allowed-tools: mcp__thumbgate__gate_stats, mcp__thumbgate__enforcement_matrix, Bash(npx thumbgate gate-stats:*)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# ThumbGate Blocked
|
|
8
|
+
|
|
9
|
+
Show the enforcement record: how many risky actions were blocked vs warned, which gates fire most, and the full feedback → check → rejection pipeline.
|
|
10
|
+
|
|
11
|
+
This command wraps existing ThumbGate capability — **no new logic**. It reads the live enforcement counters.
|
|
12
|
+
|
|
13
|
+
## Steps
|
|
14
|
+
|
|
15
|
+
1. Call the `gate_stats` MCP tool for the headline numbers: blocked count, warned count, and the top gates by hits. (CLI fallback: `npx thumbgate gate-stats`.)
|
|
16
|
+
2. Call the `enforcement_matrix` MCP tool for the full picture: feedback pipeline stats, active pre-action checks, and the rejection ledger with revival conditions.
|
|
17
|
+
3. Summarize for the user:
|
|
18
|
+
- Total blocks (each block = a repeat mistake stopped before it spent tokens or did damage).
|
|
19
|
+
- Most-triggered gates.
|
|
20
|
+
- Anything in the rejection ledger that is close to revival.
|
|
21
|
+
4. If counts are all zero, note that enforcement is wired but hasn't fired yet, and point to `/thumbgate-guard` to promote a rule.
|
|
22
|
+
|
|
23
|
+
## Example
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
/thumbgate-blocked
|
|
27
|
+
```
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: thumbgate-dashboard
|
|
3
|
+
description: Open the local HTTP dashboard for the current project in your web browser.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Open Scoped ThumbGate Dashboard
|
|
7
|
+
|
|
8
|
+
Open the local HTTP dashboard for the current project in your web browser.
|
|
9
|
+
|
|
10
|
+
## Instructions
|
|
11
|
+
Execute the following command in the project directory to open the browser dashboard:
|
|
12
|
+
```bash
|
|
13
|
+
thumbgate-dashboard
|
|
14
|
+
```
|
|
15
|
+
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: thumbgate-doctor
|
|
3
|
+
description: Health-check the ThumbGate wiring for this project — hooks, MCP server, and agent-readiness — and report what's broken. Use for "is ThumbGate wired up", "thumbgate doctor", "check my guardrails are installed", "why aren't my gates firing", "agent readiness".
|
|
4
|
+
allowed-tools: Bash(npx thumbgate doctor:*), mcp__thumbgate__check_operational_integrity
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# ThumbGate Doctor
|
|
8
|
+
|
|
9
|
+
Audit whether ThumbGate is actually wired into this agent: PreToolUse / SessionStart hooks installed, MCP server reachable, lesson store present, and overall agent-readiness — then tell the user exactly what to fix.
|
|
10
|
+
|
|
11
|
+
This command wraps existing ThumbGate capability — **no new logic**. It runs the existing doctor + integrity checks.
|
|
12
|
+
|
|
13
|
+
## Steps
|
|
14
|
+
|
|
15
|
+
1. Run the existing wiring/health audit:
|
|
16
|
+
```bash
|
|
17
|
+
npx thumbgate doctor
|
|
18
|
+
```
|
|
19
|
+
(Add `--json` for a machine-readable report.) It exits non-zero when the project is not `ready`.
|
|
20
|
+
2. For deeper runtime state, call the `check_operational_integrity` MCP tool to verify the server-side enforcement path is live, not just the local config.
|
|
21
|
+
3. Summarize:
|
|
22
|
+
- ✅ what's wired (hooks, MCP, store, statusline).
|
|
23
|
+
- ❌ what's missing, with the exact fix command (usually `npx thumbgate init`).
|
|
24
|
+
4. If everything is green, say so plainly with the readiness status; if not, lead with the single highest-impact fix.
|
|
25
|
+
|
|
26
|
+
## Example
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
/thumbgate-doctor
|
|
30
|
+
```
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: thumbgate-guard
|
|
3
|
+
description: Turn the last agent mistake into a hard prevention rule the agent cannot bypass. Use after a bad tool call, a wrong action, or a thumbs-down — "guard against this", "block this from happening again", "never do that again", "promote this to a rule".
|
|
4
|
+
allowed-tools: mcp__thumbgate__capture_feedback, Bash(npx thumbgate force-gate:*), Bash(npx thumbgate quickstart:*)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# ThumbGate Guard
|
|
8
|
+
|
|
9
|
+
Capture the mistake the agent just made and promote it into a Pre-Action Check (a `block` gate) so the same tool-call shape is intercepted before it runs again — in this and every future session, across Claude Code, Cursor, Codex, Gemini, Amp, and Cline.
|
|
10
|
+
|
|
11
|
+
This command wraps existing ThumbGate capability. It adds **no new logic** — it routes to the real capture + force-promote path.
|
|
12
|
+
|
|
13
|
+
## Steps
|
|
14
|
+
|
|
15
|
+
1. Identify the specific bad action from the recent conversation (e.g. `git push --force origin main`, `DROP TABLE users`, deploy without tests). State it in one sentence.
|
|
16
|
+
2. Record the signal with the `capture_feedback` MCP tool:
|
|
17
|
+
- `signal: "down"`
|
|
18
|
+
- `context`: one sentence describing what went wrong
|
|
19
|
+
- `whatWentWrong`: the concrete failure
|
|
20
|
+
- `whatToChange`: the prevention action
|
|
21
|
+
- `tags`: the domain (e.g. `git`, `database`, `deploy`)
|
|
22
|
+
- If the user only gave a vague signal, pass the recent turns through `conversationWindow` / `chatHistory` for history-aware distillation instead of refusing.
|
|
23
|
+
3. Promote it to an enforced block gate using the existing force-promote path:
|
|
24
|
+
```bash
|
|
25
|
+
npx thumbgate force-gate "<one-sentence context of the mistake>"
|
|
26
|
+
```
|
|
27
|
+
This prints the new `gateId` and the total active gate count.
|
|
28
|
+
4. Show the user the promoted rule and confirm it is now enforced as a PreToolUse block.
|
|
29
|
+
|
|
30
|
+
> First rule of the project and want the guided walkthrough (capture → promote → watch it block once)? Run `npx thumbgate quickstart` instead.
|
|
31
|
+
|
|
32
|
+
## Example
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
/thumbgate-guard the agent force-pushed to main and overwrote a teammate's commit
|
|
36
|
+
```
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: thumbgate-protect
|
|
3
|
+
description: Show this repo's branch/release governance and grant a scoped, time-limited approval for an action that touches protected files. Use for "protect this branch", "is main protected", "approve this protected change", "branch governance", "let me edit a protected file".
|
|
4
|
+
allowed-tools: mcp__thumbgate__get_branch_governance, mcp__thumbgate__approve_protected_action
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# ThumbGate Protect
|
|
8
|
+
|
|
9
|
+
Inspect the protected-action posture for this project and, when the user explicitly approves, grant a scoped, expiring exception so a protected-file edit or publish can proceed under audit.
|
|
10
|
+
|
|
11
|
+
This command wraps existing ThumbGate capability — **no new logic**. It reads governance state and records a time-boxed approval.
|
|
12
|
+
|
|
13
|
+
## Steps
|
|
14
|
+
|
|
15
|
+
1. Read the current posture with the `get_branch_governance` MCP tool: which branches are protected, release rules, and the protected-file globs in effect.
|
|
16
|
+
2. Report it plainly: what is protected, and what the agent is currently blocked from touching without approval.
|
|
17
|
+
3. **Only if the user explicitly asks to proceed**, grant a scoped approval with `approve_protected_action`:
|
|
18
|
+
- `pathGlobs`: the smallest set of protected globs the action needs.
|
|
19
|
+
- `reason`: why this is approved (one sentence).
|
|
20
|
+
- `evidence`: supporting note (tests passing, owner sign-off, etc.) when available.
|
|
21
|
+
- `ttlMs`: keep it short — default is 1 hour, never exceed what the task needs.
|
|
22
|
+
4. Confirm the approval id, covered globs, and expiry. Approvals are deliberately temporary and audited; re-run for the next task.
|
|
23
|
+
|
|
24
|
+
> This is for granting *narrow, temporary* exceptions, not for disabling protection. Never use it to bypass branch governance wholesale.
|
|
25
|
+
|
|
26
|
+
## Example
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
/thumbgate-protect
|
|
30
|
+
```
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: thumbgate-rules
|
|
3
|
+
description: List the active prevention rules and learned lessons guarding this project. Use to answer "what is ThumbGate protecting me from", "show my gates/rules", "what has the agent learned", "what's blocked here".
|
|
4
|
+
allowed-tools: mcp__thumbgate__prevention_rules, mcp__thumbgate__get_reliability_rules, mcp__thumbgate__search_lessons, Bash(npx thumbgate rules:*)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# ThumbGate Rules
|
|
8
|
+
|
|
9
|
+
Show the guardrails currently in force for this project: the auto-promoted prevention rules, the reliability rules, and the promoted lessons behind them.
|
|
10
|
+
|
|
11
|
+
This command wraps existing ThumbGate capability — **no new logic**. It reads the live rule + lesson stores.
|
|
12
|
+
|
|
13
|
+
## Steps
|
|
14
|
+
|
|
15
|
+
1. List the active prevention rules with the `prevention_rules` MCP tool (or the CLI fallback `npx thumbgate rules`).
|
|
16
|
+
2. Pull the reliability rules with `get_reliability_rules` to show which tool-call shapes are gated.
|
|
17
|
+
3. For each rule, surface the lesson it came from with `search_lessons` so the user sees *why* the rule exists, not just *what* it blocks.
|
|
18
|
+
4. Present a compact table:
|
|
19
|
+
|
|
20
|
+
| Rule / Gate | Blocks | From lesson | State |
|
|
21
|
+
|-------------|--------|-------------|-------|
|
|
22
|
+
| … | … | … | active / archived |
|
|
23
|
+
|
|
24
|
+
5. If there are zero active rules, point the user to `/thumbgate-guard` to promote their first one.
|
|
25
|
+
|
|
26
|
+
## Example
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
/thumbgate-rules
|
|
30
|
+
```
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thumbgate",
|
|
3
3
|
"description": "One 👎 becomes a hard rule the agent cannot bypass. Captures thumbs-down feedback, distills it into PreToolUse Pre-Action Checks, enforced across every future Claude Code session.",
|
|
4
|
-
"version": "1.27.
|
|
4
|
+
"version": "1.27.20",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Igor Ganapolsky",
|
|
7
7
|
"email": "ig5973700@gmail.com",
|
package/README.md
CHANGED
|
@@ -6,9 +6,6 @@
|
|
|
6
6
|
</a>
|
|
7
7
|
</p>
|
|
8
8
|
|
|
9
|
-
> **⚡ LAUNCH PROMO: 50% off Pro for 3 months — use code `THUMB50`**
|
|
10
|
-
> [$9.50/mo instead of $19 →](https://buy.stripe.com/eVqeVd62o0aO19v7mb3sI3g) · Limited to first 50 customers
|
|
11
|
-
|
|
12
9
|
**AI coding agents repeat mistakes — and one wrong tool call can wipe a directory, leak a key, or push broken code.**
|
|
13
10
|
|
|
14
11
|
ThumbGate is the local-first firewall for AI coding agents. It runs in the PreToolUse hook on your machine and blocks dangerous tool calls — `rm -rf`, secret exfiltration, off-scope edits, a bad `git push` — before they execute, across Claude Code, Cursor, Codex, Gemini, Amp, Cline, and OpenCode. No server, no gateway. (Regulated-industry policy templates — legal intake, financial compliance, healthcare — build on the same engine.)
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
"mcpServers": {
|
|
3
3
|
"thumbgate": {
|
|
4
4
|
"command": "npx",
|
|
5
|
-
"args": ["--yes", "--package", "thumbgate@1.27.
|
|
5
|
+
"args": ["--yes", "--package", "thumbgate@1.27.20", "thumbgate", "serve"]
|
|
6
6
|
}
|
|
7
7
|
},
|
|
8
8
|
"hooks": {
|
|
9
9
|
"preToolUse": {
|
|
10
10
|
"command": "npx",
|
|
11
|
-
"args": ["--yes", "--package", "thumbgate@1.27.
|
|
11
|
+
"args": ["--yes", "--package", "thumbgate@1.27.20", "thumbgate", "gate-check"]
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
14
|
}
|
|
@@ -0,0 +1,295 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
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.
|