thumbgate 1.27.12 → 1.27.14
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/llms.txt +2 -1
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +2 -4
- package/adapters/claude/.mcp.json +2 -2
- 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/bin/cli.js +78 -259
- package/config/gate-templates.json +0 -228
- package/config/gates/claim-verification.json +0 -18
- package/package.json +35 -25
- package/public/assets/brand/thumbgate-logo-transparent.svg +22 -0
- package/public/assets/brand/thumbgate-mark-inline-v3.svg +19 -0
- package/public/assets/brand/thumbgate-mark.svg +11 -5
- package/public/blog.html +0 -30
- package/public/brand/thumbgate-mark.svg +9 -5
- package/public/chatgpt-app.html +2 -2
- package/public/compare.html +2 -1
- package/public/dashboard.html +1 -1
- package/public/federal.html +1 -1
- package/public/index.html +95 -216
- package/public/learn.html +59 -35
- package/public/lessons.html +1 -1
- package/public/numbers.html +2 -2
- package/public/pro.html +7 -7
- package/scripts/agent-readiness.js +142 -0
- package/scripts/aws-blocks-guardrails.js +228 -0
- package/scripts/cli-schema.js +22 -10
- package/scripts/dashboard-chat.js +2 -1
- package/scripts/document-intake.js +1 -49
- package/scripts/durability/step.js +3 -3
- package/scripts/gate-stats.js +5 -11
- package/scripts/gates-engine.js +0 -49
- package/scripts/gemini-embedding-policy.js +2 -1
- package/scripts/hook-stop-anti-claim.js +116 -184
- package/scripts/hosted-config.js +0 -12
- package/scripts/lesson-search.js +1 -15
- package/scripts/llm-client.js +187 -5
- package/scripts/plausible-domain-config.js +3 -1
- package/scripts/seo-gsd.js +240 -1
- package/scripts/tool-registry.js +2 -2
- package/scripts/vector-store.js +44 -0
- package/scripts/workspace-evolver.js +62 -2
- package/src/api/server.js +340 -131
- package/public/assets/brand/thumbgate-mark-inline.svg +0 -15
- package/public/compare/adopt-ai.html +0 -219
- package/public/compare/agentix-labs.html +0 -197
- package/public/compare/ai-experience-orchestration.html +0 -216
- package/public/compare/anthropic-claude-for-legal.html +0 -260
- package/public/compare/anthropic-containment.html +0 -280
- package/public/compare/arcade.html +0 -175
- package/public/compare/arcjet.html +0 -239
- package/public/compare/bumblebee.html +0 -307
- package/public/compare/claude-code-hooks.html +0 -294
- package/public/compare/databricks-unity-ai-gateway.html +0 -215
- package/public/compare/fallow.html +0 -351
- package/public/compare/heidi.html +0 -233
- package/public/compare/mem0.html +0 -342
- package/public/compare/oak-and-sparrow-gatekeeper.html +0 -289
- package/public/compare/rein.html +0 -236
- package/public/compare/sigmashake.html +0 -256
- package/public/compare/speclock.html +0 -342
- package/public/guides/agent-harness-optimization.html +0 -342
- package/public/guides/agentic-web-governance.html +0 -406
- package/public/guides/ai-agent-governance-sprint.html +0 -415
- package/public/guides/ai-agent-pre-action-approval-gates.html +0 -401
- package/public/guides/ai-agent-workflow-migration-checklist.html +0 -392
- package/public/guides/ai-deployment-readiness.html +0 -415
- package/public/guides/ai-mode-ads-agent-governance.html +0 -401
- package/public/guides/ai-search-topical-presence.html +0 -342
- package/public/guides/autoresearch-agent-safety.html +0 -342
- package/public/guides/background-agent-governance.html +0 -358
- package/public/guides/best-tools-stop-ai-agents-breaking-production.html +0 -363
- package/public/guides/browser-automation-safety.html +0 -342
- package/public/guides/chatgpt-ads-trust.html +0 -353
- package/public/guides/claude-code-feedback.html +0 -339
- package/public/guides/claude-code-prevent-repeated-mistakes.html +0 -161
- package/public/guides/claude-code-skills-guardrails.html +0 -343
- package/public/guides/claude-desktop.html +0 -356
- package/public/guides/code-knowledge-graph-guardrails.html +0 -365
- package/public/guides/codex-cli-guardrails.html +0 -339
- package/public/guides/cursor-agent-guardrails.html +0 -339
- package/public/guides/cursor-prevent-repeated-mistakes.html +0 -161
- package/public/guides/database-agent-safety.html +0 -406
- package/public/guides/deepseek-v4-runtime-guardrails.html +0 -346
- package/public/guides/developer-machine-supply-chain-guardrails.html +0 -358
- package/public/guides/gcp-mcp-guardrails.html +0 -147
- package/public/guides/gemini-cli-feedback-memory.html +0 -339
- package/public/guides/gpt-5-5-model-evaluation.html +0 -358
- package/public/guides/internal-ai-engineering-stack-guardrails.html +0 -348
- package/public/guides/long-running-agent-context-management.html +0 -346
- package/public/guides/mcp-tool-governance.html +0 -401
- package/public/guides/multica-thumbgate-setup.html +0 -134
- package/public/guides/native-messaging-host-security.html +0 -342
- package/public/guides/policy-engine-pre-action-gates.html +0 -346
- package/public/guides/pre-action-checks.html +0 -342
- package/public/guides/pretooluse-hooks-vs-advisory-prompt-rules.html +0 -342
- package/public/guides/prompt-tricks-to-workflow-rules.html +0 -365
- package/public/guides/proxy-pointer-rag-guardrails.html +0 -352
- package/public/guides/rag-precision-tuning-guardrails.html +0 -352
- package/public/guides/reasoning-compression-guardrails.html +0 -346
- package/public/guides/relational-knowledge-ai-recommendations.html +0 -342
- package/public/guides/roo-code-alternative-cline.html +0 -339
- package/public/guides/semantic-programmatic-seo-guardrails.html +0 -352
- package/public/guides/seo-agent-skills-guardrails.html +0 -344
- package/public/guides/stop-repeated-ai-agent-mistakes.html +0 -342
- package/public/learn/ac-dc-runtime-enforcement.html +0 -277
- package/public/learn/agent-harness-pattern.html +0 -181
- package/public/learn/agent-identity-connector-governance.html +0 -146
- package/public/learn/agent-swarms-shared-gates.html +0 -173
- package/public/learn/agentic-enterprise-context-brain.html +0 -117
- package/public/learn/agentic-os-team-governance.html +0 -146
- package/public/learn/ai-agent-governance.html +0 -158
- package/public/learn/ai-agent-persistent-memory.html +0 -211
- package/public/learn/anthropomorphic-claim-gates.html +0 -180
- package/public/learn/background-agent-control-layer.html +0 -184
- package/public/learn/claude-code-goal-with-rubrics.html +0 -205
- package/public/learn/codex-role-plugins-need-governance.html +0 -125
- package/public/learn/cost-aware-agent-gate-routing.html +0 -173
- package/public/learn/databricks-unity-ai-gateway-runtime-governance.html +0 -157
- package/public/learn/deterministic-agent-workflows.html +0 -185
- package/public/learn/feedback-loop-vs-decision-layer.html +0 -283
- package/public/learn/from-prototype-to-production.html +0 -223
- package/public/learn/learn.css +0 -51
- package/public/learn/mcp-pre-action-checks-explained.html +0 -172
- package/public/learn/pretix-stripe-connect-marketplaces.html +0 -161
- package/public/learn/regulated-agent-execution-boundary.html +0 -196
- package/public/learn/spec-driven-development.html +0 -168
- package/public/learn/stop-ai-agent-force-push.html +0 -134
- package/public/learn/vibe-coding-safety-net.html +0 -142
- package/scripts/reddit-browser-notification-watch.js +0 -230
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
|
|
7
|
+
const AWS_BLOCKS_DEPENDENCY_PATTERN = /(^|\/)@aws-blocks\/(?:blocks|[^/\s"']+)/;
|
|
8
|
+
|
|
9
|
+
function toList(value) {
|
|
10
|
+
if (Array.isArray(value)) return value.map(String).map((entry) => entry.trim()).filter(Boolean);
|
|
11
|
+
if (typeof value === 'string') return value.split(',').map((entry) => entry.trim()).filter(Boolean);
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function normalizeText(value) {
|
|
16
|
+
if (value == null) return '';
|
|
17
|
+
if (typeof value === 'string') return value;
|
|
18
|
+
try {
|
|
19
|
+
return JSON.stringify(value);
|
|
20
|
+
} catch {
|
|
21
|
+
return String(value);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function readPackageJson(projectDir) {
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(fs.readFileSync(path.join(projectDir, 'package.json'), 'utf8'));
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function packageUsesAwsBlocks(pkg) {
|
|
34
|
+
if (!pkg || typeof pkg !== 'object') return false;
|
|
35
|
+
const dependencyText = JSON.stringify({
|
|
36
|
+
dependencies: pkg.dependencies || {},
|
|
37
|
+
devDependencies: pkg.devDependencies || {},
|
|
38
|
+
peerDependencies: pkg.peerDependencies || {},
|
|
39
|
+
optionalDependencies: pkg.optionalDependencies || {},
|
|
40
|
+
});
|
|
41
|
+
const scriptsText = JSON.stringify(pkg.scripts || {});
|
|
42
|
+
return AWS_BLOCKS_DEPENDENCY_PATTERN.test(dependencyText) || /aws-blocks|blocks-app|cdk|sandbox/i.test(scriptsText);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function detectAwsBlocksProject(projectDir = process.cwd()) {
|
|
46
|
+
const pkg = readPackageJson(projectDir);
|
|
47
|
+
if (packageUsesAwsBlocks(pkg)) return true;
|
|
48
|
+
return fs.existsSync(path.join(projectDir, 'aws-blocks', 'index.ts'))
|
|
49
|
+
|| fs.existsSync(path.join(projectDir, 'aws-blocks', 'index.js'))
|
|
50
|
+
|| fs.existsSync(path.join(projectDir, 'blocks.spec.json'));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function detectAction(input = {}) {
|
|
54
|
+
const command = normalizeText(input.command || input.args || input.shell || input.toolInput);
|
|
55
|
+
const toolName = normalizeText(input.toolName || input.tool || input.name);
|
|
56
|
+
const code = normalizeText(input.code || input.file || input.diff || input.body);
|
|
57
|
+
const combined = `${toolName}\n${command}\n${code}`;
|
|
58
|
+
const lower = combined.toLowerCase();
|
|
59
|
+
|
|
60
|
+
const signals = [];
|
|
61
|
+
const add = (id, label, severity = 'medium') => signals.push({ id, label, severity });
|
|
62
|
+
|
|
63
|
+
if (/\b(cdk\s+deploy|npm\s+run\s+deploy|pnpm\s+deploy|yarn\s+deploy|sst\s+deploy|blocks?\s+deploy)\b/i.test(combined)) {
|
|
64
|
+
add('aws-blocks-production-deploy', 'production deploy from an AWS Blocks workflow', 'high');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (/\b(cdk\s+destroy|npm\s+run\s+destroy|pnpm\s+destroy|yarn\s+destroy|sandbox\b.*--destroy|--destroy\b|aws\s+cloudformation\s+delete-stack)\b/i.test(combined)) {
|
|
68
|
+
add('aws-blocks-destroy', 'destroy command can remove AWS resources created from local Blocks code', 'critical');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (/\b(drop\s+(table|database|schema|index|column)|truncate\s+table|delete\s+from\s+[\w".-]+(?:\s*;|\s*$)|update\s+[\w".-]+\s+set\b(?![\s\S]{0,160}\bwhere\b))/i.test(combined)) {
|
|
72
|
+
add('destructive-sql-or-ddl', 'destructive or unscoped SQL mutation', 'critical');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (/\b(aws\s+dynamodb\s+delete-table|aws\s+rds\s+delete-db-instance|aws\s+s3\s+rm\b[\s\S]*--recursive|aws\s+lambda\s+delete-function|aws\s+bedrock-agent\s+delete-|aws\s+bedrock-agent-runtime\b)/i.test(combined)) {
|
|
76
|
+
add('destructive-aws-cli', 'destructive AWS CLI or Bedrock agent action', 'critical');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (/\b(gcloud|aws)\b[\s\S]{0,220}\b(add-iam-policy-binding|put-role-policy|attach-role-policy|create-policy-version)\b/i.test(combined)
|
|
80
|
+
|| /\b(AdministratorAccess|roles\/owner|iam:PassRole|serviceAccountTokenCreator)\b/i.test(combined)) {
|
|
81
|
+
add('iam-escalation', 'agent session attempts to grant broad IAM authority', 'critical');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (/\b(new\s+Agent\s*\(|@aws-blocks\/(?:agent|blocks)|\bAgent\b[\s\S]{0,120}\btools?\b)/.test(combined)) {
|
|
85
|
+
add('blocks-agent-tool-call', 'AWS Blocks Agent or Bedrock-style tool action needs a tool boundary', 'medium');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (/\b(npm\s+run\s+dev|npm\s+start|pnpm\s+dev|yarn\s+dev|create-blocks-app)\b/i.test(combined)) {
|
|
89
|
+
add('local-dev', 'local AWS Blocks development loop', 'low');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
command,
|
|
94
|
+
toolName,
|
|
95
|
+
code,
|
|
96
|
+
signals,
|
|
97
|
+
highRisk: signals.some((signal) => ['high', 'critical'].includes(signal.severity)),
|
|
98
|
+
localOnly: signals.some((signal) => signal.id === 'local-dev') && signals.every((signal) => signal.severity === 'low'),
|
|
99
|
+
lower,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function evaluateAwsBlocksAction(input = {}) {
|
|
104
|
+
const projectDir = input.projectDir || input.cwd || process.cwd();
|
|
105
|
+
const projectUsesAwsBlocks = Boolean(
|
|
106
|
+
input.projectUsesAwsBlocks
|
|
107
|
+
|| input.awsBlocks
|
|
108
|
+
|| input.blocksProject
|
|
109
|
+
|| detectAwsBlocksProject(projectDir)
|
|
110
|
+
);
|
|
111
|
+
const evidence = new Set(toList(input.evidence || input.proof || input.receipts));
|
|
112
|
+
const action = detectAction(input);
|
|
113
|
+
const requiredEvidence = [];
|
|
114
|
+
|
|
115
|
+
const requireEvidence = (id, label) => {
|
|
116
|
+
if (!evidence.has(id)) requiredEvidence.push({ id, label });
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
if (projectUsesAwsBlocks && action.signals.some((signal) => signal.id === 'aws-blocks-production-deploy')) {
|
|
120
|
+
requireEvidence('local-tests-pass', 'local AWS Blocks tests pass against local implementations');
|
|
121
|
+
requireEvidence('cdk-diff-reviewed', 'CDK diff or synthesized CloudFormation change set reviewed');
|
|
122
|
+
requireEvidence('cost-blast-radius-reviewed', 'AWS cost and resource blast radius reviewed');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (action.signals.some((signal) => signal.id === 'aws-blocks-destroy')) {
|
|
126
|
+
requireEvidence('resource-inventory-exported', 'resource inventory exported before destroy');
|
|
127
|
+
requireEvidence('human-destroy-approval', 'named human approval for destroy');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (action.signals.some((signal) => signal.id === 'destructive-sql-or-ddl')) {
|
|
131
|
+
requireEvidence('backup-or-rollback-ready', 'backup, rollback, or restore point exists');
|
|
132
|
+
requireEvidence('bounded-row-count-reviewed', 'row/table impact was previewed before mutation');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (action.signals.some((signal) => signal.id === 'destructive-aws-cli')) {
|
|
136
|
+
requireEvidence('aws-account-and-region-confirmed', 'target AWS account and region confirmed');
|
|
137
|
+
requireEvidence('rollback-plan-attached', 'rollback or recovery plan attached');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (action.signals.some((signal) => signal.id === 'iam-escalation')) {
|
|
141
|
+
requireEvidence('least-privilege-review', 'least-privilege review completed');
|
|
142
|
+
requireEvidence('security-owner-approval', 'security owner approval captured');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (action.signals.some((signal) => signal.id === 'blocks-agent-tool-call')) {
|
|
146
|
+
requireEvidence('agent-tool-allowlist', 'agent tool allowlist and data boundary declared');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const shouldBlock = projectUsesAwsBlocks && requiredEvidence.length > 0;
|
|
150
|
+
const status = shouldBlock ? 'blocked' : (action.highRisk ? 'needs-review' : 'allowed');
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
name: 'thumbgate-aws-blocks-guardrails',
|
|
154
|
+
status,
|
|
155
|
+
projectUsesAwsBlocks,
|
|
156
|
+
signals: action.signals,
|
|
157
|
+
requiredEvidence,
|
|
158
|
+
enforcementBoundary: 'local AWS Blocks confidence must not automatically become production AWS authority',
|
|
159
|
+
gates: [
|
|
160
|
+
'allow local AWS Blocks dev loops without AWS credentials',
|
|
161
|
+
'require local test proof plus CDK diff before production deploy',
|
|
162
|
+
'block destroy and destructive data mutations until backup, blast-radius, and human approval evidence exists',
|
|
163
|
+
'block IAM escalation and Bedrock/Agent tool expansion until an owner approves the tool boundary',
|
|
164
|
+
'write a ThumbGate receipt for every allowed high-risk cloud action',
|
|
165
|
+
],
|
|
166
|
+
nextActions: shouldBlock
|
|
167
|
+
? requiredEvidence.map((item) => `Provide ${item.id}: ${item.label}`)
|
|
168
|
+
: ['Record an allow receipt when this action touches real AWS resources'],
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function buildAwsBlocksHardeningOffer(input = {}) {
|
|
173
|
+
const workflow = String(input.workflow || input.name || 'AWS Blocks backend').trim();
|
|
174
|
+
const buyer = String(input.buyer || input.owner || 'platform owner').trim();
|
|
175
|
+
return {
|
|
176
|
+
name: 'thumbgate-aws-blocks-hardening-offer',
|
|
177
|
+
status: 'ready-for-positioning',
|
|
178
|
+
buyer,
|
|
179
|
+
workflow,
|
|
180
|
+
headline: 'AWS Blocks helps agents build the backend. ThumbGate stops them before unsafe cloud actions run.',
|
|
181
|
+
offer: 'AWS Blocks Agent Safety Review',
|
|
182
|
+
diagnosticPrice: '$499',
|
|
183
|
+
proofPlan: [
|
|
184
|
+
'map one AWS Blocks local-to-cloud workflow',
|
|
185
|
+
'install ThumbGate against the agent running that workflow',
|
|
186
|
+
'add gates for deploy, destroy, data mutation, IAM, Bedrock Agent, and cost-blast-radius actions',
|
|
187
|
+
'produce a receipt showing the first blocked repeat and the evidence required to allow it',
|
|
188
|
+
],
|
|
189
|
+
cta: 'Send one AWS Blocks workflow that is about to deploy, mutate data, or call Bedrock tools.',
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function parseArgs(argv = process.argv.slice(2)) {
|
|
194
|
+
const args = {};
|
|
195
|
+
for (const arg of argv) {
|
|
196
|
+
if (arg === '--json') args.json = true;
|
|
197
|
+
else if (arg === '--aws-blocks' || arg === '--project-uses-aws-blocks') args.projectUsesAwsBlocks = true;
|
|
198
|
+
else if (arg.startsWith('--command=')) args.command = arg.slice('--command='.length);
|
|
199
|
+
else if (arg.startsWith('--tool=')) args.toolName = arg.slice('--tool='.length);
|
|
200
|
+
else if (arg.startsWith('--code=')) args.code = arg.slice('--code='.length);
|
|
201
|
+
else if (arg.startsWith('--cwd=')) args.projectDir = arg.slice('--cwd='.length);
|
|
202
|
+
else if (arg.startsWith('--evidence=')) args.evidence = arg.slice('--evidence='.length);
|
|
203
|
+
else if (arg.startsWith('--workflow=')) args.workflow = arg.slice('--workflow='.length);
|
|
204
|
+
else if (arg.startsWith('--buyer=')) args.buyer = arg.slice('--buyer='.length);
|
|
205
|
+
else if (arg === 'offer') args.commandName = 'offer';
|
|
206
|
+
}
|
|
207
|
+
return args;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function runCli(args = parseArgs()) {
|
|
211
|
+
const report = args.commandName === 'offer'
|
|
212
|
+
? buildAwsBlocksHardeningOffer(args)
|
|
213
|
+
: evaluateAwsBlocksAction(args);
|
|
214
|
+
if (args.json) console.log(JSON.stringify(report, null, 2));
|
|
215
|
+
else {
|
|
216
|
+
console.log(`${report.name}: ${report.status}`);
|
|
217
|
+
for (const action of report.nextActions || report.proofPlan || []) console.log(`- ${action}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (require.main === module) runCli();
|
|
222
|
+
|
|
223
|
+
module.exports = {
|
|
224
|
+
detectAwsBlocksProject,
|
|
225
|
+
detectAction,
|
|
226
|
+
evaluateAwsBlocksAction,
|
|
227
|
+
buildAwsBlocksHardeningOffer,
|
|
228
|
+
};
|
package/scripts/cli-schema.js
CHANGED
|
@@ -123,16 +123,6 @@ const CLI_COMMANDS = [
|
|
|
123
123
|
{ name: 'remote', type: 'boolean', description: 'Fetch from hosted Railway instance' },
|
|
124
124
|
],
|
|
125
125
|
},
|
|
126
|
-
{
|
|
127
|
-
name: 'community',
|
|
128
|
-
aliases: ['registry'],
|
|
129
|
-
description: 'Query or share verified prevention rules with the community knowledge registry',
|
|
130
|
-
group: 'discovery',
|
|
131
|
-
flags: [
|
|
132
|
-
{ name: 'json', type: 'boolean', description: 'Output as JSON' },
|
|
133
|
-
{ name: 'remote', type: 'boolean', description: 'Fetch from community remote API' },
|
|
134
|
-
],
|
|
135
|
-
},
|
|
136
126
|
{
|
|
137
127
|
name: 'gate-stats',
|
|
138
128
|
description: 'Check engine statistics — active checks, blocks, warns, time saved',
|
|
@@ -515,6 +505,12 @@ const CLI_COMMANDS = [
|
|
|
515
505
|
group: 'gates',
|
|
516
506
|
flags: [],
|
|
517
507
|
},
|
|
508
|
+
{
|
|
509
|
+
name: 'hermes-gate',
|
|
510
|
+
description: 'Hermes Agent pre_tool_call hook: gate runtime tool calls (incl. skill_manage) before they run',
|
|
511
|
+
group: 'gates',
|
|
512
|
+
flags: [],
|
|
513
|
+
},
|
|
518
514
|
{
|
|
519
515
|
name: 'force-gate',
|
|
520
516
|
description: 'Immediately create a blocking gate from a pattern string',
|
|
@@ -660,6 +656,22 @@ const CLI_COMMANDS = [
|
|
|
660
656
|
{ name: 'json', type: 'boolean', description: 'Output results as JSON' },
|
|
661
657
|
],
|
|
662
658
|
},
|
|
659
|
+
{
|
|
660
|
+
name: 'check-update',
|
|
661
|
+
aliases: ['upgrade-check'],
|
|
662
|
+
description: 'Check for newer versions of ThumbGate from npm or GitHub',
|
|
663
|
+
group: 'ops',
|
|
664
|
+
flags: [
|
|
665
|
+
{ name: 'json', type: 'boolean', description: 'Output results as JSON' },
|
|
666
|
+
],
|
|
667
|
+
},
|
|
668
|
+
{
|
|
669
|
+
name: 'self-update',
|
|
670
|
+
aliases: ['upgrade-cli'],
|
|
671
|
+
description: 'Automatically install the latest version of ThumbGate globally',
|
|
672
|
+
group: 'ops',
|
|
673
|
+
flags: [],
|
|
674
|
+
},
|
|
663
675
|
];
|
|
664
676
|
|
|
665
677
|
/**
|
|
@@ -317,7 +317,8 @@ async function answerDataQuestion(question, opts = {}) {
|
|
|
317
317
|
if (isPerplexity) return await callPerplexityEndpoint({ apiKey, prompt, fetchImpl, sources });
|
|
318
318
|
return await callGeminiEndpoint({ apiKey, model, prompt, fetchImpl, sources });
|
|
319
319
|
} catch (err) {
|
|
320
|
-
|
|
320
|
+
const safeMessage = (err && err.message) ? String(err.message).split('\n')[0].slice(0, 100) : 'An unexpected error occurred.';
|
|
321
|
+
return { ok: false, error: 'network', message: safeMessage, sources };
|
|
321
322
|
}
|
|
322
323
|
}
|
|
323
324
|
|
|
@@ -708,7 +708,6 @@ function buildDocumentSummary(document) {
|
|
|
708
708
|
sourcePath: document.sourcePath || null,
|
|
709
709
|
sourceName: document.sourceName || null,
|
|
710
710
|
sourceFormat: document.sourceFormat,
|
|
711
|
-
sourceUrl: document.sourceUrl || null,
|
|
712
711
|
importedAt: document.importedAt,
|
|
713
712
|
tags: normalizeTags(document.tags),
|
|
714
713
|
excerpt: document.excerpt,
|
|
@@ -769,11 +768,7 @@ function persistDocument(document, options = {}) {
|
|
|
769
768
|
const summaries = listImportedDocuments({
|
|
770
769
|
...options,
|
|
771
770
|
limit: MAX_SEARCH_SCAN,
|
|
772
|
-
}).documents.filter((entry) =>
|
|
773
|
-
if (entry.documentId === document.documentId) return false;
|
|
774
|
-
if (document.sourceUrl && entry.sourceUrl === document.sourceUrl) return false;
|
|
775
|
-
return true;
|
|
776
|
-
});
|
|
771
|
+
}).documents.filter((entry) => entry.documentId !== document.documentId);
|
|
777
772
|
const nextSummaries = [
|
|
778
773
|
buildDocumentSummary(document),
|
|
779
774
|
...summaries,
|
|
@@ -887,48 +882,6 @@ function importDocument(options = {}) {
|
|
|
887
882
|
sourceFormat,
|
|
888
883
|
});
|
|
889
884
|
const fingerprint = sha256(`${title}\n${normalizedContent}`);
|
|
890
|
-
|
|
891
|
-
// -- deduplication and RAG drift tracking ----------------------------------
|
|
892
|
-
const paths = getDocumentStorePaths(options);
|
|
893
|
-
let duplicate = null;
|
|
894
|
-
if (fs.existsSync(paths.catalogPath)) {
|
|
895
|
-
try {
|
|
896
|
-
const catalog = readJsonl(paths.catalogPath);
|
|
897
|
-
const urlMatch = options.sourceUrl ? String(options.sourceUrl).trim() : null;
|
|
898
|
-
const matchedSummary = catalog.find((summary) =>
|
|
899
|
-
(urlMatch && summary.sourceUrl === urlMatch) ||
|
|
900
|
-
(summary.fingerprint === fingerprint)
|
|
901
|
-
);
|
|
902
|
-
if (matchedSummary) {
|
|
903
|
-
const fullDoc = readImportedDocument(matchedSummary.documentId, options);
|
|
904
|
-
if (fullDoc) {
|
|
905
|
-
if (fullDoc.fingerprint === fingerprint) {
|
|
906
|
-
// Case A: Content is identical
|
|
907
|
-
const dedupReason = urlMatch && fullDoc.sourceUrl === urlMatch
|
|
908
|
-
? 'url-and-content-unchanged'
|
|
909
|
-
: 'content-identical';
|
|
910
|
-
return {
|
|
911
|
-
...fullDoc,
|
|
912
|
-
duplicate: true,
|
|
913
|
-
updated: false,
|
|
914
|
-
dedupReason,
|
|
915
|
-
};
|
|
916
|
-
} else {
|
|
917
|
-
// Case B: URL matches but content changed (RAG Drift!)
|
|
918
|
-
duplicate = {
|
|
919
|
-
previousDocumentId: fullDoc.documentId,
|
|
920
|
-
previousFingerprint: fullDoc.fingerprint,
|
|
921
|
-
updated: true,
|
|
922
|
-
dedupReason: 'url-content-updated',
|
|
923
|
-
};
|
|
924
|
-
}
|
|
925
|
-
}
|
|
926
|
-
}
|
|
927
|
-
} catch (err) {
|
|
928
|
-
// best-effort
|
|
929
|
-
}
|
|
930
|
-
}
|
|
931
|
-
|
|
932
885
|
const importedAt = nowIso();
|
|
933
886
|
const sourceName = sourcePath ? path.basename(sourcePath) : null;
|
|
934
887
|
const documentId = `doc_${slugify(title || sourceName || 'document').slice(0, 24) || 'document'}_${fingerprint.slice(0, 12)}`;
|
|
@@ -948,7 +901,6 @@ function importDocument(options = {}) {
|
|
|
948
901
|
contentBytes: Buffer.byteLength(normalizedContent, 'utf8'),
|
|
949
902
|
lineCount: normalizedContent.split('\n').filter(Boolean).length,
|
|
950
903
|
headings: extractHeadings(normalizedContent),
|
|
951
|
-
...(duplicate || {}),
|
|
952
904
|
};
|
|
953
905
|
document.proposals = options.proposeGates === false
|
|
954
906
|
? []
|
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
* the full durable-execution runtime. Gives each external call (HTTP,
|
|
8
8
|
* LanceDB, LLM) a uniform retry + idempotency wrapper:
|
|
9
9
|
*
|
|
10
|
-
* const result = await runStep('
|
|
10
|
+
* const result = await runStep('directSocial.publishPost', {
|
|
11
11
|
* retries: 3,
|
|
12
12
|
* idempotencyKey: idempotencyKey(content, platforms),
|
|
13
13
|
* }, async ({ attempt }) => {
|
|
14
|
-
* return
|
|
14
|
+
* return directSocialFetch('POST', '/posts', body, { idempotencyKey: ... });
|
|
15
15
|
* });
|
|
16
16
|
*
|
|
17
17
|
* Why a custom helper instead of Vercel Workflows / Temporal / Inngest?
|
|
@@ -98,7 +98,7 @@ function idempotencyKey(...parts) {
|
|
|
98
98
|
* `fn` resolves to, or throws the last error after exhausting retries /
|
|
99
99
|
* hitting a non-retryable verdict.
|
|
100
100
|
*
|
|
101
|
-
* @param {string} name Step name, used in logs. e.g. '
|
|
101
|
+
* @param {string} name Step name, used in logs. e.g. 'directSocial.publishPost'.
|
|
102
102
|
* @param {object|function} options { retries, backoffMs, classify, onRetry, onFail, logger }
|
|
103
103
|
* (may be passed directly as the callback shorthand)
|
|
104
104
|
* @param {function({attempt:number}):Promise} fn The actual work.
|
package/scripts/gate-stats.js
CHANGED
|
@@ -11,11 +11,6 @@ const PROJECT_ROOT = path.join(__dirname, '..');
|
|
|
11
11
|
const MANUAL_GATES_PATH = path.join(PROJECT_ROOT, 'config', 'gates', 'default.json');
|
|
12
12
|
const STATS_PATH = path.join(process.env.HOME || '/tmp', '.thumbgate', 'gate-stats.json');
|
|
13
13
|
|
|
14
|
-
function safeOccurrenceCount(value) {
|
|
15
|
-
const n = Number(value);
|
|
16
|
-
return Number.isFinite(n) && n > 0 ? n : 0;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
14
|
function loadGatesFile(filePath) {
|
|
20
15
|
if (!fs.existsSync(filePath)) return [];
|
|
21
16
|
try {
|
|
@@ -44,16 +39,16 @@ function calculateStats() {
|
|
|
44
39
|
// Count total blocks/warns from occurrences in auto-promoted gates
|
|
45
40
|
const totalBlocked = autoGates
|
|
46
41
|
.filter((g) => g.action === 'block')
|
|
47
|
-
.reduce((sum, g) => sum +
|
|
42
|
+
.reduce((sum, g) => sum + (g.occurrences || 0), 0);
|
|
48
43
|
const totalWarned = autoGates
|
|
49
44
|
.filter((g) => g.action === 'warn')
|
|
50
|
-
.reduce((sum, g) => sum +
|
|
45
|
+
.reduce((sum, g) => sum + (g.occurrences || 0), 0);
|
|
51
46
|
|
|
52
47
|
// Top blocked gate. A configured block rule with zero occurrences is not a
|
|
53
48
|
// "top blocker"; only recorded block events should appear here.
|
|
54
49
|
const topBlocked = [...allGates]
|
|
55
|
-
.filter((g) => g.action === 'block' &&
|
|
56
|
-
.sort((a, b) =>
|
|
50
|
+
.filter((g) => g.action === 'block' && Number(g.occurrences || 0) > 0)
|
|
51
|
+
.sort((a, b) => (b.occurrences || 0) - (a.occurrences || 0))
|
|
57
52
|
.at(0) || null;
|
|
58
53
|
|
|
59
54
|
// Last promotion event
|
|
@@ -110,7 +105,7 @@ function computeCalibration(gates) {
|
|
|
110
105
|
const calibration = [];
|
|
111
106
|
for (const gate of gates || []) {
|
|
112
107
|
if (!gate || !gate.id) continue;
|
|
113
|
-
const occurrences =
|
|
108
|
+
const occurrences = Number(gate.occurrences || 0);
|
|
114
109
|
const action = gate.action || 'unknown';
|
|
115
110
|
// Only annotate gates with recorded occurrence data
|
|
116
111
|
if (occurrences === 0) continue;
|
|
@@ -263,7 +258,6 @@ module.exports = {
|
|
|
263
258
|
loadGatesFile,
|
|
264
259
|
tryComputeBayesErrorRate,
|
|
265
260
|
computeCalibration,
|
|
266
|
-
safeOccurrenceCount,
|
|
267
261
|
MANUAL_GATES_PATH,
|
|
268
262
|
STATS_PATH,
|
|
269
263
|
};
|
package/scripts/gates-engine.js
CHANGED
|
@@ -16,13 +16,6 @@ const {
|
|
|
16
16
|
const {
|
|
17
17
|
evaluateWorkflowSentinel,
|
|
18
18
|
} = require('./workflow-sentinel');
|
|
19
|
-
const {
|
|
20
|
-
extractPayloadText,
|
|
21
|
-
extractPayloadPreviousUserText,
|
|
22
|
-
hasPositiveFeedback,
|
|
23
|
-
isLowValueCloseout,
|
|
24
|
-
buildResponseQualityReason,
|
|
25
|
-
} = require('./hook-stop-anti-claim');
|
|
26
19
|
const {
|
|
27
20
|
recordDecisionEvaluation,
|
|
28
21
|
recordDecisionOutcome,
|
|
@@ -2592,39 +2585,6 @@ function buildReminderOutput(context) {
|
|
|
2592
2585
|
});
|
|
2593
2586
|
}
|
|
2594
2587
|
|
|
2595
|
-
function inferHookEventName(input = {}) {
|
|
2596
|
-
const explicit = input.hook_event_name || input.hookEventName || input.event || input.lifecycle;
|
|
2597
|
-
if (explicit) return String(explicit);
|
|
2598
|
-
return extractPayloadText(input) ? 'Stop' : 'PreToolUse';
|
|
2599
|
-
}
|
|
2600
|
-
|
|
2601
|
-
function buildResponseQualityBlockOutput(reason, input = {}) {
|
|
2602
|
-
return JSON.stringify({
|
|
2603
|
-
decision: 'block',
|
|
2604
|
-
reason,
|
|
2605
|
-
hookSpecificOutput: {
|
|
2606
|
-
hookEventName: inferHookEventName(input),
|
|
2607
|
-
permissionDecision: 'deny',
|
|
2608
|
-
permissionDecisionReason: reason,
|
|
2609
|
-
},
|
|
2610
|
-
});
|
|
2611
|
-
}
|
|
2612
|
-
|
|
2613
|
-
function evaluateFinalResponseQualityGate(input = {}) {
|
|
2614
|
-
const finalText = extractPayloadText(input) || process.env.CLAUDE_RESPONSE || '';
|
|
2615
|
-
const previousUserText = extractPayloadPreviousUserText(input)
|
|
2616
|
-
|| process.env.CLAUDE_PREVIOUS_USER_TEXT
|
|
2617
|
-
|| process.env.CLAUDE_PREVIOUS_USER
|
|
2618
|
-
|| '';
|
|
2619
|
-
|
|
2620
|
-
if (!finalText || !hasPositiveFeedback(previousUserText)) return null;
|
|
2621
|
-
if (!isLowValueCloseout(finalText, '')) return null;
|
|
2622
|
-
recordStat('response-quality-shallow-closeout', 'block', null, {
|
|
2623
|
-
hookEventName: inferHookEventName(input),
|
|
2624
|
-
});
|
|
2625
|
-
return buildResponseQualityBlockOutput(buildResponseQualityReason(), input);
|
|
2626
|
-
}
|
|
2627
|
-
|
|
2628
2588
|
// ---------------------------------------------------------------------------
|
|
2629
2589
|
// Upgrade nudge: surfaces Pro value at usage milestones and trial expiry.
|
|
2630
2590
|
// Block-action Pro CTA: brief upgrade mention after a deny/warn decision.
|
|
@@ -2955,9 +2915,6 @@ function mergeContextStrings(...ctxs) {
|
|
|
2955
2915
|
}
|
|
2956
2916
|
|
|
2957
2917
|
async function runAsync(input) {
|
|
2958
|
-
const responseQualityGate = evaluateFinalResponseQualityGate(input);
|
|
2959
|
-
if (responseQualityGate) return responseQualityGate;
|
|
2960
|
-
|
|
2961
2918
|
const secretGuard = evaluateSecretGuard(input);
|
|
2962
2919
|
if (secretGuard) {
|
|
2963
2920
|
return formatOutput(secretGuard);
|
|
@@ -3005,9 +2962,6 @@ async function runAsync(input) {
|
|
|
3005
2962
|
}
|
|
3006
2963
|
|
|
3007
2964
|
function run(input) {
|
|
3008
|
-
const responseQualityGate = evaluateFinalResponseQualityGate(input);
|
|
3009
|
-
if (responseQualityGate) return responseQualityGate;
|
|
3010
|
-
|
|
3011
2965
|
const secretGuard = evaluateSecretGuard(input);
|
|
3012
2966
|
if (secretGuard) {
|
|
3013
2967
|
return formatOutput(secretGuard);
|
|
@@ -3342,9 +3296,6 @@ module.exports = {
|
|
|
3342
3296
|
evaluateGatesAsync,
|
|
3343
3297
|
computeExecutableHash,
|
|
3344
3298
|
formatOutput,
|
|
3345
|
-
inferHookEventName,
|
|
3346
|
-
buildResponseQualityBlockOutput,
|
|
3347
|
-
evaluateFinalResponseQualityGate,
|
|
3348
3299
|
isApprovalGatesEnabled,
|
|
3349
3300
|
run,
|
|
3350
3301
|
runAsync,
|
|
@@ -122,7 +122,7 @@ function resolveGeminiEmbeddingConfig(env = process.env) {
|
|
|
122
122
|
|
|
123
123
|
return {
|
|
124
124
|
enabled,
|
|
125
|
-
provider: enabled ? 'gemini' : 'local',
|
|
125
|
+
provider: provider === 'coreai' ? 'coreai' : (enabled ? 'gemini' : 'local'),
|
|
126
126
|
model: String(env.THUMBGATE_GEMINI_EMBED_MODEL || GEMINI_EMBEDDING_2_MODEL).trim() || GEMINI_EMBEDDING_2_MODEL,
|
|
127
127
|
apiKey,
|
|
128
128
|
apiBaseUrl: trimTrailingSlashes(env.THUMBGATE_GEMINI_API_BASE_URL || 'https://generativelanguage.googleapis.com/v1beta'),
|
|
@@ -171,6 +171,7 @@ function buildGeminiEmbeddingRolloutPlan(args = {}) {
|
|
|
171
171
|
},
|
|
172
172
|
rolloutSteps: [
|
|
173
173
|
'Keep local embeddings as the default offline path.',
|
|
174
|
+
'For Apple Silicon developers, route local queries through Core AI (AOT compiled models) to bypass CPU overhead.',
|
|
174
175
|
'Enable Gemini Embedding 2 only when a Gemini API key is present.',
|
|
175
176
|
'Use task-specific query/document prefixes at index and retrieval time.',
|
|
176
177
|
'Start at 768 dimensions, then benchmark 1536 only if recall misses show up.',
|