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,133 @@
|
|
|
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
|
+
};
|
|
@@ -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.27.
|
|
234
|
+
const SERVER_INFO = { name: 'thumbgate-mcp', version: '1.27.20' };
|
|
235
235
|
const COMMERCE_CATEGORIES = [
|
|
236
236
|
'product_recommendation',
|
|
237
237
|
'brand_compliance',
|
|
@@ -0,0 +1,68 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { normalizeProviderAction } = require('../../scripts/provider-action-normalizer');
|
|
5
|
+
|
|
6
|
+
const BLOCK_DECISIONS = new Set([
|
|
7
|
+
'block',
|
|
8
|
+
'blocked',
|
|
9
|
+
'deny',
|
|
10
|
+
'denied',
|
|
11
|
+
'disallow',
|
|
12
|
+
'disallowed',
|
|
13
|
+
'fail',
|
|
14
|
+
'failed',
|
|
15
|
+
'forbid',
|
|
16
|
+
'forbidden',
|
|
17
|
+
'reject',
|
|
18
|
+
'rejected',
|
|
19
|
+
'unsafe',
|
|
20
|
+
'violation',
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
const REVIEW_DECISIONS = new Set([
|
|
24
|
+
'approval',
|
|
25
|
+
'approval-required',
|
|
26
|
+
'approval_required',
|
|
27
|
+
'approve',
|
|
28
|
+
'human-review',
|
|
29
|
+
'human_review',
|
|
30
|
+
'manual-review',
|
|
31
|
+
'manual_review',
|
|
32
|
+
'review',
|
|
33
|
+
'requires-approval',
|
|
34
|
+
'requires_approval',
|
|
35
|
+
'requires-review',
|
|
36
|
+
'requires_review',
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
const ALLOW_DECISIONS = new Set([
|
|
40
|
+
'accept',
|
|
41
|
+
'accepted',
|
|
42
|
+
'allow',
|
|
43
|
+
'allowed',
|
|
44
|
+
'ok',
|
|
45
|
+
'pass',
|
|
46
|
+
'passed',
|
|
47
|
+
'permit',
|
|
48
|
+
'permitted',
|
|
49
|
+
'safe',
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
function asObject(value) {
|
|
53
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function asArray(value) {
|
|
57
|
+
return Array.isArray(value) ? value : [];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function firstString(...values) {
|
|
61
|
+
for (const value of values) {
|
|
62
|
+
const text = String(value || '').trim();
|
|
63
|
+
if (text) return text;
|
|
64
|
+
}
|
|
65
|
+
return '';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function normalizeDecisionToken(value) {
|
|
69
|
+
return String(value || '')
|
|
70
|
+
.trim()
|
|
71
|
+
.toLowerCase()
|
|
72
|
+
.replace(/\s+/g, '-');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function normalizeEvidence(value) {
|
|
76
|
+
const direct = asArray(value.evidence);
|
|
77
|
+
const citations = asArray(value.citations);
|
|
78
|
+
const violations = asArray(value.violations);
|
|
79
|
+
const reasons = asArray(value.reasons);
|
|
80
|
+
const reasoning = asArray(value.reasoning);
|
|
81
|
+
const threatTypes = asArray(value.threat_types || value.threatTypes)
|
|
82
|
+
.map((threatType) => ({
|
|
83
|
+
code: String(threatType || '').trim(),
|
|
84
|
+
message: `Threat type: ${String(threatType || '').trim()}`,
|
|
85
|
+
source: 'guardian',
|
|
86
|
+
severity: firstString(value.threat_level, value.threatLevel),
|
|
87
|
+
}));
|
|
88
|
+
return [...direct, ...citations, ...violations, ...reasons, ...reasoning, ...threatTypes]
|
|
89
|
+
.map((entry) => {
|
|
90
|
+
if (typeof entry === 'string') return { text: entry };
|
|
91
|
+
const object = asObject(entry);
|
|
92
|
+
if (!Object.keys(object).length) return null;
|
|
93
|
+
return {
|
|
94
|
+
id: firstString(object.id, object.ruleId, object.rule_id, object.code),
|
|
95
|
+
text: firstString(object.text, object.reason, object.message, object.description, object.title),
|
|
96
|
+
source: firstString(object.source, object.provider, object.policy),
|
|
97
|
+
severity: firstString(object.severity, object.level),
|
|
98
|
+
raw: object,
|
|
99
|
+
};
|
|
100
|
+
})
|
|
101
|
+
.filter(Boolean);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function extractPolicyDecision(input = {}) {
|
|
105
|
+
const event = asObject(input);
|
|
106
|
+
for (const candidate of [
|
|
107
|
+
event.policyDecision,
|
|
108
|
+
event.policy_decision,
|
|
109
|
+
event.guardrailResult,
|
|
110
|
+
event.guardrail_result,
|
|
111
|
+
event.result,
|
|
112
|
+
]) {
|
|
113
|
+
const object = asObject(candidate);
|
|
114
|
+
if (Object.keys(object).length) return object;
|
|
115
|
+
}
|
|
116
|
+
return event;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function classifyPolicyDecision(input = {}) {
|
|
120
|
+
const value = extractPolicyDecision(input);
|
|
121
|
+
const token = normalizeDecisionToken(firstString(
|
|
122
|
+
value.decision,
|
|
123
|
+
value.action,
|
|
124
|
+
value.status,
|
|
125
|
+
value.result,
|
|
126
|
+
value.verdict,
|
|
127
|
+
value.outcome,
|
|
128
|
+
value.effect,
|
|
129
|
+
value.recommended_action,
|
|
130
|
+
value.recommendedAction,
|
|
131
|
+
));
|
|
132
|
+
|
|
133
|
+
if (
|
|
134
|
+
value.allowed === false
|
|
135
|
+
|| value.is_safe === false
|
|
136
|
+
|| value.isSafe === false
|
|
137
|
+
|| value.accepted === false
|
|
138
|
+
|| value.blocked === true
|
|
139
|
+
|| value.denied === true
|
|
140
|
+
|| BLOCK_DECISIONS.has(token)
|
|
141
|
+
) {
|
|
142
|
+
return 'block';
|
|
143
|
+
}
|
|
144
|
+
if (
|
|
145
|
+
value.requiresApproval === true
|
|
146
|
+
|| value.requires_approval === true
|
|
147
|
+
|| value.reviewRequired === true
|
|
148
|
+
|| value.review_required === true
|
|
149
|
+
|| REVIEW_DECISIONS.has(token)
|
|
150
|
+
) {
|
|
151
|
+
return 'approval_required';
|
|
152
|
+
}
|
|
153
|
+
if (
|
|
154
|
+
value.allowed === true
|
|
155
|
+
|| value.is_safe === true
|
|
156
|
+
|| value.isSafe === true
|
|
157
|
+
|| value.accepted === true
|
|
158
|
+
|| ALLOW_DECISIONS.has(token)
|
|
159
|
+
) {
|
|
160
|
+
return 'allow';
|
|
161
|
+
}
|
|
162
|
+
return 'unknown';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function normalizePolicyDecision(input = {}, options = {}) {
|
|
166
|
+
const value = extractPolicyDecision(input);
|
|
167
|
+
const decision = classifyPolicyDecision(value);
|
|
168
|
+
const source = firstString(
|
|
169
|
+
options.source,
|
|
170
|
+
value.source,
|
|
171
|
+
value.provider,
|
|
172
|
+
value.engine,
|
|
173
|
+
value.policyEngine,
|
|
174
|
+
value.policy_engine,
|
|
175
|
+
'policy-engine'
|
|
176
|
+
);
|
|
177
|
+
const reason = firstString(
|
|
178
|
+
value.reason,
|
|
179
|
+
value.message,
|
|
180
|
+
value.explanation,
|
|
181
|
+
value.summary,
|
|
182
|
+
asArray(value.reasoning).join('; '),
|
|
183
|
+
asArray(value.reasons).join('; '),
|
|
184
|
+
decision === 'unknown' ? 'Policy engine returned an unknown decision; approval required before execution.' : ''
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
allowed: decision === 'allow',
|
|
189
|
+
blocked: decision === 'block',
|
|
190
|
+
approvalRequired: decision === 'approval_required' || decision === 'unknown',
|
|
191
|
+
decision,
|
|
192
|
+
reason,
|
|
193
|
+
source,
|
|
194
|
+
confidence: Number.isFinite(Number(value.confidence)) ? Number(value.confidence) : null,
|
|
195
|
+
policyId: firstString(value.policyId, value.policy_id, value.ruleId, value.rule_id, value.id),
|
|
196
|
+
severity: firstString(value.severity, value.level, value.threat_level, value.threatLevel),
|
|
197
|
+
score: Number.isFinite(Number(value.score))
|
|
198
|
+
? Number(value.score)
|
|
199
|
+
: (Number.isFinite(Number(value.threat_score)) ? Number(value.threat_score) : null),
|
|
200
|
+
evidence: normalizeEvidence(value),
|
|
201
|
+
raw: value,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function normalizePolicyAction(input = {}) {
|
|
206
|
+
const event = asObject(input);
|
|
207
|
+
return {
|
|
208
|
+
...normalizeProviderAction({
|
|
209
|
+
...event,
|
|
210
|
+
provider: firstString(event.provider, event.agentRuntime, event.runtime, 'policy-engine'),
|
|
211
|
+
toolName: firstString(event.toolName, event.tool_name, event.name),
|
|
212
|
+
input: asObject(event.toolInput || event.input || event.arguments || event.args),
|
|
213
|
+
}),
|
|
214
|
+
policyContext: asObject(event.policyContext || event.policy_context),
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function createPolicyEngineGuard({
|
|
219
|
+
policyCheck,
|
|
220
|
+
executeTool,
|
|
221
|
+
gateCheck,
|
|
222
|
+
onDecision,
|
|
223
|
+
source = 'policy-engine',
|
|
224
|
+
} = {}) {
|
|
225
|
+
if (typeof policyCheck !== 'function') {
|
|
226
|
+
throw new TypeError('createPolicyEngineGuard requires a policyCheck function');
|
|
227
|
+
}
|
|
228
|
+
if (typeof executeTool !== 'function') {
|
|
229
|
+
throw new TypeError('createPolicyEngineGuard requires an executeTool function');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return async function guardedPolicyTool(input = {}) {
|
|
233
|
+
const normalizedAction = normalizePolicyAction(input);
|
|
234
|
+
const policyDecision = normalizePolicyDecision(await policyCheck(normalizedAction), { source });
|
|
235
|
+
const gateDecision = typeof gateCheck === 'function'
|
|
236
|
+
? normalizePolicyDecision(await gateCheck({ normalizedAction, policyDecision }), { source: 'thumbgate' })
|
|
237
|
+
: null;
|
|
238
|
+
const effectiveDecision = gateDecision && !gateDecision.allowed ? gateDecision : policyDecision;
|
|
239
|
+
|
|
240
|
+
if (typeof onDecision === 'function') {
|
|
241
|
+
await onDecision({ normalizedAction, policyDecision, gateDecision, effectiveDecision });
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (!effectiveDecision.allowed) {
|
|
245
|
+
const error = new Error(effectiveDecision.reason || 'ThumbGate blocked this action before execution.');
|
|
246
|
+
error.code = effectiveDecision.approvalRequired ? 'THUMBGATE_APPROVAL_REQUIRED' : 'THUMBGATE_BLOCKED';
|
|
247
|
+
error.thumbgate = { normalizedAction, policyDecision, gateDecision, effectiveDecision };
|
|
248
|
+
throw error;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return executeTool(input, { normalizedAction, policyDecision, gateDecision, effectiveDecision });
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
module.exports = {
|
|
256
|
+
createPolicyEngineGuard,
|
|
257
|
+
extractPolicyDecision,
|
|
258
|
+
normalizePolicyAction,
|
|
259
|
+
normalizePolicyDecision,
|
|
260
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"cases": [
|
|
3
|
+
{
|
|
4
|
+
"id": "checkout-link-grounding",
|
|
5
|
+
"traceId": "trace_checkout_001",
|
|
6
|
+
"question": "Is the Pro checkout link working?",
|
|
7
|
+
"response": "The Pro checkout link is working because the checkout diagnostic confirms Stripe Pro checkout is reachable.",
|
|
8
|
+
"retrievedContexts": [
|
|
9
|
+
"Stripe Pro checkout diagnostic confirms the Pro checkout link is reachable and returns a valid checkout target.",
|
|
10
|
+
"The workflow sprint checkout is intake-led and should not expose a stale payment link."
|
|
11
|
+
],
|
|
12
|
+
"reference": "The Pro checkout link is reachable when the Stripe diagnostic passes."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"id": "letta-tool-gate",
|
|
16
|
+
"traceId": "trace_letta_001",
|
|
17
|
+
"question": "Should Letta execute a force push tool call?",
|
|
18
|
+
"response": "ThumbGate blocks force push before execution for Letta tool calls.",
|
|
19
|
+
"retrievedContexts": [
|
|
20
|
+
"ThumbGate's Letta adapter wraps Letta tool calls and runs gate-check before the executor is called.",
|
|
21
|
+
"Force push to main is a blocked high-risk git action."
|
|
22
|
+
],
|
|
23
|
+
"reference": "ThumbGate blocks high-risk Letta tool calls before execution."
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
}
|
package/bin/cli.js
CHANGED
|
@@ -3934,6 +3934,7 @@ switch (COMMAND) {
|
|
|
3934
3934
|
// PreToolUse hook interface: reads tool call JSON from stdin, outputs gate verdict
|
|
3935
3935
|
// Used by: generate-pretool-hook.sh → npx thumbgate gate-check
|
|
3936
3936
|
const { run: gateRun, runAsync: gateRunAsync } = require(path.join(PKG_ROOT, 'scripts', 'gates-engine'));
|
|
3937
|
+
const { evaluateSelfProtection } = require(path.join(PKG_ROOT, 'scripts', 'self-protection'));
|
|
3937
3938
|
let stdinData = '';
|
|
3938
3939
|
process.stdin.setEncoding('utf8');
|
|
3939
3940
|
process.stdin.on('data', (chunk) => { stdinData += chunk; });
|
|
@@ -3941,7 +3942,32 @@ switch (COMMAND) {
|
|
|
3941
3942
|
try {
|
|
3942
3943
|
const input = JSON.parse(stdinData);
|
|
3943
3944
|
const output = await gateRunAsync(input);
|
|
3944
|
-
|
|
3945
|
+
// Self-protection overlay (2026-07-08): the gate engine only denies edits
|
|
3946
|
+
// to ThumbGate's own governance files under strict enforcement; by default
|
|
3947
|
+
// such edits pass as a clean ALLOW. Surface them (or block in strict) so an
|
|
3948
|
+
// agent can't silently disable the firewall. Only overlays when the engine
|
|
3949
|
+
// did not already produce a hard deny.
|
|
3950
|
+
let verdict = output;
|
|
3951
|
+
try {
|
|
3952
|
+
const parsed = JSON.parse(output || '{}');
|
|
3953
|
+
const alreadyDeny = (parsed.hookSpecificOutput && parsed.hookSpecificOutput.permissionDecision === 'deny')
|
|
3954
|
+
|| parsed.decision === 'block';
|
|
3955
|
+
if (!alreadyDeny) {
|
|
3956
|
+
const sp = evaluateSelfProtection(input.tool_name, input.tool_input);
|
|
3957
|
+
if (sp && sp.action === 'block') {
|
|
3958
|
+
verdict = JSON.stringify({
|
|
3959
|
+
decision: 'block',
|
|
3960
|
+
reason: sp.message,
|
|
3961
|
+
hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: sp.message },
|
|
3962
|
+
});
|
|
3963
|
+
} else if (sp && sp.action === 'warn') {
|
|
3964
|
+
verdict = JSON.stringify({
|
|
3965
|
+
hookSpecificOutput: { hookEventName: 'PreToolUse', additionalContext: sp.message },
|
|
3966
|
+
});
|
|
3967
|
+
}
|
|
3968
|
+
}
|
|
3969
|
+
} catch (_e) { /* non-JSON engine output: pass through unchanged */ }
|
|
3970
|
+
process.stdout.write(verdict + '\n');
|
|
3945
3971
|
process.exit(0);
|
|
3946
3972
|
} catch (err) {
|
|
3947
3973
|
process.stderr.write(`gate-check error: ${err.message}\n`);
|
package/bin/postinstall.js
CHANGED
|
@@ -15,41 +15,32 @@ if (isCI || isQuiet) process.exit(0);
|
|
|
15
15
|
const {
|
|
16
16
|
PRO_MONTHLY_PAYMENT_LINK,
|
|
17
17
|
PRO_PRICE_LABEL,
|
|
18
|
+
TEAM_PRICE_LABEL,
|
|
18
19
|
} = require('../scripts/commercial-offer');
|
|
19
20
|
|
|
20
21
|
// Tracked click-through path: /go/pro → /checkout/pro → Stripe.
|
|
22
|
+
// This captures UTM attribution in our funnel before handing off to Stripe.
|
|
21
23
|
const PRO_CTA_URL = 'https://thumbgate.ai/go/pro?utm_source=npm&utm_medium=postinstall&utm_campaign=first_dollar';
|
|
24
|
+
const WORKFLOW_SPRINT_URL = 'https://thumbgate.ai/#workflow-sprint-intake';
|
|
22
25
|
const DASHBOARD_URL = 'https://thumbgate.ai/dashboard?utm_source=npm&utm_medium=postinstall&utm_campaign=dashboard_nudge';
|
|
23
26
|
|
|
24
27
|
process.stderr.write(`
|
|
25
28
|
╭─────────────────────────────────────────────────────╮
|
|
26
|
-
│ ThumbGate installed.
|
|
29
|
+
│ ThumbGate installed — 7-day Pro trial is live. │
|
|
27
30
|
│ │
|
|
28
|
-
│
|
|
29
|
-
│
|
|
30
|
-
│ BEFORE the model sees them — zero tokens spent │
|
|
31
|
-
│ on mistakes you've already corrected. │
|
|
31
|
+
│ Start now: npx thumbgate init │
|
|
32
|
+
│ Updates: npx thumbgate subscribe you@company.com│
|
|
32
33
|
│ │
|
|
33
|
-
│
|
|
34
|
-
│
|
|
35
|
-
│ npx thumbgate stats │
|
|
36
|
-
│ │
|
|
37
|
-
│ See your gates firing live: │
|
|
38
|
-
│ ${DASHBOARD_URL.slice(0, 47).padEnd(47, ' ')} │
|
|
34
|
+
│ Free after trial: 3 rules, 2 captures/day. │
|
|
35
|
+
│ Pro ($19/mo): unlimited everything. │
|
|
39
36
|
╰─────────────────────────────────────────────────────╯
|
|
40
37
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
Org dashboard. DPO export. 24×7 ops.
|
|
46
|
-
Upgrade: ${PRO_CTA_URL}
|
|
47
|
-
Direct: ${PRO_MONTHLY_PAYMENT_LINK}
|
|
48
|
-
|
|
49
|
-
⚡ LAUNCH PROMO: 50% off first 3 months with code THUMB50
|
|
50
|
-
One-click (discount applied): https://buy.stripe.com/eVqeVd62o0aO19v7mb3sI3g
|
|
51
|
-
Limited to first 50 customers. July 2026
|
|
38
|
+
Trial unlocks: unlimited rules, lesson search, DPO export,
|
|
39
|
+
hosted dashboard. After 7 days, free tier limits apply.
|
|
40
|
+
Subscribe for the 5-min setup guide + weekly tips:
|
|
41
|
+
npx thumbgate subscribe you@company.com
|
|
52
42
|
|
|
53
|
-
|
|
43
|
+
Dashboard: ${DASHBOARD_URL}
|
|
44
|
+
Keep Pro: ${PRO_CTA_URL}
|
|
54
45
|
|
|
55
46
|
`);
|
|
@@ -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,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
|
+
|