thumbgate 1.34.0 → 1.34.2

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.
@@ -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.34.0",
4
+ "version": "1.34.2",
5
5
  "author": {
6
6
  "name": "Igor Ganapolsky",
7
7
  "email": "ig5973700@gmail.com",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thumbgate",
3
- "version": "1.34.0",
3
+ "version": "1.34.2",
4
4
  "description": "ThumbGate — 👍👎 feedback that teaches your AI agent. Thumbs down a mistake, it never happens again.",
5
5
  "homepage": "https://thumbgate.ai",
6
6
  "transport": "stdio",
@@ -2,13 +2,13 @@
2
2
  "mcpServers": {
3
3
  "thumbgate": {
4
4
  "command": "npx",
5
- "args": ["--yes", "--package", "thumbgate@1.34.0", "thumbgate", "serve"]
5
+ "args": ["--yes", "--package", "thumbgate@1.34.2", "thumbgate", "serve"]
6
6
  }
7
7
  },
8
8
  "hooks": {
9
9
  "preToolUse": {
10
10
  "command": "npx",
11
- "args": ["--yes", "--package", "thumbgate@1.34.0", "thumbgate", "gate-check"]
11
+ "args": ["--yes", "--package", "thumbgate@1.34.2", "thumbgate", "gate-check"]
12
12
  }
13
13
  }
14
14
  }
@@ -9,12 +9,12 @@ version: "1"
9
9
  skills:
10
10
  thumbgate-gate-check:
11
11
  description: "ThumbGate PreToolUse gate — blocks known-bad tool calls"
12
- command: "npx --yes --package thumbgate@1.34.0 thumbgate gate-check"
12
+ command: "npx --yes --package thumbgate@1.34.2 thumbgate gate-check"
13
13
  trigger: pre_tool_use
14
14
 
15
15
  thumbgate-feedback:
16
16
  description: "ThumbGate feedback capture — logs user prompt context"
17
- command: "npx --yes --package thumbgate@1.34.0 thumbgate hook-auto-capture"
17
+ command: "npx --yes --package thumbgate@1.34.2 thumbgate hook-auto-capture"
18
18
  trigger: user_prompt
19
19
 
20
20
  mcp:
@@ -23,6 +23,6 @@ mcp:
23
23
  args:
24
24
  - "--yes"
25
25
  - "--package"
26
- - "thumbgate@1.34.0"
26
+ - "thumbgate@1.34.2"
27
27
  - "thumbgate"
28
28
  - "serve"
@@ -328,7 +328,7 @@ const {
328
328
  finalizeSession: finalizeFeedbackSession,
329
329
  } = require('../../scripts/feedback-session');
330
330
 
331
- const SERVER_INFO = { name: 'thumbgate-mcp', version: '1.34.0' };
331
+ const SERVER_INFO = { name: 'thumbgate-mcp', version: '1.34.2' };
332
332
  const COMMERCE_CATEGORIES = [
333
333
  'product_recommendation',
334
334
  'brand_compliance',
@@ -7,7 +7,7 @@
7
7
  "npx",
8
8
  "--yes",
9
9
  "--package",
10
- "thumbgate@1.34.0",
10
+ "thumbgate@1.34.2",
11
11
  "thumbgate",
12
12
  "serve"
13
13
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thumbgate",
3
- "version": "1.34.0",
3
+ "version": "1.34.2",
4
4
  "description": "ThumbGate Pre-Action Checks self-improve from ranked lessons and repeated failures, hard-block detected secret leaks, and block matches in strict mode.",
5
5
  "homepage": "https://thumbgate.ai",
6
6
  "repository": {
@@ -614,7 +614,7 @@
614
614
  "test:evolution": "node --test tests/workspace-evolver.test.js",
615
615
  "test:watcher": "node --test tests/jsonl-watcher.test.js",
616
616
  "test:autoresearch": "node --test tests/autoresearch.test.js",
617
- "test:ops": "node --test tests/qa-scenario-planner.test.js tests/adk-consolidator.test.js tests/anthropic-partner-strategy.test.js tests/auto-promote-gates.test.js tests/auto-wire-hooks.test.js tests/claude-skill.test.js tests/codegraph-context.test.js tests/commercial-signals.test.js tests/decision-journal.test.js tests/delegation-runtime.test.js tests/disagreement-mining.test.js tests/failure-diagnostics.test.js tests/financial-control-plane.test.js tests/gate-stats.test.js tests/gates-engine-upgrade-cta.test.js tests/git-hook-installer.test.js tests/github-billing.test.js tests/intervention-policy.test.js tests/markdown-escape.test.js tests/mcp-tools-gates.test.js tests/native-messaging-audit.test.js tests/project-bayes-e2e.test.js tests/project-bayes.test.js tests/rate-limiter.test.js tests/schedule-manager.test.js tests/session-handoff.test.js tests/skill-generator.test.js tests/smart-learning.test.js tests/spike-and-sink.test.js tests/stripe-revenue.test.js tests/stripe-webhook-route.test.js tests/stripe-webhook-rotation.test.js tests/train-from-feedback.test.js tests/workflow-hardening-sprint.test.js tests/workflow-sentinel.test.js tests/test-suite-parity.test.js tests/a2ui-engine.test.js tests/webhook-delivery.test.js tests/auto-context-packs.test.js tests/daily-block-cap.test.js tests/auto-promote-regression-gate.test.js",
617
+ "test:ops": "node --test tests/qa-scenario-planner.test.js tests/adk-consolidator.test.js tests/anthropic-partner-strategy.test.js tests/auto-promote-gates.test.js tests/auto-wire-hooks.test.js tests/claude-skill.test.js tests/codegraph-context.test.js tests/commercial-signals.test.js tests/decision-journal.test.js tests/delegation-runtime.test.js tests/disagreement-mining.test.js tests/failure-diagnostics.test.js tests/financial-control-plane.test.js tests/financial-spend-attack-matrix.test.js tests/gate-stats.test.js tests/gates-engine-upgrade-cta.test.js tests/git-hook-installer.test.js tests/github-billing.test.js tests/intervention-policy.test.js tests/markdown-escape.test.js tests/mcp-tools-gates.test.js tests/native-messaging-audit.test.js tests/project-bayes-e2e.test.js tests/project-bayes.test.js tests/rate-limiter.test.js tests/schedule-manager.test.js tests/session-handoff.test.js tests/skill-generator.test.js tests/smart-learning.test.js tests/spike-and-sink.test.js tests/stripe-revenue.test.js tests/stripe-webhook-route.test.js tests/stripe-webhook-rotation.test.js tests/train-from-feedback.test.js tests/workflow-hardening-sprint.test.js tests/workflow-sentinel.test.js tests/test-suite-parity.test.js tests/a2ui-engine.test.js tests/webhook-delivery.test.js tests/auto-context-packs.test.js tests/daily-block-cap.test.js tests/auto-promote-regression-gate.test.js",
618
618
  "test:session-analyzer": "node --test tests/session-analyzer.test.js",
619
619
  "test:tessl": "node --test tests/tessl-export.test.js",
620
620
  "test:gates": "node --test --test-concurrency=1 tests/gate-templates.test.js tests/gates-engine.test.js tests/claim-verification.test.js tests/universal-claim-evaluator.test.js tests/secret-scanner.test.js tests/secret-fixture-safety.test.js tests/prompt-guard.test.js tests/audit-trail.test.js tests/profile-router.test.js tests/workflow-sentinel.test.js tests/docker-sandbox-planner.test.js tests/mcp-tools-suggest-fix.test.js tests/deny-network-egress-pattern.test.js tests/git-pathspec-scope.test.js tests/git-global-option-bypass.test.js tests/gate-evasion-matrix.test.js",
@@ -996,7 +996,7 @@
996
996
  "@google/genai": {
997
997
  "protobufjs": "7.6.5"
998
998
  },
999
- "brace-expansion": "5.0.8",
999
+ "brace-expansion": "5.0.9",
1000
1000
  "onnxruntime-web": {
1001
1001
  "protobufjs": "7.6.5"
1002
1002
  },
package/public/index.html CHANGED
@@ -5,7 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <meta name="generator" content="ThumbGate">
7
7
  <meta name="author" content="Igor Ganapolsky">
8
- <meta name="thumbgate-version" content="1.34.0">
8
+ <meta name="thumbgate-version" content="1.34.2">
9
9
  __GOOGLE_SITE_VERIFICATION_META__
10
10
  <link rel="icon" type="image/png" href="/thumbgate-icon.png">
11
11
  <link rel="canonical" href="__APP_ORIGIN__/">
@@ -942,7 +942,7 @@ next decision recorded before execution</pre>
942
942
 
943
943
  <footer>
944
944
  <div class="shell footer-inner">
945
- <span>ThumbGate · MIT License · npm v1.34.0</span>
945
+ <span>ThumbGate · MIT License · npm v1.34.2</span>
946
946
  <div class="footer-links">
947
947
  <a href="https://github.com/IgorGanapolsky/ThumbGate" target="_blank" rel="noopener">GitHub</a>
948
948
  <a href="/guide">Technical setup</a>
@@ -10,12 +10,12 @@
10
10
  <meta property="og:title" content="ThumbGate — The Numbers">
11
11
  <meta property="og:description" content="Generated first-party operational snapshot: configured gates, recorded interventions, and explicit zero-evidence caveats.">
12
12
  <meta property="og:type" content="website">
13
- <meta property="og:url" content="https://thumbgate.ai/numbers">
13
+ <meta property="og:url" content="https://thumbgate-production.up.railway.app/numbers">
14
14
  <meta name="twitter:card" content="summary_large_image">
15
- <link rel="canonical" href="https://thumbgate.ai/numbers">
15
+ <link rel="canonical" href="https://thumbgate-production.up.railway.app/numbers">
16
16
  <link rel="icon" type="image/png" href="/thumbgate-icon.png">
17
17
  <link rel="apple-touch-icon" href="/apple-touch-icon.png">
18
- <script defer data-domain="thumbgate.ai" src="https://plausible.io/js/script.js"></script>
18
+ <script defer data-domain="thumbgate-production.up.railway.app" src="https://plausible.io/js/script.js"></script>
19
19
 
20
20
  <script type="application/ld+json">
21
21
  {
@@ -25,9 +25,9 @@
25
25
  "alternateName": "thumbgate",
26
26
  "applicationCategory": "DeveloperApplication",
27
27
  "operatingSystem": "Cross-platform, Node.js >=18.18.0",
28
- "softwareVersion": "1.34.0",
29
- "url": "https://thumbgate.ai/numbers",
30
- "dateModified": "2026-05-07",
28
+ "softwareVersion": "1.34.2",
29
+ "url": "https://thumbgate-production.up.railway.app/numbers",
30
+ "dateModified": "2026-08-03",
31
31
  "creator": {
32
32
  "@type": "Person",
33
33
  "name": "Igor Ganapolsky",
@@ -46,7 +46,7 @@
46
46
  "@type": "Dataset",
47
47
  "name": "ThumbGate Operational Snapshot",
48
48
  "description": "First-party operational snapshot from the ThumbGate pre-action check runtime: configured checks, recorded block/warn events, estimated token savings from recorded blocks, and Bayes error rate when the sample supports it.",
49
- "url": "https://thumbgate.ai/numbers",
49
+ "url": "https://thumbgate-production.up.railway.app/numbers",
50
50
  "license": "https://opensource.org/licenses/MIT",
51
51
  "creator": {
52
52
  "@type": "Person",
@@ -57,8 +57,8 @@
57
57
  "https://www.linkedin.com/in/igorganapolsky"
58
58
  ]
59
59
  },
60
- "dateModified": "2026-05-07",
61
- "datePublished": "2026-05-07",
60
+ "dateModified": "2026-08-03",
61
+ "datePublished": "2026-08-03",
62
62
  "keywords": [
63
63
  "AI agent gates",
64
64
  "LLM token savings",
@@ -70,7 +70,7 @@
70
70
  {
71
71
  "@type": "PropertyValue",
72
72
  "name": "configured_gates",
73
- "value": 36
73
+ "value": 40
74
74
  },
75
75
  {
76
76
  "@type": "PropertyValue",
@@ -195,7 +195,6 @@
195
195
  <a href="/learn">Learn</a>
196
196
  <a href="/compare">Compare</a>
197
197
  <a href="/numbers">Numbers</a>
198
- <a href="/evaluations">Evaluations</a>
199
198
  <a href="/dashboard">Dashboard</a>
200
199
  <a href="/pro">Pro</a>
201
200
  </nav>
@@ -203,15 +202,15 @@
203
202
  <main class="container">
204
203
  <h1>The Numbers</h1>
205
204
  <p class="subtitle">Generated first-party operational snapshot from the ThumbGate runtime. This is not customer traction, install volume, revenue, or proof that a configured gate has fired.</p>
206
- <div class="freshness">Updated: 2026-05-07 · Version 1.34.0</div>
205
+ <div class="freshness">Updated: 2026-08-03 · Version 1.34.2</div>
207
206
  <div class="truth-note"><strong>Read this first:</strong> configured checks are inventory. Recorded blocks and warnings are usage evidence. This snapshot currently reports 0 recorded hard-block event(s) and 0 recorded warning event(s).</div>
208
207
 
209
208
  <h2>Gate enforcement</h2>
210
209
  <div class="stats-grid">
211
210
  <div class="stat-card">
212
211
  <div class="stat-label">Configured checks</div>
213
- <div class="stat-value">36</div>
214
- <div class="stat-sub">36 shipped defaults · 0 auto-promoted; inventory, not usage</div>
212
+ <div class="stat-value">40</div>
213
+ <div class="stat-sub">40 shipped defaults · 0 auto-promoted; inventory, not usage</div>
215
214
  <a class="stat-source" href="https://github.com/IgorGanapolsky/ThumbGate/blob/main/scripts/gate-stats.js">source: gate-stats.js</a>
216
215
  </div>
217
216
  <div class="stat-card">
@@ -278,7 +277,7 @@
278
277
  <div class="cta">
279
278
  <a href="https://www.npmjs.com/package/thumbgate">Install ThumbGate — npx thumbgate init</a>
280
279
  <div class="footer-note">Prefer the raw feed? See <a href="https://github.com/IgorGanapolsky/ThumbGate">GitHub</a> or run <code>npm run gate:stats</code> locally.</div>
281
- <div class="footer-note">Generated at 2026-05-07T14:59:51.816Z UTC.</div>
280
+ <div class="footer-note">Generated at 2026-08-03T05:26:21.201Z UTC.</div>
282
281
  </div>
283
282
  </main>
284
283
  </body>
@@ -48,6 +48,10 @@ const ECONOMIC_ACTION_PATTERNS = [
48
48
  /\badd\s+(?:a\s+)?(?:credit\s+)?card\b/i,
49
49
  /\badd\s+(?:a\s+)?payment\s+method\b/i,
50
50
  /\b(?:buy|purchase|subscribe|top-?up)\b/i,
51
+ /\bcharge\s+(?:me\s+)?(?:\$|usd\s*)\d/i,
52
+ /\bupgrade\b.{0,40}\b(?:pro|premium|business|enterprise)\b/i,
53
+ /\b(?:activate|cancel|change|create|update|upgrade)\s+(?:a\s+)?(?:\w+\s+){0,3}(?:plan|subscription|tier|seat)\b/i,
54
+ /\b(?:activate|cancel|change|create|update|upgrade)\s+(?:a\s+)?(?:\w+\s+){0,3}(?:checkout|payment\s+method)\b/i,
51
55
  /\b(?:confirm|complete|open|start)\s+(?:the\s+)?checkout\b/i,
52
56
  /\bcheckout\s+(?:flow|page|session)\b/i,
53
57
  /\b(?:issue|send)\s+(?:a\s+)?(?:invoice|payout|refund)\b/i,
@@ -58,8 +62,31 @@ const ECONOMIC_ACTION_PATTERNS = [
58
62
  /\brenew\s+(?:a\s+)?subscription\b/i,
59
63
  /\btransfer\s+(?:dollars?|funds|money|usd)\b/i,
60
64
  /\bupgrade\s+(?:a\s+)?plan\b/i,
61
- /\b(?:charges?|checkout[\s_-]*sessions?|invoices?|payment[\s_-]*intents?|payment[\s_-]*methods?|payouts?|refunds?|subscriptions?|top[\s_-]*ups?|transfers?)\s+(?:attach|cancel|capture|confirm|create|detach|finalize|pay|refund|send|update)\b/i,
62
- /\b(?:attach|cancel|capture|confirm|create|detach|finalize|pay|refund|send|update)\s+(?:a\s+)?(?:charge|invoice|payment|payment\s+method|payout|refund|subscription|top-?up|transfer)\b/i,
65
+ /\bcharges?\s+(?:attach|cancel|capture|confirm|create|detach|finalize|pay|refund|send|update)\b/i,
66
+ /\bcheckout[\s_-]*sessions?\s+(?:attach|cancel|capture|confirm|create|detach|finalize|pay|refund|send|update)\b/i,
67
+ /\binvoices?\s+(?:attach|cancel|capture|confirm|create|detach|finalize|pay|refund|send|update)\b/i,
68
+ /\bpayment[\s_-]*intents?\s+(?:attach|cancel|capture|confirm|create|detach|finalize|pay|refund|send|update)\b/i,
69
+ /\bpayment[\s_-]*methods?\s+(?:attach|cancel|capture|confirm|create|detach|finalize|pay|refund|send|update)\b/i,
70
+ /\bpayouts?\s+(?:attach|cancel|capture|confirm|create|detach|finalize|pay|refund|send|update)\b/i,
71
+ /\brefunds?\s+(?:attach|cancel|capture|confirm|create|detach|finalize|pay|refund|send|update)\b/i,
72
+ /\bsubscriptions?\s+(?:attach|cancel|capture|confirm|create|detach|finalize|pay|refund|send|update)\b/i,
73
+ /\btop[\s_-]*ups?\s+(?:attach|cancel|capture|confirm|create|detach|finalize|pay|refund|send|update)\b/i,
74
+ /\btransfers?\s+(?:attach|cancel|capture|confirm|create|detach|finalize|pay|refund|send|update)\b/i,
75
+ /\b(?:attach|cancel|capture|confirm|create|detach)\s+(?:a\s+)?(?:charge|invoice|payment|payment\s+method)\b/i,
76
+ /\b(?:finalize|pay|refund|send|update)\s+(?:a\s+)?(?:charge|invoice|payment|payment\s+method)\b/i,
77
+ /\b(?:attach|cancel|capture|confirm|create|detach)\s+(?:a\s+)?(?:payout|refund|subscription|top-?up|transfer)\b/i,
78
+ /\b(?:finalize|pay|refund|send|update)\s+(?:a\s+)?(?:payout|refund|subscription|top-?up|transfer)\b/i,
79
+ ];
80
+
81
+ // Host/path URL surfaces that open paid checkout or plan-upgrade flows.
82
+ // These are NOT covered by prose patterns alone (e.g. `open https://app.apollo.io/#/settings/plans/upgrade`
83
+ // or WebFetch { url: 'https://checkout.stripe.com/...' }) — Apollo $588 incident class.
84
+ const ECONOMIC_URL_PATTERNS = [
85
+ /(?:https?:\/\/)?(?:www\.)?checkout\.stripe\.com\b/i,
86
+ /(?:https?:\/\/)?(?:www\.)?buy\.stripe\.com\b/i,
87
+ /(?:https?:\/\/)?(?:www\.)?billing\.stripe\.com\b/i,
88
+ /(?:https?:\/\/)?(?:www\.)?app\.apollo\.io\b[^\s"'`]*?(?:plans|upgrade|billing|checkout|subscription)/i,
89
+ /(?:https?:\/\/)?(?:www\.)?apollo\.io\b[^\s"'`]*?(?:plans|upgrade|billing|checkout|subscription)/i,
63
90
  ];
64
91
 
65
92
  const SCREEN_TOOL_PATTERN = /(?:browser|computer|playwright|puppeteer|selenium|click|tap|press)/i;
@@ -156,14 +183,14 @@ function remoteFinancialLedgerAnchorStore(options = {}) {
156
183
  return {
157
184
  read({ ledgerId }) {
158
185
  const response = invoke({ operation: 'read', ledgerId });
159
- if (!response || response.ok !== true || !Object.hasOwn(response, 'anchor')) {
186
+ if (response?.ok !== true || !Object.hasOwn(response, 'anchor')) {
160
187
  throw financialError('remote financial anchor returned an invalid read response');
161
188
  }
162
189
  return response.anchor;
163
190
  },
164
191
  compareAndSet({ ledgerId, expected, next }) {
165
192
  const response = invoke({ operation: 'compareAndSet', ledgerId, expected, next });
166
- if (!response || response.ok !== true || typeof response.applied !== 'boolean') {
193
+ if (response?.ok !== true || typeof response.applied !== 'boolean') {
167
194
  throw financialError('remote financial anchor returned an invalid compare-and-set response');
168
195
  }
169
196
  return response.applied;
@@ -324,17 +351,39 @@ function detectEconomicAction(toolName, toolInput = {}) {
324
351
  // approved; caller-supplied prose must never downgrade a blind click.
325
352
  if (detectOpaqueScreenMutation(normalizedToolName, toolInput)) return true;
326
353
  const command = shellEconomicText(normalizedToolName, toolInput.command || toolInput.cmd);
354
+ const screenMutationProse = isDeclaredScreenMutation(normalizedToolName, toolInput)
355
+ ? [toolInput.goal, toolInput.description, toolInput.prompt, metadata.context]
356
+ : [];
357
+ // WebFetch / browser navigate often put the spend surface only in url/uri/href.
358
+ const navigationTargets = [
359
+ toolInput.url,
360
+ toolInput.uri,
361
+ toolInput.href,
362
+ toolInput.link,
363
+ toolInput.navigate,
364
+ toolInput.target,
365
+ ].map((value) => String(value || '')).join(' ');
327
366
  const combined = [
328
367
  normalizedToolName.replace(/[_-]+/g, ' '),
329
368
  command,
330
- toolInput.goal,
331
369
  toolInput.action,
332
370
  toolInput.operation,
333
- metadata.context,
371
+ navigationTargets,
372
+ ...screenMutationProse,
334
373
  ].map((value) => String(value || '')).join(' ');
374
+ if (ECONOMIC_URL_PATTERNS.some((pattern) => pattern.test(combined))) return true;
335
375
  return ECONOMIC_ACTION_PATTERNS.some((pattern) => pattern.test(combined));
336
376
  }
337
377
 
378
+ function isDeclaredScreenMutation(toolName, toolInput = {}) {
379
+ if (!SCREEN_TOOL_PATTERN.test(String(toolName || ''))) return false;
380
+ const input = objectValue(toolInput);
381
+ const declaredOperation = [input.action, input.operation, input.type]
382
+ .map((value) => String(value || ''))
383
+ .join(' ');
384
+ return SCREEN_MUTATION_PATTERN.test(toolName) || SCREEN_MUTATION_PATTERN.test(declaredOperation);
385
+ }
386
+
338
387
  function detectOpaqueScreenMutation(toolName, toolInput = {}) {
339
388
  const normalizedToolName = String(toolName || '').trim();
340
389
  const input = objectValue(toolInput);
@@ -372,8 +421,8 @@ function shellEconomicText(toolName, rawCommand) {
372
421
  return command
373
422
  .split(/(?:&&|\|\||[;\n]|(?<!\|)\|(?!\|))/)
374
423
  .map((segment) => {
375
- const clean = segment.trim().replace(/^(?:sudo\s+)?(?:env\s+)?(?:[A-Za-z_][A-Za-z0-9_]*=[^\s]+\s+)*/, '');
376
- const executable = clean.match(/^([A-Za-z0-9_.\/-]+)/)?.[1]?.split('/').at(-1)?.toLowerCase();
424
+ const clean = segment.trim().replace(/^(?:sudo\s+)?(?:env\s+)?(?:[A-Za-z_]\w*=[^\s]+\s+)*/, '');
425
+ const executable = /^([\w./-]+)/.exec(clean)?.[1]?.split('/').at(-1)?.toLowerCase();
377
426
  if (['rg', 'grep', 'git', 'cat', 'head', 'tail', 'less', 'more', 'echo', 'printf'].includes(executable)) return '';
378
427
  return clean;
379
428
  })
@@ -780,40 +829,38 @@ function validateAttachedControl(result) {
780
829
  }
781
830
  }
782
831
 
783
- function validateRequisition(result, options) {
784
- const reconciliation = reconcilePurchaseLedger(options);
785
- if (!reconciliation.ok && (reconciliation.invalidEventHashes.length > 0
832
+ function reconciliationHasIntegrityFailure(reconciliation) {
833
+ return !reconciliation.ok && (
834
+ reconciliation.invalidEventHashes.length > 0
786
835
  || reconciliation.invalidChainLinks.length > 0
787
836
  || reconciliation.ledgerHeadMismatches.length > 0
788
- || reconciliation.malformedRows.length > 0)) {
789
- const anchorUnavailable = reconciliation.ledgerHeadMismatches.some(
790
- (entry) => optionalString(entry.rollbackResistantAnchorError)
837
+ || reconciliation.malformedRows.length > 0
838
+ );
839
+ }
840
+
841
+ function blockLedgerIntegrityFailures(result, reconciliation) {
842
+ const anchorUnavailable = reconciliation.ledgerHeadMismatches.some(
843
+ (entry) => optionalString(entry.rollbackResistantAnchorError)
844
+ );
845
+ const independentTampering = reconciliation.invalidEventHashes.length > 0
846
+ || reconciliation.invalidChainLinks.length > 0
847
+ || reconciliation.malformedRows.length > 0
848
+ || reconciliation.ledgerHeadMismatches.some(
849
+ (entry) => !optionalString(entry.rollbackResistantAnchorError)
791
850
  );
792
- const independentTampering = reconciliation.invalidEventHashes.length > 0
793
- || reconciliation.invalidChainLinks.length > 0
794
- || reconciliation.malformedRows.length > 0
795
- || reconciliation.ledgerHeadMismatches.some(
796
- (entry) => !optionalString(entry.rollbackResistantAnchorError)
797
- );
798
- if (independentTampering) {
799
- addBlock(result, 'financial_ledger_tampered', 'Financial ledger integrity verification failed.');
800
- }
801
- if (anchorUnavailable) {
802
- addBlock(
803
- result,
804
- 'financial_ledger_anchor_unavailable',
805
- 'Rollback-resistant financial ledger anchor is unavailable; financial actions fail closed.'
806
- );
807
- }
808
- return;
851
+ if (independentTampering) {
852
+ addBlock(result, 'financial_ledger_tampered', 'Financial ledger integrity verification failed.');
809
853
  }
810
- if (!result.requisitionId) return;
811
- const requisition = projectRequisition(result.requisitionId, options);
812
- result.requisition = requisition;
813
- if (!requisition) {
814
- addBlock(result, 'unknown_purchase_requisition', `Purchase requisition '${result.requisitionId}' does not exist.`);
815
- return;
854
+ if (anchorUnavailable) {
855
+ addBlock(
856
+ result,
857
+ 'financial_ledger_anchor_unavailable',
858
+ 'Rollback-resistant financial ledger anchor is unavailable; financial actions fail closed.'
859
+ );
816
860
  }
861
+ }
862
+
863
+ function validateRequisitionBindings(result, requisition, options) {
817
864
  try {
818
865
  verifyPurchaseApprovalBinding(requisition, options);
819
866
  } catch (error) {
@@ -824,6 +871,9 @@ function validateRequisition(result, options) {
824
871
  } catch (error) {
825
872
  addBlock(result, 'reservation_not_bound_to_approval', error.message);
826
873
  }
874
+ }
875
+
876
+ function validateRequisitionConstraints(result, requisition) {
827
877
  if (requisition.status !== 'reserved') {
828
878
  addBlock(result, 'requisition_not_reserved', `Purchase requisition '${result.requisitionId}' is ${requisition.status}, not reserved.`);
829
879
  }
@@ -846,6 +896,23 @@ function validateRequisition(result, options) {
846
896
  if (normalizeToolName(result.actionBinding.toolName) !== normalizeToolName(requisition.approvedToolName)) {
847
897
  addBlock(result, 'financial_tool_mismatch', 'Actual financial tool does not match the approved tool.');
848
898
  }
899
+ }
900
+
901
+ function validateRequisition(result, options) {
902
+ const reconciliation = reconcilePurchaseLedger(options);
903
+ if (reconciliationHasIntegrityFailure(reconciliation)) {
904
+ blockLedgerIntegrityFailures(result, reconciliation);
905
+ return;
906
+ }
907
+ if (!result.requisitionId) return;
908
+ const requisition = projectRequisition(result.requisitionId, options);
909
+ result.requisition = requisition;
910
+ if (!requisition) {
911
+ addBlock(result, 'unknown_purchase_requisition', `Purchase requisition '${result.requisitionId}' does not exist.`);
912
+ return;
913
+ }
914
+ validateRequisitionBindings(result, requisition, options);
915
+ validateRequisitionConstraints(result, requisition);
849
916
  validateScope(result, requisition);
850
917
  }
851
918
 
@@ -897,7 +964,7 @@ function consumeReservation(input, options) {
897
964
  const chain = validateLedgerChain(ledger.events, ledger.malformedRows, ledger.head, options);
898
965
  if (!chain.ok) throw financialError('financial ledger integrity check failed before authorization');
899
966
  const current = projectRequisitionFromEvents(ledger.events, input.requisitionId, options);
900
- if (!current || current.status !== 'reserved') {
967
+ if (current?.status !== 'reserved') {
901
968
  throw financialError(`purchase requisition '${input.requisitionId}' is not available for single-use authorization`);
902
969
  }
903
970
  if (current.reservationId !== input.reservationId) {
@@ -1070,25 +1137,74 @@ function withLedgerLock(options, callback) {
1070
1137
  });
1071
1138
  }
1072
1139
 
1073
- function recoverLedgerTransaction(options = {}) {
1074
- const journalPath = getLedgerJournalPath(options);
1075
- let journal;
1140
+ function readLedgerJournal(journalPath) {
1076
1141
  try {
1077
- journal = JSON.parse(fs.readFileSync(journalPath, 'utf8'));
1142
+ return JSON.parse(fs.readFileSync(journalPath, 'utf8'));
1078
1143
  } catch (error) {
1079
- if (error.code === 'ENOENT') return;
1144
+ if (error.code === 'ENOENT') return null;
1080
1145
  throw financialError(`cannot recover financial ledger journal: ${error.message}`);
1081
1146
  }
1082
- const integrityKey = ledgerIntegrityKey(options);
1147
+ }
1148
+
1149
+ function verifiedJournalEvent(journal, integrityKey) {
1150
+ const event = journal?.event;
1083
1151
  if (journal?.schemaVersion !== LEDGER_JOURNAL_SCHEMA
1084
- || !journal.event
1085
- || journal.event.eventHash !== hashEvent(journal.event)
1152
+ || !event
1153
+ || event.eventHash !== hashEvent(event)
1086
1154
  || !verifyIntegrityRecord(journal, integrityKey)) {
1087
1155
  throw financialError('financial ledger journal integrity verification failed');
1088
1156
  }
1157
+ return event;
1158
+ }
1159
+
1160
+ function recoverAppendedLedgerEvent({
1161
+ ledger,
1162
+ event,
1163
+ previousHead,
1164
+ currentAnchor,
1165
+ headAtPrevious,
1166
+ headAtEvent,
1167
+ journalPath,
1168
+ options,
1169
+ }) {
1170
+ const preceding = ledger.events.at(-2) || null;
1171
+ const expectedPrevious = preceding
1172
+ ? { sequence: preceding.sequence, eventHash: preceding.eventHash }
1173
+ : null;
1174
+ const chain = validateLedgerChain(
1175
+ ledger.events,
1176
+ ledger.malformedRows,
1177
+ null,
1178
+ { ...options, skipLedgerHead: true }
1179
+ );
1180
+ const anchorAtPrevious = sameHead(currentAnchor, previousHead);
1181
+ const anchorAtEvent = sameHead(currentAnchor, { sequence: event.sequence, eventHash: event.eventHash });
1182
+ if (!sameHead(previousHead, expectedPrevious)
1183
+ || !chain.ok
1184
+ || (!headAtPrevious && !headAtEvent)
1185
+ || (!anchorAtPrevious && !anchorAtEvent)) {
1186
+ throw financialError('financial ledger journal does not match the recoverable append');
1187
+ }
1188
+ if (!headAtEvent) writeLedgerHeadFile(event, options);
1189
+ if (!anchorAtEvent) advanceFinancialLedgerAnchor(previousHead, event, options);
1190
+ removeDurableFile(journalPath);
1191
+ }
1089
1192
 
1193
+ function canDiscardPreparedLedgerEvent({ ledger, event, previousHead, currentAnchor, headAtPrevious, options }) {
1194
+ const currentChain = validateLedgerChain(ledger.events, ledger.malformedRows, ledger.head, options);
1195
+ return currentChain.ok
1196
+ && headAtPrevious
1197
+ && sameHead(currentAnchor, previousHead)
1198
+ && event.sequence === ledger.events.length + 1;
1199
+ }
1200
+
1201
+ function recoverLedgerTransaction(options = {}) {
1202
+ const journalPath = getLedgerJournalPath(options);
1203
+ const journal = readLedgerJournal(journalPath);
1204
+ if (!journal) return;
1205
+ const integrityKey = ledgerIntegrityKey(options);
1206
+ const event = verifiedJournalEvent(journal, integrityKey);
1090
1207
  const ledger = readLedger(options);
1091
- const event = journal.event;
1092
1208
  const currentLast = ledger.events.at(-1) || null;
1093
1209
  const previousHead = journal.previousHead;
1094
1210
  const currentHead = ledger.head;
@@ -1099,35 +1215,27 @@ function recoverLedgerTransaction(options = {}) {
1099
1215
  const headAtEvent = sameHead(currentHead, { sequence: event.sequence, eventHash: event.eventHash });
1100
1216
 
1101
1217
  if (eventAlreadyAppended) {
1102
- const preceding = ledger.events.at(-2) || null;
1103
- const expectedPrevious = preceding
1104
- ? { sequence: preceding.sequence, eventHash: preceding.eventHash }
1105
- : null;
1106
- const chain = validateLedgerChain(
1107
- ledger.events,
1108
- ledger.malformedRows,
1109
- null,
1110
- { ...options, skipLedgerHead: true }
1111
- );
1112
- const anchorAtPrevious = sameHead(currentAnchor, previousHead);
1113
- const anchorAtEvent = sameHead(currentAnchor, { sequence: event.sequence, eventHash: event.eventHash });
1114
- if (!sameHead(previousHead, expectedPrevious)
1115
- || !chain.ok
1116
- || (!headAtPrevious && !headAtEvent)
1117
- || (!anchorAtPrevious && !anchorAtEvent)) {
1118
- throw financialError('financial ledger journal does not match the recoverable append');
1119
- }
1120
- if (!headAtEvent) writeLedgerHeadFile(event, options);
1121
- if (!anchorAtEvent) advanceFinancialLedgerAnchor(previousHead, event, options);
1122
- removeDurableFile(journalPath);
1218
+ recoverAppendedLedgerEvent({
1219
+ ledger,
1220
+ event,
1221
+ previousHead,
1222
+ currentAnchor,
1223
+ headAtPrevious,
1224
+ headAtEvent,
1225
+ journalPath,
1226
+ options,
1227
+ });
1123
1228
  return;
1124
1229
  }
1125
1230
 
1126
- const currentChain = validateLedgerChain(ledger.events, ledger.malformedRows, ledger.head, options);
1127
- if (currentChain.ok
1128
- && headAtPrevious
1129
- && sameHead(currentAnchor, previousHead)
1130
- && event.sequence === ledger.events.length + 1) {
1231
+ if (canDiscardPreparedLedgerEvent({
1232
+ ledger,
1233
+ event,
1234
+ previousHead,
1235
+ currentAnchor,
1236
+ headAtPrevious,
1237
+ options,
1238
+ })) {
1131
1239
  // The crash happened before the event append. The caller never received a
1132
1240
  // success response, so discard the prepared transaction instead of
1133
1241
  // executing it during recovery.
@@ -1268,7 +1376,7 @@ function signIntegrityRecord(record, key) {
1268
1376
 
1269
1377
  function verifyIntegrityRecord(record, key) {
1270
1378
  const auth = record?.auth;
1271
- if (!record || !auth || auth.algorithm !== 'hmac-sha256') return false;
1379
+ if (!record || auth?.algorithm !== 'hmac-sha256') return false;
1272
1380
  const expectedKeyId = crypto.createHash('sha256').update(key).digest('hex').slice(0, 16);
1273
1381
  if (auth.keyId !== expectedKeyId || !/^[a-f0-9]{64}$/i.test(String(auth.signature || ''))) return false;
1274
1382
  const expected = crypto.createHmac('sha256', key).update(integrityPayload(record)).digest();
@@ -1365,7 +1473,7 @@ function verifyPurchaseApprovalBinding(requisition, options) {
1365
1473
  } catch (error) {
1366
1474
  throw financialError(`requisition '${requisition.requisitionId}' approval verification failed: ${error.message}`);
1367
1475
  }
1368
- if (!escalation || escalation.status !== 'approved') {
1476
+ if (escalation?.status !== 'approved') {
1369
1477
  throw financialError(`requisition '${requisition.requisitionId}' does not have independent human approval`);
1370
1478
  }
1371
1479
  const expectedApprovalContextDigest = requestComparableHash(requisition);
@@ -1495,6 +1603,7 @@ function financialError(message) {
1495
1603
 
1496
1604
  module.exports = {
1497
1605
  ECONOMIC_ACTION_PATTERNS,
1606
+ ECONOMIC_URL_PATTERNS,
1498
1607
  createPurchaseRequisition,
1499
1608
  buildActionAuthorization,
1500
1609
  detectEconomicAction,
@@ -164,6 +164,10 @@ const UNCONDITIONAL_HARD_FLOOR_GATE_IDS = new Set([
164
164
  'secret-exfiltration',
165
165
  'security-vuln-scan',
166
166
  'slopsquat-guard',
167
+ // Money path: even if a refactor later routes financial denials through
168
+ // applyEnforcementPosture, never demote spend blocks to warn-by-default.
169
+ // Apollo $588 incident class (2026-08).
170
+ 'financial-control',
167
171
  TASK_SCOPE_LEASE_EXPIRED_GATE_ID,
168
172
  ...SELF_PROTECT_HARD_FLOOR_GATE_IDS,
169
173
  ]);
@@ -175,12 +179,15 @@ const UNCONDITIONAL_HARD_FLOOR_GATE_IDS = new Set([
175
179
  // `git reset --hard`, or `git clean -f` silently allowed through. These four
176
180
  // map directly to CLAUDE.md's own hard-block list and must never be subject
177
181
  // to the daily cap discount, regardless of tier or strict-mode setting.
182
+ // financial-control is included for the same reason: a daily-cap discount on
183
+ // checkout/subscription spends would re-open the never-spend interlock.
178
184
  const CATASTROPHIC_DECLARATIVE_GATE_IDS = new Set([
179
185
  TASK_SCOPE_LEASE_EXPIRED_GATE_ID,
180
186
  'force-push',
181
187
  'git-reset-hard',
182
188
  'git-clean-force',
183
189
  'rm-rf-home-or-root',
190
+ 'financial-control',
184
191
  ]);
185
192
  const SELF_PROTECT_CONFIG_TARGET_PATTERN = /(?:^|\/)(?:config\/gates\/|config\/(?:budget|enforcement|mcp-allowlists)\.json$|\.thumbgate\/config\.json$|thumbgate\.json$)/i;
186
193
  const SELF_PROTECT_HOOK_TARGET_PATTERN = /(?:^|\/)(?:\.claude\/settings(?:\.local)?\.json|\.codex\/config\.toml|scripts\/hook-[^/]+\.(?:js|sh))$/i;
@@ -3387,8 +3394,9 @@ function evaluateUnconditionalHardFloor(input = {}, options = {}) {
3387
3394
 
3388
3395
  function evaluateFinancialHardFloor(input = {}, consumeReservation = false, options = {}) {
3389
3396
  const toolName = input.tool_name || input.toolName || 'unknown';
3390
- const toolInput = input.tool_input && typeof input.tool_input === 'object'
3391
- ? input.tool_input
3397
+ const rawToolInput = input.tool_input ?? input.toolInput;
3398
+ const toolInput = rawToolInput && typeof rawToolInput === 'object'
3399
+ ? rawToolInput
3392
3400
  : {};
3393
3401
  const normalizedAction = normalizeProviderAction({
3394
3402
  toolName,
package/server.json CHANGED
@@ -8,13 +8,13 @@
8
8
  "source": "github",
9
9
  "url": "https://github.com/IgorGanapolsky/ThumbGate"
10
10
  },
11
- "version": "1.34.0",
11
+ "version": "1.34.2",
12
12
  "packages": [
13
13
  {
14
14
  "registryType": "npm",
15
15
  "registryBaseUrl": "https://registry.npmjs.org",
16
16
  "identifier": "thumbgate",
17
- "version": "1.34.0",
17
+ "version": "1.34.2",
18
18
  "runtimeHint": "npx",
19
19
  "runtimeArguments": [
20
20
  {