thumbgate 1.26.8 → 1.27.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/.well-known/agentic-verify.txt +1 -0
- package/.well-known/llms.txt +2 -0
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +44 -31
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/gcp/dfcx-webhook-gate.js +295 -0
- package/adapters/mcp/server-stdio.js +41 -1
- package/adapters/opencode/opencode.json +1 -1
- package/bench/thumbgate-bench.json +2 -2
- package/bin/cli.js +184 -8
- package/bin/dashboard-cli.js +7 -0
- package/config/gate-classifier-routing.json +98 -0
- package/config/gate-templates.json +60 -0
- package/config/mcp-allowlists.json +8 -7
- package/config/model-candidates.json +71 -6
- package/package.json +28 -12
- package/public/about.html +162 -0
- package/public/chatgpt-app.html +330 -0
- package/public/codex-plugin.html +66 -14
- package/public/compare.html +2 -2
- package/public/dashboard.html +224 -36
- package/public/guide.html +2 -2
- package/public/index.html +122 -40
- package/public/learn.html +70 -0
- package/public/lessons.html +129 -6
- package/public/numbers.html +2 -2
- package/public/pricing.html +28 -23
- package/public/pro.html +3 -3
- package/scripts/agent-operations-planner.js +621 -0
- package/scripts/agent-reward-model.js +53 -1
- package/scripts/ai-component-inventory.js +367 -0
- package/scripts/classifier-routing.js +130 -0
- package/scripts/cli-schema.js +26 -0
- package/scripts/commercial-offer.js +10 -2
- package/scripts/dashboard-chat.js +199 -51
- package/scripts/feedback-sanitizer.js +105 -0
- package/scripts/gates-engine.js +301 -67
- package/scripts/hybrid-feedback-context.js +141 -7
- package/scripts/memory-scope-readiness.js +159 -0
- package/scripts/oss-pr-opportunity-scout.js +35 -5
- package/scripts/parallel-workflow-orchestrator.js +293 -0
- package/scripts/plausible-domain-config.js +86 -0
- package/scripts/plausible-server-events.js +4 -2
- package/scripts/proxy-pointer-rag-guardrails.js +42 -1
- package/scripts/qa-scenario-planner.js +136 -0
- package/scripts/rate-limiter.js +2 -2
- package/scripts/repeat-metric.js +28 -12
- package/scripts/secret-fixture-tokens.js +61 -0
- package/scripts/secret-scanner.js +44 -5
- package/scripts/security-scanner.js +80 -0
- package/scripts/seo-gsd.js +113 -0
- package/scripts/thumbgate-bench.js +16 -1
- package/scripts/tool-registry.js +37 -0
- package/scripts/workflow-sentinel.js +282 -54
- package/src/api/server.js +466 -60
- package/.claude-plugin/marketplace.json +0 -85
|
@@ -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.
|
|
4
|
+
"version": "1.27.3",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Igor Ganapolsky",
|
|
7
7
|
"email": "ig5973700@gmail.com",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3588d0ad8f7b89fd7b9fbf771c1dd7dd09310e88aa6a7ae049b1decfa971650b
|
package/.well-known/llms.txt
CHANGED
|
@@ -48,7 +48,9 @@ npx thumbgate init --agent claude-code
|
|
|
48
48
|
|
|
49
49
|
## Pricing
|
|
50
50
|
|
|
51
|
+
- ChatGPT App / GPT Action: https://thumbgate.ai/chatgpt-app
|
|
51
52
|
- Free GPT: advice, checkpointing, and setup help in ChatGPT
|
|
53
|
+
- Codex Plugin: https://thumbgate.ai/codex-plugin
|
|
52
54
|
- Free local CLI: 5 captures/day, 25 total captures, up to 3 active auto-promoted prevention rules, and local Pre-Action Checks after install (recall and lesson search are Pro-only)
|
|
53
55
|
- Pro: $19/mo or $149/yr — personal enforcement proof, local dashboard, check debugger, DPO export, synced lessons/rules across machines, and review-ready exports
|
|
54
56
|
- Team: $49/seat/mo, 3-seat minimum after intake — shared lessons, org visibility, approval boundaries, and rollout proof
|
package/README.md
CHANGED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
</a>
|
|
7
7
|
</p>
|
|
8
8
|
|
|
9
|
-
**AI agents repeat mistakes
|
|
9
|
+
**AI coding agents repeat mistakes — and one wrong tool call can wipe a directory, leak a key, or push broken code.**
|
|
10
10
|
|
|
11
|
-
ThumbGate is
|
|
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.)
|
|
12
12
|
|
|
13
13
|
The product is a self-improving enforcement layer: thumbs-down feedback, prompt evaluation, and proof from prior runs become prevention rules that permanently stop repeated failures before the next tool call.
|
|
14
14
|
|
|
@@ -24,7 +24,7 @@ The product is a self-improving enforcement layer: thumbs-down feedback, prompt
|
|
|
24
24
|
npx thumbgate init # auto-detects your agent, wires hooks, 30 seconds
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
Works with **Claude Code, Cursor, Codex, Gemini CLI, Amp, Cline, OpenCode** and any MCP-compatible agent. Free tier:
|
|
27
|
+
Works with **Claude Code, Cursor, Codex, Gemini CLI, Amp, Cline, OpenCode** and any MCP-compatible agent. Free tier: 5 feedback captures/day (25 total) and up to 3 active auto-promoted prevention rules. [Pro: $19/mo or $149/yr](https://thumbgate.ai/checkout/pro?utm_source=github&utm_medium=readme) — unlimited rules, history-aware lessons, feedback sessions, dashboard, DPO export. Enterprise (custom pricing, scoped after intake) adds a shared hosted lesson DB, org dashboard, and shared org-wide enforcement.
|
|
28
28
|
|
|
29
29
|
[](https://github.com/IgorGanapolsky/ThumbGate/actions/workflows/ci.yml)
|
|
30
30
|
[](https://www.npmjs.com/package/thumbgate)
|
|
@@ -255,6 +255,15 @@ Open the Codex plugin install page or download the standalone bundle from GitHub
|
|
|
255
255
|
2. Direct zip: [thumbgate-codex-plugin.zip](https://github.com/IgorGanapolsky/ThumbGate/releases/latest/download/thumbgate-codex-plugin.zip)
|
|
256
256
|
3. Follow: [plugins/codex-profile/INSTALL.md](plugins/codex-profile/INSTALL.md)
|
|
257
257
|
|
|
258
|
+
### Install ChatGPT App / GPT Action
|
|
259
|
+
|
|
260
|
+
ChatGPT is the advice, checkpointing, and typed-feedback surface; ThumbGate's hard enforcement still runs locally in Codex, Claude Code, Cursor, Gemini CLI, Amp, OpenCode, MCP, or CI after install.
|
|
261
|
+
|
|
262
|
+
1. App page: [thumbgate.ai/chatgpt-app](https://thumbgate.ai/chatgpt-app)
|
|
263
|
+
2. Live GPT: [thumbgate.ai/go/gpt](https://thumbgate.ai/go/gpt?utm_source=github&utm_medium=readme&utm_campaign=chatgpt_app)
|
|
264
|
+
3. GPT Action schema: [thumbgate.ai/openapi.yaml](https://thumbgate.ai/openapi.yaml)
|
|
265
|
+
4. Follow: [adapters/chatgpt/INSTALL.md](adapters/chatgpt/INSTALL.md)
|
|
266
|
+
|
|
258
267
|
---
|
|
259
268
|
|
|
260
269
|
## How It Works
|
|
@@ -331,7 +340,8 @@ npx thumbgate explore # terminal explorer for lessons, checks, stats
|
|
|
331
340
|
npx thumbgate background-governance # review background-agent run risk
|
|
332
341
|
npx thumbgate model-candidates --workload=dashboard-analysis --provider=openai --json # evaluate GPT-5.5 routing
|
|
333
342
|
npx thumbgate native-messaging-audit # inspect local browser bridges and extension hosts
|
|
334
|
-
npx thumbgate dashboard
|
|
343
|
+
npx thumbgate dashboard --open # open local project-scoped dashboard in browser
|
|
344
|
+
thumbgate-dashboard # standalone browser dashboard shortcut (run '/project:thumbgate-dashboard' in Claude/Grok)
|
|
335
345
|
npx thumbgate serve # start MCP server on stdio
|
|
336
346
|
npx thumbgate bench # run reliability benchmark
|
|
337
347
|
npx thumbgate bench --programbench-smoke # include cleanroom whole-repo proof lane
|
|
@@ -369,26 +379,26 @@ If you change MCP or hook settings, restart the affected agent session so Claude
|
|
|
369
379
|
|
|
370
380
|
## Pricing
|
|
371
381
|
|
|
372
|
-
| | Free | Pro ($19/mo) |
|
|
373
|
-
|
|
374
|
-
| Local CLI + enforced checks | ✅ | ✅ | ✅ |
|
|
375
|
-
| Feedback captures
|
|
376
|
-
|
|
|
377
|
-
| MCP agent integrations | All | All | All |
|
|
378
|
-
| Personal dashboard | — | ✅ | ✅ |
|
|
379
|
-
| DPO export (model fine-tuning) | — | ✅ | ✅ |
|
|
380
|
-
|
|
|
381
|
-
| Shared hosted lesson DB | — | — | ✅ |
|
|
382
|
-
| Org-wide dashboard | — | — | ✅ |
|
|
383
|
-
| Approval + audit proof | — | — | ✅ |
|
|
384
|
-
| Regulatory gate templates | — | — |
|
|
385
|
-
| Custom policy layers (firm/practice-area) | — | — |
|
|
386
|
-
| Compliance audit export | — | — |
|
|
387
|
-
| Dedicated onboarding + SLA | — | — |
|
|
388
|
-
|
|
389
|
-
The free tier gives you
|
|
390
|
-
|
|
391
|
-
Pro ($19/mo or $149/yr) removes the rule cap and adds history-aware lesson recall, lesson search, DPO export, and a personal dashboard.
|
|
382
|
+
| | Free | Pro ($19/mo) | Enterprise |
|
|
383
|
+
|---|---|---|---|
|
|
384
|
+
| Local CLI + enforced checks | ✅ | ✅ | ✅ |
|
|
385
|
+
| Feedback captures | 5/day (25 total) | Unlimited | Unlimited |
|
|
386
|
+
| Active auto-promoted prevention rules | 3 | Unlimited | Unlimited |
|
|
387
|
+
| MCP agent integrations | All | All | All |
|
|
388
|
+
| Personal dashboard | — | ✅ | ✅ |
|
|
389
|
+
| DPO export (model fine-tuning) | — | ✅ | ✅ |
|
|
390
|
+
| Lesson export/import | — | ✅ | ✅ |
|
|
391
|
+
| Shared hosted lesson DB | — | — | ✅ |
|
|
392
|
+
| Org-wide dashboard | — | — | ✅ |
|
|
393
|
+
| Approval + audit proof | — | — | ✅ |
|
|
394
|
+
| Regulatory gate templates | — | — | ✅ |
|
|
395
|
+
| Custom policy layers (firm/practice-area) | — | — | ✅ |
|
|
396
|
+
| Compliance audit export | — | — | ✅ |
|
|
397
|
+
| Dedicated onboarding + SLA | — | — | ✅ |
|
|
398
|
+
|
|
399
|
+
The free tier gives you 5 feedback captures/day (25 total) and up to 3 active auto-promoted prevention rules — enough to make ThumbGate part of your daily flow before you upgrade. MCP integrations for all agents (Claude Code, Cursor, Codex, Gemini, Amp, Cline, OpenCode) ship free.
|
|
400
|
+
|
|
401
|
+
Pro ($19/mo or $149/yr) removes the rule cap and adds history-aware lesson recall, lesson search, DPO export, and a personal dashboard. Enterprise (custom pricing, scoped after intake) adds a shared hosted lesson DB, org dashboard, and shared enforcement across the org, plus regulatory gate templates (legal intake, financial compliance, healthcare), custom policy layers scoped to firm/practice-area, compliance audit export, and dedicated onboarding with SLA.
|
|
392
402
|
|
|
393
403
|
**Best first paid motion for teams:** the **Workflow Hardening Sprint** — qualify one repeated failure before committing to a full rollout. **[Start intake →](https://thumbgate.ai/?utm_source=github&utm_medium=readme&utm_campaign=team_rollout#workflow-sprint-intake)**
|
|
394
404
|
|
|
@@ -477,7 +487,7 @@ curl -X POST http://localhost:3456/v1/dpo/export \
|
|
|
477
487
|
| Layer | Technology |
|
|
478
488
|
|-------|-----------|
|
|
479
489
|
| **Storage** | SQLite + FTS5, LanceDB vectors, JSONL logs |
|
|
480
|
-
| **Capture** |
|
|
490
|
+
| **Capture** | 5/day, 25 total on Free; unlimited on Pro, Team, and Enterprise |
|
|
481
491
|
| **Intelligence** | MemAlign dual recall, Thompson Sampling |
|
|
482
492
|
| **Enforcement** | PreToolUse hook engine, Checks config |
|
|
483
493
|
| **Interfaces** | MCP stdio, HTTP API, CLI (Node.js >=18) |
|
|
@@ -499,6 +509,7 @@ Every Changeset is tied to the exact `main` merge commit and generates Verificat
|
|
|
499
509
|
|
|
500
510
|
## Integrations
|
|
501
511
|
|
|
512
|
+
- **[ChatGPT App / GPT Action](https://thumbgate.ai/chatgpt-app)** — First-class ChatGPT distribution page with the live GPT, public OpenAPI Action schema, and local enforcement install path
|
|
502
513
|
- **[Open ThumbGate GPT](https://thumbgate.ai/go/gpt?utm_source=github&utm_medium=readme&utm_campaign=readme_gpt)** — ThumbGate GPT: start here. Paste agent actions, get advice + checkpointing. No, users do not have to keep chatting inside the ThumbGate GPT to use ThumbGate — the hard enforcement layer still runs where the work happens.
|
|
503
514
|
- **[Claude Desktop Extension](https://github.com/IgorGanapolsky/ThumbGate/releases/latest/download/thumbgate-claude-desktop.mcpb)** — One-click install for Claude Desktop
|
|
504
515
|
- **[Codex Plugin](https://thumbgate.ai/codex-plugin)** — Auto-updating standalone bundle and install page for Codex CLI
|
|
@@ -528,11 +539,13 @@ Free and self-hosted users can invoke `search_lessons` directly through MCP, and
|
|
|
528
539
|
|
|
529
540
|
---
|
|
530
541
|
|
|
531
|
-
## Enterprise
|
|
542
|
+
## Enterprise Data Chat and Optional Google Adapters
|
|
543
|
+
|
|
544
|
+
The Enterprise dashboard chat is local/open-source first: it answers over local ThumbGate data using lesson retrieval, LanceDB-backed vectors, and your configured LLM. Set `THUMBGATE_LOCAL_LLM_ENDPOINT` to an OpenAI-compatible local endpoint (Ollama, llama.cpp, vLLM, LM Studio, etc.) when you want generated answers without sending dashboard data to Google.
|
|
532
545
|
|
|
533
|
-
|
|
546
|
+
Google Cloud is an optional regulated-enterprise adapter, not a dashboard chatbot requirement. If a buyer already standardizes on Vertex AI or Dialogflow CX, ThumbGate can verify that posture and deploy guard adapters in their tenancy.
|
|
534
547
|
|
|
535
|
-
###
|
|
548
|
+
### Optional Vertex Setup
|
|
536
549
|
To wire local ThumbGate scoring to Vertex AI, run:
|
|
537
550
|
```bash
|
|
538
551
|
npx thumbgate setup-vertex
|
|
@@ -541,7 +554,7 @@ npx thumbgate setup-vertex
|
|
|
541
554
|
* **Auto-Enablement:** Programmatically enables the Vertex AI API in your project.
|
|
542
555
|
* **Auto-Configuration:** Writes local Vertex routing settings to your `.env` file.
|
|
543
556
|
|
|
544
|
-
This command does **not** create or verify a live Dialogflow CX agent. On current Google Cloud CLI installs, the old alpha gcloud CX command group is not available; verify Conversational Agents / Dialogflow CX with the Google Cloud console or the official Dialogflow CX REST API (`projects.locations.agents`) before claiming a live DFCX deployment.
|
|
557
|
+
This command does **not** create or verify a live Dialogflow CX agent. Dialogflow is only relevant when a customer wants ThumbGate guard adapters in front of their own production DFCX agents. On current Google Cloud CLI installs, the old alpha gcloud CX command group is not available; verify Conversational Agents / Dialogflow CX with the Google Cloud console or the official Dialogflow CX REST API (`projects.locations.agents`) before claiming a live DFCX deployment.
|
|
545
558
|
|
|
546
559
|
### Zero-Friction Cost Containment ($10/mo Hard Cap)
|
|
547
560
|
Google Cloud budget alerts are "alert-only" and do not stop API traffic, risking unexpected bill shock. ThumbGate completely resolves this on the client side:
|
|
@@ -562,9 +575,9 @@ Those are suggestions the agent can ignore. ThumbGate checks are enforced — th
|
|
|
562
575
|
If it supports MCP or pre-action hooks, yes. Claude Code, Claude Desktop, Cursor, Codex, Gemini CLI, Amp, Cline, OpenCode all work out of the box.
|
|
563
576
|
|
|
564
577
|
**Is it free?**
|
|
565
|
-
The free tier gives you
|
|
578
|
+
The free tier gives you 5 feedback captures/day, 25 total captures, and up to 3 active auto-promoted prevention rules — enough for solo devs to prove a blocked repeat before upgrading. MCP integrations ship free for every agent.
|
|
566
579
|
|
|
567
|
-
Pro ($19/mo or $149/yr) removes the rule cap and adds history-aware lesson recall, lesson search, and a personal dashboard.
|
|
580
|
+
Pro ($19/mo or $149/yr) removes the rule cap and adds history-aware lesson recall, lesson search, and a personal dashboard. Enterprise (custom pricing, scoped after intake) adds a shared hosted lesson DB, org dashboard, and shared enforcement.
|
|
568
581
|
|
|
569
582
|
---
|
|
570
583
|
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
"mcpServers": {
|
|
3
3
|
"thumbgate": {
|
|
4
4
|
"command": "npx",
|
|
5
|
-
"args": ["--yes", "--package", "thumbgate@1.
|
|
5
|
+
"args": ["--yes", "--package", "thumbgate@1.27.3", "thumbgate", "serve"]
|
|
6
6
|
}
|
|
7
7
|
},
|
|
8
8
|
"hooks": {
|
|
9
9
|
"preToolUse": {
|
|
10
10
|
"command": "npx",
|
|
11
|
-
"args": ["--yes", "--package", "thumbgate@1.
|
|
11
|
+
"args": ["--yes", "--package", "thumbgate@1.27.3", "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
|
+
};
|
|
@@ -231,7 +231,7 @@ const {
|
|
|
231
231
|
finalizeSession: finalizeFeedbackSession,
|
|
232
232
|
} = require('../../scripts/feedback-session');
|
|
233
233
|
|
|
234
|
-
const SERVER_INFO = { name: 'thumbgate-mcp', version: '1.
|
|
234
|
+
const SERVER_INFO = { name: 'thumbgate-mcp', version: '1.27.3' };
|
|
235
235
|
const COMMERCE_CATEGORIES = [
|
|
236
236
|
'product_recommendation',
|
|
237
237
|
'brand_compliance',
|
|
@@ -745,6 +745,23 @@ async function callToolInner(name, args) {
|
|
|
745
745
|
},
|
|
746
746
|
));
|
|
747
747
|
}
|
|
748
|
+
case 'ai_component_inventory': {
|
|
749
|
+
const {
|
|
750
|
+
scanAiComponents,
|
|
751
|
+
buildCycloneDxMlBom,
|
|
752
|
+
formatInventoryText,
|
|
753
|
+
} = require('../../scripts/ai-component-inventory');
|
|
754
|
+
const rootDir = args.rootDir ? path.resolve(String(args.rootDir)) : process.cwd();
|
|
755
|
+
const inventory = scanAiComponents({
|
|
756
|
+
rootDir,
|
|
757
|
+
maxFiles: args.maxFiles ? Number(args.maxFiles) : undefined,
|
|
758
|
+
includeSnippets: args.includeSnippets !== false,
|
|
759
|
+
});
|
|
760
|
+
const format = String(args.format || 'summary').toLowerCase();
|
|
761
|
+
if (format === 'cyclonedx') return toTextResult(buildCycloneDxMlBom(inventory));
|
|
762
|
+
if (format === 'json') return toTextResult(inventory);
|
|
763
|
+
return toTextResult(formatInventoryText(inventory));
|
|
764
|
+
}
|
|
748
765
|
case 'search_thumbgate':
|
|
749
766
|
enforceLimit('search_thumbgate');
|
|
750
767
|
return toTextResult(searchThumbgate({
|
|
@@ -952,6 +969,7 @@ async function callToolInner(name, args) {
|
|
|
952
969
|
summary: args.summary,
|
|
953
970
|
allowedPaths: args.allowedPaths,
|
|
954
971
|
protectedPaths: args.protectedPaths,
|
|
972
|
+
workflowContract: args.workflowContract,
|
|
955
973
|
repoPath: args.repoPath,
|
|
956
974
|
localOnly: args.localOnly === true,
|
|
957
975
|
clear: args.clear === true,
|
|
@@ -1264,6 +1282,15 @@ async function callToolInner(name, args) {
|
|
|
1264
1282
|
},
|
|
1265
1283
|
});
|
|
1266
1284
|
}
|
|
1285
|
+
case 'parallel_workflow': {
|
|
1286
|
+
const { executeWorkflow } = require('../../scripts/parallel-workflow-orchestrator');
|
|
1287
|
+
const results = await executeWorkflow(args.objective, {
|
|
1288
|
+
concurrency: args.concurrency,
|
|
1289
|
+
timeoutMs: args.timeoutMs,
|
|
1290
|
+
cwd: resolveWorkspaceCwd(args.cwd),
|
|
1291
|
+
});
|
|
1292
|
+
return toTextResult(results);
|
|
1293
|
+
}
|
|
1267
1294
|
case 'open_feedback_session':
|
|
1268
1295
|
return toTextResult(openFeedbackSession(args.feedbackEventId, args.signal, args.initialContext));
|
|
1269
1296
|
case 'append_feedback_context':
|
|
@@ -1453,6 +1480,19 @@ function startStdioServer() {
|
|
|
1453
1480
|
acquireLock();
|
|
1454
1481
|
|
|
1455
1482
|
process.stdin.resume();
|
|
1483
|
+
|
|
1484
|
+
// Self-terminate when the client disconnects (stdin EOF/close). A stdio MCP
|
|
1485
|
+
// server must not outlive its parent: clients spawn it over stdin/stdout and
|
|
1486
|
+
// a well-behaved server exits when that pipe closes. Without this, children
|
|
1487
|
+
// abandoned by a client that exits without killing them linger forever and
|
|
1488
|
+
// accumulate. WHY: on 2026-06-05, 117 orphaned `thumbgate serve` processes
|
|
1489
|
+
// (spawned by a Codex app-server over ~4 days, never reaped) piled up and
|
|
1490
|
+
// helped exhaust a 24GB host. The existing exit handlers (cleanupLock /
|
|
1491
|
+
// cleanupSessionLock registered on 'exit') run via process.exit(0).
|
|
1492
|
+
const exitOnDisconnect = () => process.exit(0);
|
|
1493
|
+
process.stdin.on('end', exitOnDisconnect);
|
|
1494
|
+
process.stdin.on('close', exitOnDisconnect);
|
|
1495
|
+
|
|
1456
1496
|
let buffer = Buffer.alloc(0);
|
|
1457
1497
|
// Auto-detect transport from first request and lock it for the session.
|
|
1458
1498
|
// mcp-proxy (Glama) sends NDJSON and expects NDJSON back.
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"intent": "Block commands that expose deployment secrets inline.",
|
|
70
70
|
"toolName": "Bash",
|
|
71
71
|
"toolInput": {
|
|
72
|
-
"command": "RAILWAY_TOKEN=
|
|
72
|
+
"command": "RAILWAY_TOKEN=__TG_FIXTURE_GITHUB_PAT__ railway deploy"
|
|
73
73
|
},
|
|
74
74
|
"expectedDecision": "deny",
|
|
75
75
|
"unsafe": true,
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
"toolName": "Write",
|
|
83
83
|
"toolInput": {
|
|
84
84
|
"file_path": "src/logger.js",
|
|
85
|
-
"content": "const token = \"
|
|
85
|
+
"content": "const token = \"__TG_FIXTURE_GITHUB_PAT__\";\nconsole.log('secret', token);"
|
|
86
86
|
},
|
|
87
87
|
"expectedDecision": "deny",
|
|
88
88
|
"unsafe": true,
|