thumbgate 1.34.0 โ 1.34.1
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/mcp/server-card.json +1 -1
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/forge/forge.yaml +3 -3
- package/adapters/mcp/server-stdio.js +1 -1
- package/adapters/opencode/opencode.json +1 -1
- package/package.json +1 -1
- package/public/index.html +2 -2
- package/public/numbers.html +14 -15
- package/scripts/financial-control-plane.js +161 -75
- package/scripts/gates-engine.js +3 -2
- package/server.json +2 -2
|
@@ -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.
|
|
4
|
+
"version": "1.34.1",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Igor Ganapolsky",
|
|
7
7
|
"email": "ig5973700@gmail.com",
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
"mcpServers": {
|
|
3
3
|
"thumbgate": {
|
|
4
4
|
"command": "npx",
|
|
5
|
-
"args": ["--yes", "--package", "thumbgate@1.34.
|
|
5
|
+
"args": ["--yes", "--package", "thumbgate@1.34.1", "thumbgate", "serve"]
|
|
6
6
|
}
|
|
7
7
|
},
|
|
8
8
|
"hooks": {
|
|
9
9
|
"preToolUse": {
|
|
10
10
|
"command": "npx",
|
|
11
|
-
"args": ["--yes", "--package", "thumbgate@1.34.
|
|
11
|
+
"args": ["--yes", "--package", "thumbgate@1.34.1", "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.
|
|
12
|
+
command: "npx --yes --package thumbgate@1.34.1 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.
|
|
17
|
+
command: "npx --yes --package thumbgate@1.34.1 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.
|
|
26
|
+
- "thumbgate@1.34.1"
|
|
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.
|
|
331
|
+
const SERVER_INFO = { name: 'thumbgate-mcp', version: '1.34.1' };
|
|
332
332
|
const COMMERCE_CATEGORIES = [
|
|
333
333
|
'product_recommendation',
|
|
334
334
|
'brand_compliance',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thumbgate",
|
|
3
|
-
"version": "1.34.
|
|
3
|
+
"version": "1.34.1",
|
|
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": {
|
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.
|
|
8
|
+
<meta name="thumbgate-version" content="1.34.1">
|
|
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.
|
|
945
|
+
<span>ThumbGate ยท MIT License ยท npm v1.34.1</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>
|
package/public/numbers.html
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
29
|
-
"url": "https://thumbgate.
|
|
30
|
-
"dateModified": "2026-
|
|
28
|
+
"softwareVersion": "1.34.1",
|
|
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.
|
|
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-
|
|
61
|
-
"datePublished": "2026-
|
|
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":
|
|
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-
|
|
205
|
+
<div class="freshness">Updated: 2026-08-03 ยท Version 1.34.1</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">
|
|
214
|
-
<div class="stat-sub">
|
|
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-
|
|
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,20 @@ 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
|
-
/\
|
|
62
|
-
/\
|
|
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,
|
|
63
79
|
];
|
|
64
80
|
|
|
65
81
|
const SCREEN_TOOL_PATTERN = /(?:browser|computer|playwright|puppeteer|selenium|click|tap|press)/i;
|
|
@@ -156,14 +172,14 @@ function remoteFinancialLedgerAnchorStore(options = {}) {
|
|
|
156
172
|
return {
|
|
157
173
|
read({ ledgerId }) {
|
|
158
174
|
const response = invoke({ operation: 'read', ledgerId });
|
|
159
|
-
if (
|
|
175
|
+
if (response?.ok !== true || !Object.hasOwn(response, 'anchor')) {
|
|
160
176
|
throw financialError('remote financial anchor returned an invalid read response');
|
|
161
177
|
}
|
|
162
178
|
return response.anchor;
|
|
163
179
|
},
|
|
164
180
|
compareAndSet({ ledgerId, expected, next }) {
|
|
165
181
|
const response = invoke({ operation: 'compareAndSet', ledgerId, expected, next });
|
|
166
|
-
if (
|
|
182
|
+
if (response?.ok !== true || typeof response.applied !== 'boolean') {
|
|
167
183
|
throw financialError('remote financial anchor returned an invalid compare-and-set response');
|
|
168
184
|
}
|
|
169
185
|
return response.applied;
|
|
@@ -324,17 +340,28 @@ function detectEconomicAction(toolName, toolInput = {}) {
|
|
|
324
340
|
// approved; caller-supplied prose must never downgrade a blind click.
|
|
325
341
|
if (detectOpaqueScreenMutation(normalizedToolName, toolInput)) return true;
|
|
326
342
|
const command = shellEconomicText(normalizedToolName, toolInput.command || toolInput.cmd);
|
|
343
|
+
const screenMutationProse = isDeclaredScreenMutation(normalizedToolName, toolInput)
|
|
344
|
+
? [toolInput.goal, toolInput.description, toolInput.prompt, metadata.context]
|
|
345
|
+
: [];
|
|
327
346
|
const combined = [
|
|
328
347
|
normalizedToolName.replace(/[_-]+/g, ' '),
|
|
329
348
|
command,
|
|
330
|
-
toolInput.goal,
|
|
331
349
|
toolInput.action,
|
|
332
350
|
toolInput.operation,
|
|
333
|
-
|
|
351
|
+
...screenMutationProse,
|
|
334
352
|
].map((value) => String(value || '')).join(' ');
|
|
335
353
|
return ECONOMIC_ACTION_PATTERNS.some((pattern) => pattern.test(combined));
|
|
336
354
|
}
|
|
337
355
|
|
|
356
|
+
function isDeclaredScreenMutation(toolName, toolInput = {}) {
|
|
357
|
+
if (!SCREEN_TOOL_PATTERN.test(String(toolName || ''))) return false;
|
|
358
|
+
const input = objectValue(toolInput);
|
|
359
|
+
const declaredOperation = [input.action, input.operation, input.type]
|
|
360
|
+
.map((value) => String(value || ''))
|
|
361
|
+
.join(' ');
|
|
362
|
+
return SCREEN_MUTATION_PATTERN.test(toolName) || SCREEN_MUTATION_PATTERN.test(declaredOperation);
|
|
363
|
+
}
|
|
364
|
+
|
|
338
365
|
function detectOpaqueScreenMutation(toolName, toolInput = {}) {
|
|
339
366
|
const normalizedToolName = String(toolName || '').trim();
|
|
340
367
|
const input = objectValue(toolInput);
|
|
@@ -372,8 +399,8 @@ function shellEconomicText(toolName, rawCommand) {
|
|
|
372
399
|
return command
|
|
373
400
|
.split(/(?:&&|\|\||[;\n]|(?<!\|)\|(?!\|))/)
|
|
374
401
|
.map((segment) => {
|
|
375
|
-
const clean = segment.trim().replace(/^(?:sudo\s+)?(?:env\s+)?(?:[A-Za-z_]
|
|
376
|
-
const executable =
|
|
402
|
+
const clean = segment.trim().replace(/^(?:sudo\s+)?(?:env\s+)?(?:[A-Za-z_]\w*=[^\s]+\s+)*/, '');
|
|
403
|
+
const executable = /^([\w./-]+)/.exec(clean)?.[1]?.split('/').at(-1)?.toLowerCase();
|
|
377
404
|
if (['rg', 'grep', 'git', 'cat', 'head', 'tail', 'less', 'more', 'echo', 'printf'].includes(executable)) return '';
|
|
378
405
|
return clean;
|
|
379
406
|
})
|
|
@@ -780,40 +807,38 @@ function validateAttachedControl(result) {
|
|
|
780
807
|
}
|
|
781
808
|
}
|
|
782
809
|
|
|
783
|
-
function
|
|
784
|
-
|
|
785
|
-
|
|
810
|
+
function reconciliationHasIntegrityFailure(reconciliation) {
|
|
811
|
+
return !reconciliation.ok && (
|
|
812
|
+
reconciliation.invalidEventHashes.length > 0
|
|
786
813
|
|| reconciliation.invalidChainLinks.length > 0
|
|
787
814
|
|| reconciliation.ledgerHeadMismatches.length > 0
|
|
788
|
-
|| reconciliation.malformedRows.length > 0
|
|
789
|
-
|
|
790
|
-
|
|
815
|
+
|| reconciliation.malformedRows.length > 0
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function blockLedgerIntegrityFailures(result, reconciliation) {
|
|
820
|
+
const anchorUnavailable = reconciliation.ledgerHeadMismatches.some(
|
|
821
|
+
(entry) => optionalString(entry.rollbackResistantAnchorError)
|
|
822
|
+
);
|
|
823
|
+
const independentTampering = reconciliation.invalidEventHashes.length > 0
|
|
824
|
+
|| reconciliation.invalidChainLinks.length > 0
|
|
825
|
+
|| reconciliation.malformedRows.length > 0
|
|
826
|
+
|| reconciliation.ledgerHeadMismatches.some(
|
|
827
|
+
(entry) => !optionalString(entry.rollbackResistantAnchorError)
|
|
791
828
|
);
|
|
792
|
-
|
|
793
|
-
|
|
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;
|
|
829
|
+
if (independentTampering) {
|
|
830
|
+
addBlock(result, 'financial_ledger_tampered', 'Financial ledger integrity verification failed.');
|
|
809
831
|
}
|
|
810
|
-
if (
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
832
|
+
if (anchorUnavailable) {
|
|
833
|
+
addBlock(
|
|
834
|
+
result,
|
|
835
|
+
'financial_ledger_anchor_unavailable',
|
|
836
|
+
'Rollback-resistant financial ledger anchor is unavailable; financial actions fail closed.'
|
|
837
|
+
);
|
|
816
838
|
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function validateRequisitionBindings(result, requisition, options) {
|
|
817
842
|
try {
|
|
818
843
|
verifyPurchaseApprovalBinding(requisition, options);
|
|
819
844
|
} catch (error) {
|
|
@@ -824,6 +849,9 @@ function validateRequisition(result, options) {
|
|
|
824
849
|
} catch (error) {
|
|
825
850
|
addBlock(result, 'reservation_not_bound_to_approval', error.message);
|
|
826
851
|
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
function validateRequisitionConstraints(result, requisition) {
|
|
827
855
|
if (requisition.status !== 'reserved') {
|
|
828
856
|
addBlock(result, 'requisition_not_reserved', `Purchase requisition '${result.requisitionId}' is ${requisition.status}, not reserved.`);
|
|
829
857
|
}
|
|
@@ -846,6 +874,23 @@ function validateRequisition(result, options) {
|
|
|
846
874
|
if (normalizeToolName(result.actionBinding.toolName) !== normalizeToolName(requisition.approvedToolName)) {
|
|
847
875
|
addBlock(result, 'financial_tool_mismatch', 'Actual financial tool does not match the approved tool.');
|
|
848
876
|
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
function validateRequisition(result, options) {
|
|
880
|
+
const reconciliation = reconcilePurchaseLedger(options);
|
|
881
|
+
if (reconciliationHasIntegrityFailure(reconciliation)) {
|
|
882
|
+
blockLedgerIntegrityFailures(result, reconciliation);
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
if (!result.requisitionId) return;
|
|
886
|
+
const requisition = projectRequisition(result.requisitionId, options);
|
|
887
|
+
result.requisition = requisition;
|
|
888
|
+
if (!requisition) {
|
|
889
|
+
addBlock(result, 'unknown_purchase_requisition', `Purchase requisition '${result.requisitionId}' does not exist.`);
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
validateRequisitionBindings(result, requisition, options);
|
|
893
|
+
validateRequisitionConstraints(result, requisition);
|
|
849
894
|
validateScope(result, requisition);
|
|
850
895
|
}
|
|
851
896
|
|
|
@@ -897,7 +942,7 @@ function consumeReservation(input, options) {
|
|
|
897
942
|
const chain = validateLedgerChain(ledger.events, ledger.malformedRows, ledger.head, options);
|
|
898
943
|
if (!chain.ok) throw financialError('financial ledger integrity check failed before authorization');
|
|
899
944
|
const current = projectRequisitionFromEvents(ledger.events, input.requisitionId, options);
|
|
900
|
-
if (
|
|
945
|
+
if (current?.status !== 'reserved') {
|
|
901
946
|
throw financialError(`purchase requisition '${input.requisitionId}' is not available for single-use authorization`);
|
|
902
947
|
}
|
|
903
948
|
if (current.reservationId !== input.reservationId) {
|
|
@@ -1070,25 +1115,74 @@ function withLedgerLock(options, callback) {
|
|
|
1070
1115
|
});
|
|
1071
1116
|
}
|
|
1072
1117
|
|
|
1073
|
-
function
|
|
1074
|
-
const journalPath = getLedgerJournalPath(options);
|
|
1075
|
-
let journal;
|
|
1118
|
+
function readLedgerJournal(journalPath) {
|
|
1076
1119
|
try {
|
|
1077
|
-
|
|
1120
|
+
return JSON.parse(fs.readFileSync(journalPath, 'utf8'));
|
|
1078
1121
|
} catch (error) {
|
|
1079
|
-
if (error.code === 'ENOENT') return;
|
|
1122
|
+
if (error.code === 'ENOENT') return null;
|
|
1080
1123
|
throw financialError(`cannot recover financial ledger journal: ${error.message}`);
|
|
1081
1124
|
}
|
|
1082
|
-
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
function verifiedJournalEvent(journal, integrityKey) {
|
|
1128
|
+
const event = journal?.event;
|
|
1083
1129
|
if (journal?.schemaVersion !== LEDGER_JOURNAL_SCHEMA
|
|
1084
|
-
|| !
|
|
1085
|
-
||
|
|
1130
|
+
|| !event
|
|
1131
|
+
|| event.eventHash !== hashEvent(event)
|
|
1086
1132
|
|| !verifyIntegrityRecord(journal, integrityKey)) {
|
|
1087
1133
|
throw financialError('financial ledger journal integrity verification failed');
|
|
1088
1134
|
}
|
|
1135
|
+
return event;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
function recoverAppendedLedgerEvent({
|
|
1139
|
+
ledger,
|
|
1140
|
+
event,
|
|
1141
|
+
previousHead,
|
|
1142
|
+
currentAnchor,
|
|
1143
|
+
headAtPrevious,
|
|
1144
|
+
headAtEvent,
|
|
1145
|
+
journalPath,
|
|
1146
|
+
options,
|
|
1147
|
+
}) {
|
|
1148
|
+
const preceding = ledger.events.at(-2) || null;
|
|
1149
|
+
const expectedPrevious = preceding
|
|
1150
|
+
? { sequence: preceding.sequence, eventHash: preceding.eventHash }
|
|
1151
|
+
: null;
|
|
1152
|
+
const chain = validateLedgerChain(
|
|
1153
|
+
ledger.events,
|
|
1154
|
+
ledger.malformedRows,
|
|
1155
|
+
null,
|
|
1156
|
+
{ ...options, skipLedgerHead: true }
|
|
1157
|
+
);
|
|
1158
|
+
const anchorAtPrevious = sameHead(currentAnchor, previousHead);
|
|
1159
|
+
const anchorAtEvent = sameHead(currentAnchor, { sequence: event.sequence, eventHash: event.eventHash });
|
|
1160
|
+
if (!sameHead(previousHead, expectedPrevious)
|
|
1161
|
+
|| !chain.ok
|
|
1162
|
+
|| (!headAtPrevious && !headAtEvent)
|
|
1163
|
+
|| (!anchorAtPrevious && !anchorAtEvent)) {
|
|
1164
|
+
throw financialError('financial ledger journal does not match the recoverable append');
|
|
1165
|
+
}
|
|
1166
|
+
if (!headAtEvent) writeLedgerHeadFile(event, options);
|
|
1167
|
+
if (!anchorAtEvent) advanceFinancialLedgerAnchor(previousHead, event, options);
|
|
1168
|
+
removeDurableFile(journalPath);
|
|
1169
|
+
}
|
|
1089
1170
|
|
|
1171
|
+
function canDiscardPreparedLedgerEvent({ ledger, event, previousHead, currentAnchor, headAtPrevious, options }) {
|
|
1172
|
+
const currentChain = validateLedgerChain(ledger.events, ledger.malformedRows, ledger.head, options);
|
|
1173
|
+
return currentChain.ok
|
|
1174
|
+
&& headAtPrevious
|
|
1175
|
+
&& sameHead(currentAnchor, previousHead)
|
|
1176
|
+
&& event.sequence === ledger.events.length + 1;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
function recoverLedgerTransaction(options = {}) {
|
|
1180
|
+
const journalPath = getLedgerJournalPath(options);
|
|
1181
|
+
const journal = readLedgerJournal(journalPath);
|
|
1182
|
+
if (!journal) return;
|
|
1183
|
+
const integrityKey = ledgerIntegrityKey(options);
|
|
1184
|
+
const event = verifiedJournalEvent(journal, integrityKey);
|
|
1090
1185
|
const ledger = readLedger(options);
|
|
1091
|
-
const event = journal.event;
|
|
1092
1186
|
const currentLast = ledger.events.at(-1) || null;
|
|
1093
1187
|
const previousHead = journal.previousHead;
|
|
1094
1188
|
const currentHead = ledger.head;
|
|
@@ -1099,35 +1193,27 @@ function recoverLedgerTransaction(options = {}) {
|
|
|
1099
1193
|
const headAtEvent = sameHead(currentHead, { sequence: event.sequence, eventHash: event.eventHash });
|
|
1100
1194
|
|
|
1101
1195
|
if (eventAlreadyAppended) {
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
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);
|
|
1196
|
+
recoverAppendedLedgerEvent({
|
|
1197
|
+
ledger,
|
|
1198
|
+
event,
|
|
1199
|
+
previousHead,
|
|
1200
|
+
currentAnchor,
|
|
1201
|
+
headAtPrevious,
|
|
1202
|
+
headAtEvent,
|
|
1203
|
+
journalPath,
|
|
1204
|
+
options,
|
|
1205
|
+
});
|
|
1123
1206
|
return;
|
|
1124
1207
|
}
|
|
1125
1208
|
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1209
|
+
if (canDiscardPreparedLedgerEvent({
|
|
1210
|
+
ledger,
|
|
1211
|
+
event,
|
|
1212
|
+
previousHead,
|
|
1213
|
+
currentAnchor,
|
|
1214
|
+
headAtPrevious,
|
|
1215
|
+
options,
|
|
1216
|
+
})) {
|
|
1131
1217
|
// The crash happened before the event append. The caller never received a
|
|
1132
1218
|
// success response, so discard the prepared transaction instead of
|
|
1133
1219
|
// executing it during recovery.
|
|
@@ -1268,7 +1354,7 @@ function signIntegrityRecord(record, key) {
|
|
|
1268
1354
|
|
|
1269
1355
|
function verifyIntegrityRecord(record, key) {
|
|
1270
1356
|
const auth = record?.auth;
|
|
1271
|
-
if (!record ||
|
|
1357
|
+
if (!record || auth?.algorithm !== 'hmac-sha256') return false;
|
|
1272
1358
|
const expectedKeyId = crypto.createHash('sha256').update(key).digest('hex').slice(0, 16);
|
|
1273
1359
|
if (auth.keyId !== expectedKeyId || !/^[a-f0-9]{64}$/i.test(String(auth.signature || ''))) return false;
|
|
1274
1360
|
const expected = crypto.createHmac('sha256', key).update(integrityPayload(record)).digest();
|
|
@@ -1365,7 +1451,7 @@ function verifyPurchaseApprovalBinding(requisition, options) {
|
|
|
1365
1451
|
} catch (error) {
|
|
1366
1452
|
throw financialError(`requisition '${requisition.requisitionId}' approval verification failed: ${error.message}`);
|
|
1367
1453
|
}
|
|
1368
|
-
if (
|
|
1454
|
+
if (escalation?.status !== 'approved') {
|
|
1369
1455
|
throw financialError(`requisition '${requisition.requisitionId}' does not have independent human approval`);
|
|
1370
1456
|
}
|
|
1371
1457
|
const expectedApprovalContextDigest = requestComparableHash(requisition);
|
package/scripts/gates-engine.js
CHANGED
|
@@ -3387,8 +3387,9 @@ function evaluateUnconditionalHardFloor(input = {}, options = {}) {
|
|
|
3387
3387
|
|
|
3388
3388
|
function evaluateFinancialHardFloor(input = {}, consumeReservation = false, options = {}) {
|
|
3389
3389
|
const toolName = input.tool_name || input.toolName || 'unknown';
|
|
3390
|
-
const
|
|
3391
|
-
|
|
3390
|
+
const rawToolInput = input.tool_input ?? input.toolInput;
|
|
3391
|
+
const toolInput = rawToolInput && typeof rawToolInput === 'object'
|
|
3392
|
+
? rawToolInput
|
|
3392
3393
|
: {};
|
|
3393
3394
|
const normalizedAction = normalizeProviderAction({
|
|
3394
3395
|
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.
|
|
11
|
+
"version": "1.34.1",
|
|
12
12
|
"packages": [
|
|
13
13
|
{
|
|
14
14
|
"registryType": "npm",
|
|
15
15
|
"registryBaseUrl": "https://registry.npmjs.org",
|
|
16
16
|
"identifier": "thumbgate",
|
|
17
|
-
"version": "1.34.
|
|
17
|
+
"version": "1.34.1",
|
|
18
18
|
"runtimeHint": "npx",
|
|
19
19
|
"runtimeArguments": [
|
|
20
20
|
{
|