thumbgate 1.27.17 → 1.27.18
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/marketplace.json +85 -0
- package/.claude-plugin/plugin.json +3 -4
- package/.well-known/llms.txt +12 -33
- package/.well-known/mcp/server-card.json +8 -8
- package/README.md +30 -221
- package/adapters/chatgpt/openapi.yaml +1695 -0
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/gemini/function-declarations.json +0 -1
- package/adapters/mcp/server-stdio.js +11 -263
- package/adapters/opencode/opencode.json +1 -1
- package/bench/thumbgate-bench.json +2 -2
- package/bin/cli.js +122 -1421
- package/bin/postinstall.js +27 -11
- package/config/gate-templates.json +0 -216
- package/config/gates/claim-verification.json +0 -12
- package/config/gates/default.json +2 -31
- package/config/github-about.json +2 -2
- package/config/mcp-allowlists.json +13 -23
- package/config/merge-quality-checks.json +1 -0
- package/config/model-candidates.json +6 -121
- package/openapi/openapi.yaml +0 -12
- package/package.json +99 -229
- package/public/blog.html +4 -4
- package/public/codex-plugin.html +20 -72
- package/public/compare.html +8 -31
- package/public/dashboard.html +166 -930
- package/public/federal.html +2 -2
- package/public/guide.html +9 -33
- package/public/index.html +116 -495
- package/public/learn.html +18 -191
- package/public/lessons.html +10 -168
- package/public/numbers.html +7 -7
- package/public/pro.html +11 -34
- package/scripts/agent-memory-lifecycle.js +0 -211
- package/scripts/agent-readiness.js +3 -167
- package/scripts/agent-reward-model.js +1 -53
- package/scripts/auto-promote-gates.js +10 -82
- package/scripts/auto-wire-hooks.js +0 -14
- package/scripts/billing.js +1 -93
- package/scripts/build-metadata.js +10 -50
- package/scripts/cli-feedback.js +6 -18
- package/scripts/cli-schema.js +0 -90
- package/scripts/cli-telemetry.js +1 -6
- package/scripts/commercial-offer.js +2 -82
- package/scripts/context-manager.js +6 -74
- package/scripts/dashboard.js +2 -68
- package/scripts/durability/step.js +3 -3
- package/scripts/export-databricks-bundle.js +1 -5
- package/scripts/export-dpo-pairs.js +2 -7
- package/scripts/feedback-loop.js +1 -145
- package/scripts/feedback-quality.js +0 -87
- package/scripts/filesystem-search.js +10 -35
- package/scripts/gate-stats.js +0 -89
- package/scripts/gates-engine.js +85 -1105
- package/scripts/gemini-embedding-policy.js +1 -2
- package/scripts/hook-runtime.js +14 -20
- package/scripts/hook-thumbgate-cache-updater.js +2 -18
- package/scripts/hybrid-feedback-context.js +7 -142
- package/scripts/lesson-inference.js +3 -8
- package/scripts/lesson-search.js +1 -17
- package/scripts/license.js +1 -0
- package/scripts/llm-client.js +9 -390
- package/scripts/local-model-profile.js +8 -15
- package/scripts/memory-scope-readiness.js +0 -159
- package/scripts/meta-agent-loop.js +0 -36
- package/scripts/operational-integrity.js +5 -39
- package/scripts/oss-pr-opportunity-scout.js +5 -35
- package/scripts/plausible-server-events.js +6 -9
- package/scripts/pro-local-dashboard.js +4 -4
- package/scripts/proxy-pointer-rag-guardrails.js +1 -42
- package/scripts/rate-limiter.js +13 -64
- package/scripts/secret-scanner.js +5 -44
- package/scripts/security-scanner.js +10 -260
- package/scripts/self-distill-agent.js +1 -3
- package/scripts/seo-gsd.js +2 -916
- package/scripts/statusline-cache-path.js +2 -17
- package/scripts/statusline-local-stats.js +1 -9
- package/scripts/statusline-meta.js +2 -28
- package/scripts/statusline.sh +4 -20
- package/scripts/telemetry-analytics.js +0 -357
- package/scripts/thompson-sampling.js +10 -31
- package/scripts/thumbgate-bench.js +1 -16
- package/scripts/thumbgate-search.js +19 -85
- package/scripts/tool-registry.js +1 -169
- package/scripts/vector-store.js +0 -45
- package/scripts/workflow-sentinel.js +53 -286
- package/scripts/workspace-evolver.js +2 -62
- package/src/api/server.js +290 -2578
- package/.claude/commands/dashboard.md +0 -15
- package/.claude/commands/thumbgate-blocked.md +0 -27
- package/.claude/commands/thumbgate-dashboard.md +0 -15
- package/.claude/commands/thumbgate-doctor.md +0 -30
- package/.claude/commands/thumbgate-guard.md +0 -36
- package/.claude/commands/thumbgate-protect.md +0 -30
- package/.claude/commands/thumbgate-rules.md +0 -30
- package/.well-known/agentic-verify.txt +0 -1
- package/adapters/gcp/dfcx-webhook-gate.js +0 -295
- package/adapters/letta/README.md +0 -41
- package/adapters/letta/thumbgate-letta-adapter.js +0 -133
- package/adapters/policy-engine/ethicore-guardian-client.js +0 -68
- package/adapters/policy-engine/thumbgate-policy-engine-adapter.js +0 -260
- package/bench/observability-eval-suite.json +0 -26
- package/bin/dashboard-cli.js +0 -7
- package/commands/dashboard.md +0 -15
- package/commands/thumbgate-dashboard.md +0 -15
- package/config/builtin-lessons.json +0 -23
- package/config/gate-classifier-routing.json +0 -98
- package/config/post-deploy-marketing-pages.json +0 -61
- package/public/about.html +0 -162
- package/public/agent-manager.html +0 -179
- package/public/agents-cost-savings.html +0 -153
- package/public/ai-malpractice-prevention.html +0 -818
- package/public/assets/brand/github-social-preview.png +0 -0
- package/public/assets/brand/thumbgate-icon-512.png +0 -0
- package/public/assets/brand/thumbgate-icon-pro-512.png +0 -0
- package/public/assets/brand/thumbgate-icon-team-512.png +0 -0
- package/public/assets/brand/thumbgate-logo-1200x360.png +0 -0
- package/public/assets/brand/thumbgate-logo-transparent.svg +0 -22
- package/public/assets/brand/thumbgate-mark-inline-v3.svg +0 -19
- package/public/assets/brand/thumbgate-mark-pro.svg +0 -23
- package/public/assets/brand/thumbgate-mark-team.svg +0 -26
- package/public/assets/brand/thumbgate-mark.svg +0 -21
- package/public/assets/brand/thumbgate-wordmark.svg +0 -20
- package/public/assets/claude-thumbgate-statusbar.svg +0 -8
- package/public/assets/codex-thumbgate-statusbar-test.svg +0 -9
- package/public/assets/legal-intake-control-flow.svg +0 -66
- package/public/brand/thumbgate-mark.svg +0 -19
- package/public/brand/thumbgate-og.svg +0 -16
- package/public/chatgpt-app.html +0 -330
- package/public/codex-enterprise.html +0 -123
- package/public/js/buyer-intent.js +0 -672
- package/public/pricing.html +0 -399
- package/scripts/action-receipts.js +0 -324
- package/scripts/activation-quickstart.js +0 -187
- package/scripts/agent-operations-planner.js +0 -621
- package/scripts/ai-component-inventory.js +0 -367
- package/scripts/async-eval-observability.js +0 -236
- package/scripts/audit.js +0 -65
- package/scripts/aws-blocks-guardrails.js +0 -228
- package/scripts/classifier-routing.js +0 -130
- package/scripts/dashboard-chat.js +0 -332
- package/scripts/feedback-aggregate.js +0 -281
- package/scripts/feedback-sanitizer.js +0 -105
- package/scripts/hob-pack.js +0 -591
- package/scripts/hook-stop-anti-claim.js +0 -301
- package/scripts/install-shim.js +0 -87
- package/scripts/mcp-oauth.js +0 -293
- package/scripts/noop-detect.js +0 -285
- package/scripts/omlx-smoke.js +0 -192
- package/scripts/parallel-workflow-orchestrator.js +0 -293
- package/scripts/plan-gate.js +0 -243
- package/scripts/plausible-domain-config.js +0 -99
- package/scripts/qa-scenario-planner.js +0 -136
- package/scripts/repeat-metric.js +0 -137
- package/scripts/secret-fixture-tokens.js +0 -61
- package/scripts/secret-redaction.js +0 -166
- package/scripts/self-harness-optimizer.js +0 -141
- package/scripts/self-healing-check.js +0 -193
- package/scripts/silent-failure-cluster.js +0 -531
- package/scripts/statusline-cache-read.js +0 -57
- package/scripts/sync-telemetry-from-prod.js +0 -374
- package/scripts/tool-contract-validator.js +0 -76
- package/scripts/trajectory-scorer.js +0 -63
- package/scripts/verify-marketing-pages-deployed.js +0 -195
- package/scripts/visitor-journey.js +0 -172
|
@@ -1,301 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Stop hook: anti-claim enforcement.
|
|
6
|
-
*
|
|
7
|
-
* Scans the assistant's most recent turn (assistant text + same-turn tool_use
|
|
8
|
-
* blocks) and blocks the "deployed / live / done / fixed / ready" claim
|
|
9
|
-
* unless that same turn included a proof tool call (curl / grep / test).
|
|
10
|
-
*
|
|
11
|
-
* Why: CLAUDE.md anti-lying directive ("Never claim fix done until
|
|
12
|
-
* committed+pushed. Never claim 'ready' without running e2e.") was
|
|
13
|
-
* aspirational, not enforced. Per CEO 2026-05-13 feedback after a session in
|
|
14
|
-
* which 5+ unverified claims slipped through, this is the harness-level
|
|
15
|
-
* enforcement that ends the recurring trust-burn pattern. ThumbGate-on-
|
|
16
|
-
* ThumbGate dogfood — we are the prevention-rule generator and a perfect
|
|
17
|
-
* customer for our own gate.
|
|
18
|
-
*
|
|
19
|
-
* Wires through .claude/settings.json Stop hooks list. Always exits 0
|
|
20
|
-
* (informational): the goal is to surface a system reminder in the next
|
|
21
|
-
* turn so the agent corrects mid-conversation rather than to hard-block
|
|
22
|
-
* the turn that already happened.
|
|
23
|
-
*
|
|
24
|
-
* Stdin: Claude Code passes the hook payload as JSON on stdin. We read
|
|
25
|
-
* `transcript_path` to locate the JSONL session log and scan the last
|
|
26
|
-
* assistant message.
|
|
27
|
-
*
|
|
28
|
-
* Stdout: any text printed is shown to the agent on the next turn.
|
|
29
|
-
*/
|
|
30
|
-
|
|
31
|
-
const fs = require('node:fs');
|
|
32
|
-
|
|
33
|
-
// Lie-phrase patterns. These match common "claim of completion" wording
|
|
34
|
-
// the agent emits without verification. Word-boundary anchored to avoid
|
|
35
|
-
// false positives ("ready-made", "live-streaming", etc).
|
|
36
|
-
const CLAIM_PATTERNS = [
|
|
37
|
-
/\bis\s+live\b/i,
|
|
38
|
-
/\bnow\s+live\b/i,
|
|
39
|
-
/\bgoing\s+live\b/i,
|
|
40
|
-
/\bdeployed\b(?!\s*(yet|to\s+staging|on\s+a\s+branch))/i,
|
|
41
|
-
/\b(?:is|are|it'?s)\s+(?:now\s+)?(?:fully\s+)?(?:fixed|resolved|merged|shipped)\b/i,
|
|
42
|
-
/\bproduction[-\s]ready\b/i,
|
|
43
|
-
/\beverything\s+(?:is\s+)?(?:done|working|ready)\b/i,
|
|
44
|
-
/\b(?:github|repo|repository)\s+(?:about|metadata|description|topics?)\b.*\b(?:updated|verified|fixed|match(?:es|ed)?)\b/i,
|
|
45
|
-
/\b(?:about|metadata|description|topics?)\b.*\b(?:updated|verified|fixed|match(?:es|ed)?)\b.*\b(?:github|repo|repository)\b/i,
|
|
46
|
-
// Added 2026-06-11 after a cross-project failure analysis: these completion
|
|
47
|
-
// claims ("all green / stable / verified / race over / tests pass") were
|
|
48
|
-
// asserted without proof and slipped past the original set. The proof-gate
|
|
49
|
-
// below suppresses them whenever the SAME turn ran a verification tool, so a
|
|
50
|
-
// "verified" claim backed by a test/curl/Read stays silent.
|
|
51
|
-
/\b(?:all\s+)?(?:tests?|checks?|ci)\s+(?:are\s+)?(?:now\s+)?passing\b/i,
|
|
52
|
-
/\ball\s+(?:tests?|checks?)\s+pass(?:ed)?\b/i,
|
|
53
|
-
/\bverified\b/i,
|
|
54
|
-
/\bconfirmed\b/i,
|
|
55
|
-
/\b(?:is|are|it'?s|now)\s+stable\b/i,
|
|
56
|
-
/\ball\s+clear\b/i,
|
|
57
|
-
/\bgood\s+to\s+go\b/i,
|
|
58
|
-
/\brace\s+(?:is\s+)?over\b/i,
|
|
59
|
-
/\bno\s+longer\s+racing\b/i,
|
|
60
|
-
];
|
|
61
|
-
|
|
62
|
-
// Proof-of-verification patterns. If the SAME turn included one of these
|
|
63
|
-
// tool calls or shell command tokens, the claim is considered backed and
|
|
64
|
-
// the hook stays silent.
|
|
65
|
-
const PROOF_PATTERNS = [
|
|
66
|
-
/\bcurl\b/,
|
|
67
|
-
/\bgh\s+pr\s+(?:view|checks|status)\b/,
|
|
68
|
-
/\bgh\s+run\s+view\b/,
|
|
69
|
-
/\bgh\s+api\b/,
|
|
70
|
-
/\bnode\s+--test\b/,
|
|
71
|
-
/\bnpm\s+(?:run\s+)?test\b/,
|
|
72
|
-
/\bnpm\s+pack\b/,
|
|
73
|
-
/\bjest\b/,
|
|
74
|
-
/\bmocha\b/,
|
|
75
|
-
/\bpytest\b/,
|
|
76
|
-
/\bplaywright\b/,
|
|
77
|
-
/\bgrep\b/,
|
|
78
|
-
/\bstripe\b/,
|
|
79
|
-
/\bplaid\b/,
|
|
80
|
-
/\bshopify\b/,
|
|
81
|
-
/\bsquare\b/,
|
|
82
|
-
/\bquickbooks\b/,
|
|
83
|
-
/Read\s*\(/, // Claude Code Read tool call
|
|
84
|
-
/Bash\s*\(/, // Claude Code Bash tool call
|
|
85
|
-
];
|
|
86
|
-
|
|
87
|
-
const COMMERCIAL_CLAIM_SUBJECTS = [
|
|
88
|
-
'money',
|
|
89
|
-
'payment',
|
|
90
|
-
'charge',
|
|
91
|
-
'checkout',
|
|
92
|
-
'revenue',
|
|
93
|
-
'price',
|
|
94
|
-
'pricing',
|
|
95
|
-
'invoice',
|
|
96
|
-
'billing',
|
|
97
|
-
'tax',
|
|
98
|
-
'sales tax',
|
|
99
|
-
'inventory',
|
|
100
|
-
'stock',
|
|
101
|
-
'permission',
|
|
102
|
-
'access',
|
|
103
|
-
'customer facing',
|
|
104
|
-
];
|
|
105
|
-
|
|
106
|
-
const COMMERCIAL_CLAIM_STATES = [
|
|
107
|
-
'correct',
|
|
108
|
-
'accurate',
|
|
109
|
-
'verified',
|
|
110
|
-
'valid',
|
|
111
|
-
'matches',
|
|
112
|
-
'working',
|
|
113
|
-
'fixed',
|
|
114
|
-
'resolved',
|
|
115
|
-
'calculated',
|
|
116
|
-
'configured',
|
|
117
|
-
];
|
|
118
|
-
|
|
119
|
-
const GREEN_CLAIM_PHRASES = [
|
|
120
|
-
'all green',
|
|
121
|
-
'all tests green',
|
|
122
|
-
'all checks green',
|
|
123
|
-
'all the tests green',
|
|
124
|
-
'all the checks green',
|
|
125
|
-
'all tests are green',
|
|
126
|
-
'all checks are green',
|
|
127
|
-
];
|
|
128
|
-
|
|
129
|
-
function normalizeClaimText(text) {
|
|
130
|
-
return String(text || '')
|
|
131
|
-
.toLowerCase()
|
|
132
|
-
.replace(/[-_]+/g, ' ')
|
|
133
|
-
.replace(/[^a-z0-9]+/g, ' ')
|
|
134
|
-
.replace(/\s+/g, ' ')
|
|
135
|
-
.trim();
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function containsPhrase(normalizedText, phrase) {
|
|
139
|
-
return ` ${normalizedText} `.includes(` ${phrase} `);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function findTokenListClaim(text, subjects, states, label) {
|
|
143
|
-
const normalized = normalizeClaimText(text);
|
|
144
|
-
if (!normalized) return null;
|
|
145
|
-
const subject = subjects.find((candidate) => containsPhrase(normalized, candidate));
|
|
146
|
-
const state = states.find((candidate) => containsPhrase(normalized, candidate));
|
|
147
|
-
return subject && state ? `${label}: ${subject} ${state}` : null;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function findGreenClaim(text) {
|
|
151
|
-
const normalized = normalizeClaimText(text);
|
|
152
|
-
return GREEN_CLAIM_PHRASES.find((phrase) => containsPhrase(normalized, phrase)) || null;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function readLastAssistantTurn(transcriptPath) {
|
|
156
|
-
if (!transcriptPath || !fs.existsSync(transcriptPath)) return null;
|
|
157
|
-
let content;
|
|
158
|
-
try {
|
|
159
|
-
content = fs.readFileSync(transcriptPath, 'utf8');
|
|
160
|
-
} catch {
|
|
161
|
-
return null;
|
|
162
|
-
}
|
|
163
|
-
const lines = content.trim().split('\n');
|
|
164
|
-
// Walk backwards to find the last assistant message
|
|
165
|
-
for (let i = lines.length - 1; i >= 0; i--) {
|
|
166
|
-
const raw = lines[i].trim();
|
|
167
|
-
if (!raw) continue;
|
|
168
|
-
let entry;
|
|
169
|
-
try {
|
|
170
|
-
entry = JSON.parse(raw);
|
|
171
|
-
} catch {
|
|
172
|
-
continue;
|
|
173
|
-
}
|
|
174
|
-
if (entry.type === 'assistant' && entry.message) {
|
|
175
|
-
return entry.message;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
return null;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function extractText(message) {
|
|
182
|
-
if (!message || !Array.isArray(message.content)) return '';
|
|
183
|
-
return message.content
|
|
184
|
-
.filter((b) => b && typeof b.text === 'string')
|
|
185
|
-
.map((b) => b.text)
|
|
186
|
-
.join('\n');
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function extractToolUseSummary(message) {
|
|
190
|
-
if (!message || !Array.isArray(message.content)) return '';
|
|
191
|
-
return message.content
|
|
192
|
-
.filter((b) => b?.type === 'tool_use')
|
|
193
|
-
.map((b) => {
|
|
194
|
-
const name = b.name || 'tool';
|
|
195
|
-
let summary = '';
|
|
196
|
-
if (b.input && typeof b.input === 'object') {
|
|
197
|
-
if (typeof b.input.command === 'string') summary = b.input.command;
|
|
198
|
-
else if (typeof b.input.file_path === 'string') summary = b.input.file_path;
|
|
199
|
-
else if (typeof b.input.query === 'string') summary = b.input.query;
|
|
200
|
-
else summary = JSON.stringify(b.input).slice(0, 200);
|
|
201
|
-
}
|
|
202
|
-
return `${name}: ${summary}`;
|
|
203
|
-
})
|
|
204
|
-
.join('\n');
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function findClaim(text) {
|
|
208
|
-
const commercialClaim = findTokenListClaim(
|
|
209
|
-
text,
|
|
210
|
-
COMMERCIAL_CLAIM_SUBJECTS,
|
|
211
|
-
COMMERCIAL_CLAIM_STATES,
|
|
212
|
-
'commercial truth',
|
|
213
|
-
);
|
|
214
|
-
if (commercialClaim) return commercialClaim;
|
|
215
|
-
const greenClaim = findGreenClaim(text);
|
|
216
|
-
if (greenClaim) return greenClaim;
|
|
217
|
-
for (const p of CLAIM_PATTERNS) {
|
|
218
|
-
const m = text.match(p);
|
|
219
|
-
if (m) return m[0];
|
|
220
|
-
}
|
|
221
|
-
return null;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function hasProof(combined) {
|
|
225
|
-
return PROOF_PATTERNS.some((p) => p.test(combined));
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function readStdinSync() {
|
|
229
|
-
try {
|
|
230
|
-
return fs.readFileSync(0, 'utf8');
|
|
231
|
-
} catch {
|
|
232
|
-
return '';
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function main() {
|
|
237
|
-
const raw = readStdinSync();
|
|
238
|
-
let payload = {};
|
|
239
|
-
try {
|
|
240
|
-
payload = raw ? JSON.parse(raw) : {};
|
|
241
|
-
} catch {
|
|
242
|
-
payload = {};
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const transcriptPath = payload.transcript_path || process.env.CLAUDE_TRANSCRIPT_PATH;
|
|
246
|
-
const message = readLastAssistantTurn(transcriptPath);
|
|
247
|
-
if (!message) return; // no transcript visible; nothing to check
|
|
248
|
-
|
|
249
|
-
const text = extractText(message);
|
|
250
|
-
const toolUseSummary = extractToolUseSummary(message);
|
|
251
|
-
const claim = findClaim(text);
|
|
252
|
-
if (!claim) return; // no completion claim made; silent
|
|
253
|
-
|
|
254
|
-
const proofText = `${text}\n${toolUseSummary}`;
|
|
255
|
-
if (hasProof(proofText)) return; // claim backed by proof in same turn
|
|
256
|
-
|
|
257
|
-
// Strict mode (THUMBGATE_STRICT_ENFORCEMENT=1): hard-block the stop. Emit a
|
|
258
|
-
// Stop-hook block decision so Claude Code does NOT end the turn — the agent
|
|
259
|
-
// must run the verification (or retract) before it can stop. Default mode
|
|
260
|
-
// stays soft (a reminder for the next turn) so we don't break existing wiring.
|
|
261
|
-
if (process.env.THUMBGATE_STRICT_ENFORCEMENT === '1') {
|
|
262
|
-
const reason = `ThumbGate anti-claim gate (strict): you claimed completion ("${claim}") without a proof tool call in the same message. Run the verification (curl / grep / test / Read) and restate with the proof, or retract — do not end the turn on an unverified claim.`;
|
|
263
|
-
process.stdout.write(JSON.stringify({ decision: 'block', reason }) + '\n');
|
|
264
|
-
return;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// Default (soft): surface a system reminder for the NEXT turn. Do not hard-block.
|
|
268
|
-
const reminder = [
|
|
269
|
-
'⚠️ ThumbGate anti-claim gate: previous turn claimed completion',
|
|
270
|
-
` ("${claim}") without a proof tool call in the same message.`,
|
|
271
|
-
' Per CLAUDE.md anti-lying: never claim "done / live / deployed / fixed /',
|
|
272
|
-
' verified / all green / stable" or commercial truth (money / tax / inventory /',
|
|
273
|
-
' permissions / customer-facing state) without curl / grep / test / source-of-truth output in the SAME turn.',
|
|
274
|
-
' If the work really is verified, re-state the claim with the proof.',
|
|
275
|
-
' If not, retract and run the verification before re-asserting.',
|
|
276
|
-
].join('\n');
|
|
277
|
-
process.stdout.write(reminder + '\n');
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// Path-resolve check instead of `require.main === module`. SonarCloud's
|
|
281
|
-
// strict type inference (rule S3403) flags the === form as always-false
|
|
282
|
-
// in CommonJS, and CLAUDE.md "Hard-Won Lessons" pins the path-based form
|
|
283
|
-
// as the portable fix (incident 2026-04-21 / PR #1115). Resolve BOTH sides
|
|
284
|
-
// so the comparison is between two normalized absolute paths.
|
|
285
|
-
const path = require('node:path');
|
|
286
|
-
if (path.resolve(process.argv[1] || '') === path.resolve(__filename)) {
|
|
287
|
-
try {
|
|
288
|
-
main();
|
|
289
|
-
} catch {
|
|
290
|
-
// never crash the agent
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
module.exports = {
|
|
295
|
-
CLAIM_PATTERNS,
|
|
296
|
-
PROOF_PATTERNS,
|
|
297
|
-
findClaim,
|
|
298
|
-
hasProof,
|
|
299
|
-
extractText,
|
|
300
|
-
extractToolUseSummary,
|
|
301
|
-
};
|
package/scripts/install-shim.js
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* install-shim.js — Install a stable shim at ~/.thumbgate/bin/thumbgate-hook
|
|
5
|
-
*
|
|
6
|
-
* The shim is a tiny shell script that resolves the cached ThumbGate runtime
|
|
7
|
-
* first, so hook commands in settings.local.json stay stable across projects
|
|
8
|
-
* and agent restarts.
|
|
9
|
-
*
|
|
10
|
-
* The shim checks for a cached runtime binary first (fast path), and falls
|
|
11
|
-
* back to `npx --yes thumbgate@latest` (slow path, first-time self-install).
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
const fs = require('fs');
|
|
15
|
-
const path = require('path');
|
|
16
|
-
const os = require('os');
|
|
17
|
-
|
|
18
|
-
const SHIM_DIR = path.join(os.homedir(), '.thumbgate', 'bin');
|
|
19
|
-
const SHIM_PATH = path.join(SHIM_DIR, 'thumbgate-hook');
|
|
20
|
-
const RUNTIME_BIN = path.join(os.homedir(), '.thumbgate', 'runtime', 'node_modules', '.bin', 'thumbgate');
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* The shim script. Key design choices:
|
|
24
|
-
* - Uses `exec` to replace the shell process (no zombie processes)
|
|
25
|
-
* - Fast path: if cached runtime binary exists, exec it directly
|
|
26
|
-
* - Slow path: npx --yes thumbgate@latest (first-time auto-installs)
|
|
27
|
-
* - No default self-mutation: background upgrades are opt-in via
|
|
28
|
-
* THUMBGATE_SHIM_AUTO_UPDATE=1 so source checkouts, enterprise pins, and
|
|
29
|
-
* dogfood runtimes cannot be overwritten by a hook side effect.
|
|
30
|
-
*/
|
|
31
|
-
function shimContent() {
|
|
32
|
-
const escapedRuntimeBin = JSON.stringify(RUNTIME_BIN);
|
|
33
|
-
const escapedRuntimeDir = JSON.stringify(path.join(os.homedir(), '.thumbgate', 'runtime'));
|
|
34
|
-
|
|
35
|
-
return `#!/usr/bin/env bash
|
|
36
|
-
# ThumbGate hook shim — DO NOT EDIT
|
|
37
|
-
# Installed by: thumbgate init
|
|
38
|
-
# Purpose: stable hook entry point that runs the cached ThumbGate runtime
|
|
39
|
-
# Pattern: Volta-style stable shim (see https://volta.sh)
|
|
40
|
-
|
|
41
|
-
set -euo pipefail
|
|
42
|
-
|
|
43
|
-
RUNTIME_BIN=${escapedRuntimeBin}
|
|
44
|
-
RUNTIME_DIR=${escapedRuntimeDir}
|
|
45
|
-
|
|
46
|
-
# Fast path: cached runtime binary exists and is executable
|
|
47
|
-
if [ -x "$RUNTIME_BIN" ]; then
|
|
48
|
-
# Optional background upgrade. Disabled by default so hooks never mutate a
|
|
49
|
-
# source checkout, enterprise pin, or dogfood runtime behind the operator's back.
|
|
50
|
-
if [ "\${THUMBGATE_SHIM_AUTO_UPDATE:-0}" = "1" ]; then
|
|
51
|
-
( nohup npm install --prefix "$RUNTIME_DIR" --no-save --omit=dev thumbgate@latest >/dev/null 2>&1 & ) 2>/dev/null || true
|
|
52
|
-
fi
|
|
53
|
-
exec "$RUNTIME_BIN" "$@"
|
|
54
|
-
fi
|
|
55
|
-
|
|
56
|
-
# Slow path: no cached binary — install + exec via npx
|
|
57
|
-
mkdir -p "$RUNTIME_DIR"
|
|
58
|
-
exec npx --yes --package thumbgate@latest -- thumbgate "$@"
|
|
59
|
-
`;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function installShim() {
|
|
63
|
-
fs.mkdirSync(SHIM_DIR, { recursive: true });
|
|
64
|
-
fs.writeFileSync(SHIM_PATH, shimContent(), { mode: 0o755 });
|
|
65
|
-
return SHIM_PATH;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function shimInstalled() {
|
|
69
|
-
try {
|
|
70
|
-
return fs.existsSync(SHIM_PATH) && (fs.statSync(SHIM_PATH).mode & 0o111) !== 0;
|
|
71
|
-
} catch {
|
|
72
|
-
return false;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function shimPath() {
|
|
77
|
-
return SHIM_PATH;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
module.exports = {
|
|
81
|
-
installShim,
|
|
82
|
-
shimInstalled,
|
|
83
|
-
shimPath,
|
|
84
|
-
shimContent,
|
|
85
|
-
SHIM_DIR,
|
|
86
|
-
SHIM_PATH,
|
|
87
|
-
};
|
package/scripts/mcp-oauth.js
DELETED
|
@@ -1,293 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* mcp-oauth.js — OAuth 2.1 (PKCE) authorization-server machinery for the remote
|
|
6
|
-
* ThumbGate MCP connector, required by the Claude Connectors Directory
|
|
7
|
-
* ("Use OAuth 2.0 for authenticated services").
|
|
8
|
-
*
|
|
9
|
-
* Pure, dependency-free, IO-free functions + an injectable store, so the full
|
|
10
|
-
* flow is unit-testable without a network. The HTTP wiring (in src/api/server.js)
|
|
11
|
-
* is a thin shell over these.
|
|
12
|
-
*
|
|
13
|
-
* Implements:
|
|
14
|
-
* - RFC 9728 protected-resource metadata
|
|
15
|
-
* - RFC 8414 authorization-server metadata
|
|
16
|
-
* - RFC 7591 dynamic client registration (open registration)
|
|
17
|
-
* - Authorization-code grant with mandatory PKCE S256 (RFC 7636)
|
|
18
|
-
* - Bearer access tokens bound to a ThumbGate API key (so existing gating is unchanged)
|
|
19
|
-
*
|
|
20
|
-
* SECURITY NOTES:
|
|
21
|
-
* - PKCE S256 is mandatory; the plain method is rejected.
|
|
22
|
-
* - Auth codes are single-use and short-lived (60s); access tokens TTL-bound.
|
|
23
|
-
* - redirect_uri is matched exactly against the registered set.
|
|
24
|
-
* - The token never exposes the bound ThumbGate key; it maps server-side only.
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
const crypto = require('crypto');
|
|
28
|
-
|
|
29
|
-
const AUTH_CODE_TTL_MS = 60 * 1000; // 1 minute
|
|
30
|
-
const ACCESS_TOKEN_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
31
|
-
const DEFAULT_SCOPE = 'mcp:read mcp:write';
|
|
32
|
-
|
|
33
|
-
// Upper bounds on the in-memory store. The registration and authorization
|
|
34
|
-
// endpoints are reachable pre-auth, so without a cap a malicious caller could
|
|
35
|
-
// grow these Maps unboundedly and exhaust server memory. When a Map is full we
|
|
36
|
-
// evict the oldest entry (FIFO) rather than deny service to legitimate clients.
|
|
37
|
-
const MAX_CLIENTS = 10000;
|
|
38
|
-
const MAX_CODES = 10000;
|
|
39
|
-
const MAX_TOKENS = 50000;
|
|
40
|
-
|
|
41
|
-
function now() {
|
|
42
|
-
return Date.now();
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function randomToken(bytes = 32) {
|
|
46
|
-
return crypto.randomBytes(bytes).toString('base64url');
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function base64UrlSha256(input) {
|
|
50
|
-
return crypto.createHash('sha256').update(String(input)).digest('base64url');
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Create a fresh in-memory store.
|
|
55
|
-
*
|
|
56
|
-
* DURABILITY (known limitation): this uses plain Maps, so a process restart or a
|
|
57
|
-
* multi-instance / load-balanced deployment will drop issued tokens and
|
|
58
|
-
* registered clients (clients see 401s, in-flight authorizations break). For
|
|
59
|
-
* single-instance use this is fine; production multi-tenancy needs a durable,
|
|
60
|
-
* shared backing (Redis/DB) — tracked as the per-user-data-scoping follow-up.
|
|
61
|
-
* Entry counts are bounded (see MAX_* and capInsert) to prevent memory
|
|
62
|
-
* exhaustion from anonymous calls to the registration/authorization endpoints.
|
|
63
|
-
*/
|
|
64
|
-
function createStore() {
|
|
65
|
-
return {
|
|
66
|
-
clients: new Map(),
|
|
67
|
-
codes: new Map(),
|
|
68
|
-
tokens: new Map(),
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/** Insert into a Map, evicting the oldest entry (FIFO) once `max` is reached. */
|
|
73
|
-
function capInsert(map, key, value, max) {
|
|
74
|
-
if (map.size >= max && !map.has(key)) {
|
|
75
|
-
const oldest = map.keys().next().value;
|
|
76
|
-
if (oldest !== undefined) map.delete(oldest);
|
|
77
|
-
}
|
|
78
|
-
map.set(key, value);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// ---------------------------------------------------------------------------
|
|
82
|
-
// Metadata (RFC 9728 / RFC 8414)
|
|
83
|
-
// ---------------------------------------------------------------------------
|
|
84
|
-
|
|
85
|
-
function trimSlash(u) {
|
|
86
|
-
// Non-regex trailing-slash strip (avoids a SonarCloud S5852 false-positive on a
|
|
87
|
-
// provably-linear pattern).
|
|
88
|
-
let s = String(u || '');
|
|
89
|
-
while (s.endsWith('/')) s = s.slice(0, -1);
|
|
90
|
-
return s;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function buildProtectedResourceMetadata(baseUrl) {
|
|
94
|
-
const base = trimSlash(baseUrl);
|
|
95
|
-
return {
|
|
96
|
-
resource: `${base}/mcp`,
|
|
97
|
-
authorization_servers: [base],
|
|
98
|
-
bearer_methods_supported: ['header'],
|
|
99
|
-
scopes_supported: ['mcp:read', 'mcp:write'],
|
|
100
|
-
resource_documentation: `${base}/docs/connectors`,
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function buildAuthServerMetadata(baseUrl) {
|
|
105
|
-
const base = trimSlash(baseUrl);
|
|
106
|
-
return {
|
|
107
|
-
issuer: base,
|
|
108
|
-
authorization_endpoint: `${base}/oauth/authorize`,
|
|
109
|
-
token_endpoint: `${base}/oauth/token`,
|
|
110
|
-
registration_endpoint: `${base}/oauth/register`,
|
|
111
|
-
scopes_supported: ['mcp:read', 'mcp:write'],
|
|
112
|
-
response_types_supported: ['code'],
|
|
113
|
-
grant_types_supported: ['authorization_code'],
|
|
114
|
-
code_challenge_methods_supported: ['S256'],
|
|
115
|
-
token_endpoint_auth_methods_supported: ['none'],
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// ---------------------------------------------------------------------------
|
|
120
|
-
// Dynamic client registration (RFC 7591)
|
|
121
|
-
// ---------------------------------------------------------------------------
|
|
122
|
-
|
|
123
|
-
// The MCP authorization spec is explicit: "All redirect URIs MUST be either
|
|
124
|
-
// `localhost` or use HTTPS." We therefore accept only HTTPS and loopback
|
|
125
|
-
// (http://localhost | http://127.0.0.1) and reject every other scheme —
|
|
126
|
-
// including native-app custom schemes (myapp://, intent://, etc.), which the MCP
|
|
127
|
-
// profile does not sanction and the real client (Claude) does not use.
|
|
128
|
-
function isAllowedRedirectUri(uri) {
|
|
129
|
-
const u = String(uri || '');
|
|
130
|
-
if (/^https:\/\//i.test(u)) return true;
|
|
131
|
-
if (/^http:\/\/(localhost|127\.0\.0\.1)(:\d+)?(\/|$)/i.test(u)) return true;
|
|
132
|
-
return false;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function registerClient(store, body = {}) {
|
|
136
|
-
const redirectUris = Array.isArray(body.redirect_uris) ? body.redirect_uris.filter(Boolean) : [];
|
|
137
|
-
if (redirectUris.length === 0) {
|
|
138
|
-
return { error: 'invalid_redirect_uri', error_description: 'redirect_uris is required' };
|
|
139
|
-
}
|
|
140
|
-
for (const uri of redirectUris) {
|
|
141
|
-
if (!isAllowedRedirectUri(uri)) {
|
|
142
|
-
return { error: 'invalid_redirect_uri', error_description: `unsupported redirect_uri: ${uri}` };
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
const clientId = `tg_${randomToken(16)}`;
|
|
146
|
-
const record = {
|
|
147
|
-
client_id: clientId,
|
|
148
|
-
redirect_uris: redirectUris,
|
|
149
|
-
token_endpoint_auth_method: 'none',
|
|
150
|
-
grant_types: ['authorization_code'],
|
|
151
|
-
response_types: ['code'],
|
|
152
|
-
client_name: typeof body.client_name === 'string' ? body.client_name.slice(0, 200) : 'mcp-client',
|
|
153
|
-
created_at: now(),
|
|
154
|
-
};
|
|
155
|
-
capInsert(store.clients, clientId, record, MAX_CLIENTS);
|
|
156
|
-
return record;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function getClient(store, clientId) {
|
|
160
|
-
return store.clients.get(clientId) || null;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// ---------------------------------------------------------------------------
|
|
164
|
-
// Authorization code (PKCE S256)
|
|
165
|
-
// ---------------------------------------------------------------------------
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Issue an auth code. `boundKey` is the ThumbGate API key the resulting access
|
|
169
|
-
* token will act as (resolved by the authorize step once the user consents).
|
|
170
|
-
*/
|
|
171
|
-
function createAuthorizationCode(store, {
|
|
172
|
-
clientId, redirectUri, codeChallenge, codeChallengeMethod, scope, boundKey, state, resource,
|
|
173
|
-
} = {}) {
|
|
174
|
-
const client = getClient(store, clientId);
|
|
175
|
-
if (!client) return { error: 'invalid_client' };
|
|
176
|
-
if (!client.redirect_uris.includes(redirectUri)) return { error: 'invalid_request', error_description: 'redirect_uri mismatch' };
|
|
177
|
-
if (codeChallengeMethod !== 'S256') return { error: 'invalid_request', error_description: 'code_challenge_method must be S256' };
|
|
178
|
-
if (!codeChallenge || String(codeChallenge).length < 16) return { error: 'invalid_request', error_description: 'code_challenge required' };
|
|
179
|
-
|
|
180
|
-
const code = randomToken(24);
|
|
181
|
-
capInsert(store.codes, code, {
|
|
182
|
-
clientId,
|
|
183
|
-
redirectUri,
|
|
184
|
-
codeChallenge,
|
|
185
|
-
scope: scope || DEFAULT_SCOPE,
|
|
186
|
-
boundKey: boundKey || '',
|
|
187
|
-
resource: resource || '', // RFC 8707 resource indicator (the MCP server URL)
|
|
188
|
-
expiresAt: now() + AUTH_CODE_TTL_MS,
|
|
189
|
-
used: false,
|
|
190
|
-
}, MAX_CODES);
|
|
191
|
-
return { code, state };
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function verifyPkce(codeChallenge, codeVerifier) {
|
|
195
|
-
if (!codeChallenge || !codeVerifier) return false;
|
|
196
|
-
// RFC 7636: verifier 43–128 chars.
|
|
197
|
-
if (String(codeVerifier).length < 43 || String(codeVerifier).length > 128) return false;
|
|
198
|
-
try {
|
|
199
|
-
return crypto.timingSafeEqual(
|
|
200
|
-
Buffer.from(base64UrlSha256(codeVerifier)),
|
|
201
|
-
Buffer.from(String(codeChallenge)),
|
|
202
|
-
);
|
|
203
|
-
} catch {
|
|
204
|
-
return false;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// ---------------------------------------------------------------------------
|
|
209
|
-
// Token exchange + validation
|
|
210
|
-
// ---------------------------------------------------------------------------
|
|
211
|
-
|
|
212
|
-
function exchangeCode(store, { code, codeVerifier, clientId, redirectUri, resource } = {}) {
|
|
213
|
-
const entry = store.codes.get(code);
|
|
214
|
-
if (!entry) return { error: 'invalid_grant', error_description: 'unknown code' };
|
|
215
|
-
// Single-use + expiry: consume regardless of outcome.
|
|
216
|
-
store.codes.delete(code);
|
|
217
|
-
if (entry.used) return { error: 'invalid_grant', error_description: 'code already used' };
|
|
218
|
-
if (now() > entry.expiresAt) return { error: 'invalid_grant', error_description: 'code expired' };
|
|
219
|
-
if (entry.clientId !== clientId) return { error: 'invalid_grant', error_description: 'client mismatch' };
|
|
220
|
-
if (entry.redirectUri !== redirectUri) return { error: 'invalid_grant', error_description: 'redirect_uri mismatch' };
|
|
221
|
-
if (!verifyPkce(entry.codeChallenge, codeVerifier)) return { error: 'invalid_grant', error_description: 'PKCE verification failed' };
|
|
222
|
-
// RFC 8707: the resource at token time must match the one bound at authorize time.
|
|
223
|
-
if (entry.resource && resource && entry.resource !== resource) {
|
|
224
|
-
return { error: 'invalid_target', error_description: 'resource indicator mismatch' };
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
const accessToken = `tgat_${randomToken(32)}`;
|
|
228
|
-
capInsert(store.tokens, accessToken, {
|
|
229
|
-
boundKey: entry.boundKey,
|
|
230
|
-
scope: entry.scope,
|
|
231
|
-
clientId,
|
|
232
|
-
aud: entry.resource || resource || '',
|
|
233
|
-
expiresAt: now() + ACCESS_TOKEN_TTL_MS,
|
|
234
|
-
}, MAX_TOKENS);
|
|
235
|
-
return {
|
|
236
|
-
access_token: accessToken,
|
|
237
|
-
token_type: 'Bearer',
|
|
238
|
-
expires_in: Math.floor(ACCESS_TOKEN_TTL_MS / 1000),
|
|
239
|
-
scope: entry.scope,
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/** Resolve an access token to its session, or null if invalid/expired. */
|
|
244
|
-
function resolveAccessToken(store, token) {
|
|
245
|
-
if (!token) return null;
|
|
246
|
-
const entry = store.tokens.get(token);
|
|
247
|
-
if (!entry) return null;
|
|
248
|
-
if (now() > entry.expiresAt) {
|
|
249
|
-
store.tokens.delete(token);
|
|
250
|
-
return null;
|
|
251
|
-
}
|
|
252
|
-
return { boundKey: entry.boundKey, scope: entry.scope, clientId: entry.clientId, aud: entry.aud };
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* RFC 8707 audience validation: a token is valid for `expectedResource` only if it
|
|
257
|
-
* was issued for it (or carries no audience, for back-compat). MCP servers MUST
|
|
258
|
-
* reject tokens minted for a different resource.
|
|
259
|
-
*/
|
|
260
|
-
function tokenAudienceValid(session, expectedResource) {
|
|
261
|
-
if (!session) return false;
|
|
262
|
-
if (!session.aud) return true; // no audience recorded — accept (back-compat)
|
|
263
|
-
return session.aud === expectedResource;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/** Best-effort GC of expired codes/tokens (call opportunistically). */
|
|
267
|
-
function pruneExpired(store) {
|
|
268
|
-
const t = now();
|
|
269
|
-
for (const [k, v] of store.codes) if (t > v.expiresAt) store.codes.delete(k);
|
|
270
|
-
for (const [k, v] of store.tokens) if (t > v.expiresAt) store.tokens.delete(k);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
module.exports = {
|
|
274
|
-
createStore,
|
|
275
|
-
buildProtectedResourceMetadata,
|
|
276
|
-
buildAuthServerMetadata,
|
|
277
|
-
registerClient,
|
|
278
|
-
getClient,
|
|
279
|
-
createAuthorizationCode,
|
|
280
|
-
verifyPkce,
|
|
281
|
-
exchangeCode,
|
|
282
|
-
resolveAccessToken,
|
|
283
|
-
tokenAudienceValid,
|
|
284
|
-
pruneExpired,
|
|
285
|
-
isAllowedRedirectUri,
|
|
286
|
-
base64UrlSha256,
|
|
287
|
-
AUTH_CODE_TTL_MS,
|
|
288
|
-
ACCESS_TOKEN_TTL_MS,
|
|
289
|
-
DEFAULT_SCOPE,
|
|
290
|
-
MAX_CLIENTS,
|
|
291
|
-
MAX_CODES,
|
|
292
|
-
MAX_TOKENS,
|
|
293
|
-
};
|