thumbgate 1.27.18 → 1.27.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/dashboard.md +15 -0
- package/.claude/commands/thumbgate-blocked.md +27 -0
- package/.claude/commands/thumbgate-dashboard.md +15 -0
- package/.claude/commands/thumbgate-doctor.md +30 -0
- package/.claude/commands/thumbgate-guard.md +36 -0
- package/.claude/commands/thumbgate-protect.md +30 -0
- package/.claude/commands/thumbgate-rules.md +30 -0
- package/.claude-plugin/plugin.json +4 -3
- package/.well-known/agentic-verify.txt +1 -0
- package/.well-known/llms.txt +33 -12
- package/.well-known/mcp/server-card.json +8 -8
- package/README.md +246 -30
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/codex/config.toml +2 -2
- package/adapters/gcp/dfcx-webhook-gate.js +295 -0
- package/adapters/gemini/function-declarations.json +1 -0
- package/adapters/letta/README.md +41 -0
- package/adapters/letta/thumbgate-letta-adapter.js +133 -0
- package/adapters/mcp/server-stdio.js +263 -11
- package/adapters/opencode/opencode.json +1 -1
- package/adapters/policy-engine/ethicore-guardian-client.js +68 -0
- package/adapters/policy-engine/thumbgate-policy-engine-adapter.js +260 -0
- package/bench/observability-eval-suite.json +26 -0
- package/bench/thumbgate-bench.json +2 -2
- package/bin/cli.js +1456 -122
- package/bin/dashboard-cli.js +7 -0
- package/bin/postinstall.js +11 -27
- package/commands/dashboard.md +15 -0
- package/commands/thumbgate-dashboard.md +15 -0
- package/config/gate-classifier-routing.json +98 -0
- package/config/gate-templates.json +216 -0
- package/config/gates/claim-verification.json +12 -0
- package/config/gates/default.json +31 -2
- package/config/github-about.json +2 -2
- package/config/mcp-allowlists.json +23 -13
- package/config/merge-quality-checks.json +0 -1
- package/config/model-candidates.json +121 -6
- package/config/post-deploy-marketing-pages.json +80 -0
- package/config/tessl-tiles.json +1 -3
- package/openapi/openapi.yaml +12 -0
- package/package.json +225 -100
- package/public/about.html +162 -0
- package/public/agent-manager.html +179 -0
- package/public/agents-cost-savings.html +153 -0
- package/public/ai-malpractice-prevention.html +818 -0
- package/public/assets/brand/github-social-preview.png +0 -0
- package/public/assets/brand/thumbgate-icon-512.png +0 -0
- package/public/assets/brand/thumbgate-icon-pro-512.png +0 -0
- package/public/assets/brand/thumbgate-icon-team-512.png +0 -0
- package/public/assets/brand/thumbgate-logo-1200x360.png +0 -0
- package/public/assets/brand/thumbgate-logo-transparent.svg +28 -0
- package/public/assets/brand/thumbgate-mark-inline-v3.svg +18 -0
- package/public/assets/brand/thumbgate-mark-pro.svg +23 -0
- package/public/assets/brand/thumbgate-mark-team.svg +26 -0
- package/public/assets/brand/thumbgate-mark.svg +21 -0
- package/public/assets/brand/thumbgate-wordmark.svg +20 -0
- package/public/assets/claude-thumbgate-statusbar.svg +8 -0
- package/public/assets/codex-thumbgate-statusbar-test.svg +9 -0
- package/public/assets/legal-intake-control-flow.svg +66 -0
- package/public/blog.html +4 -4
- package/public/brand/thumbgate-mark.svg +19 -0
- package/public/brand/thumbgate-og.svg +16 -0
- package/public/chatgpt-app.html +330 -0
- package/public/codex-enterprise.html +123 -0
- package/public/codex-plugin.html +72 -20
- package/public/compare.html +31 -8
- package/public/dashboard.html +930 -166
- package/public/diagnostic.html +345 -0
- package/public/federal.html +2 -2
- package/public/guide.html +33 -13
- package/public/index.html +469 -111
- package/public/install.html +193 -0
- package/public/js/buyer-intent.js +672 -0
- package/public/learn.html +183 -18
- package/public/lessons.html +168 -10
- package/public/numbers.html +7 -7
- package/public/pricing.html +399 -0
- package/public/pro.html +34 -11
- package/scripts/action-receipts.js +324 -0
- package/scripts/activation-quickstart.js +187 -0
- package/scripts/agent-memory-lifecycle.js +211 -0
- package/scripts/agent-operations-planner.js +621 -0
- package/scripts/agent-readiness.js +20 -3
- package/scripts/agent-reward-model.js +53 -1
- package/scripts/ai-component-inventory.js +367 -0
- package/scripts/async-eval-observability.js +236 -0
- package/scripts/audit.js +65 -0
- package/scripts/auto-promote-gates.js +82 -10
- package/scripts/auto-wire-hooks.js +14 -0
- package/scripts/aws-blocks-guardrails.js +272 -0
- package/scripts/billing.js +93 -1
- package/scripts/bot-detection.js +61 -3
- package/scripts/build-metadata.js +50 -10
- package/scripts/classifier-routing.js +130 -0
- package/scripts/cli-feedback.js +4 -2
- package/scripts/cli-schema.js +97 -0
- package/scripts/cli-telemetry.js +6 -1
- package/scripts/commercial-offer.js +82 -2
- package/scripts/context-manager.js +74 -6
- package/scripts/dashboard-chat.js +332 -0
- package/scripts/dashboard.js +68 -2
- package/scripts/export-databricks-bundle.js +5 -1
- package/scripts/export-dpo-pairs.js +7 -2
- package/scripts/feedback-aggregate.js +281 -0
- package/scripts/feedback-loop.js +123 -1
- package/scripts/feedback-quality.js +87 -0
- package/scripts/feedback-sanitizer.js +105 -0
- package/scripts/filesystem-search.js +35 -10
- package/scripts/gate-stats.js +89 -0
- package/scripts/gates-engine.js +1176 -85
- package/scripts/gemini-embedding-policy.js +2 -1
- package/scripts/hook-runtime.js +20 -14
- package/scripts/hook-stop-anti-claim.js +301 -0
- package/scripts/hook-thumbgate-cache-updater.js +18 -2
- package/scripts/hybrid-feedback-context.js +142 -7
- package/scripts/install-shim.js +87 -0
- package/scripts/lesson-inference.js +8 -3
- package/scripts/lesson-search.js +17 -1
- package/scripts/license.js +10 -10
- package/scripts/llm-client.js +169 -4
- package/scripts/local-model-profile.js +15 -8
- package/scripts/mcp-config.js +7 -1
- package/scripts/mcp-oauth.js +293 -0
- package/scripts/memory-scope-readiness.js +159 -0
- package/scripts/meta-agent-loop.js +36 -0
- package/scripts/noop-detect.js +285 -0
- package/scripts/operational-integrity.js +39 -5
- package/scripts/oss-pr-opportunity-scout.js +35 -5
- package/scripts/parallel-workflow-orchestrator.js +293 -0
- package/scripts/plan-gate.js +243 -0
- package/scripts/plausible-domain-config.js +99 -0
- package/scripts/plausible-server-events.js +9 -6
- package/scripts/pro-local-dashboard.js +4 -4
- package/scripts/proxy-pointer-rag-guardrails.js +42 -1
- package/scripts/published-cli.js +0 -8
- package/scripts/qa-scenario-planner.js +136 -0
- package/scripts/rate-limiter.js +64 -13
- package/scripts/repeat-metric.js +137 -0
- package/scripts/secret-fixture-tokens.js +61 -0
- package/scripts/secret-redaction.js +166 -0
- package/scripts/secret-scanner.js +44 -5
- package/scripts/security-scanner.js +260 -10
- package/scripts/self-distill-agent.js +3 -1
- package/scripts/self-harness-optimizer.js +141 -0
- package/scripts/self-healing-check.js +193 -0
- package/scripts/self-protection.js +90 -0
- package/scripts/seo-gsd.js +916 -7
- package/scripts/silent-failure-cluster.js +531 -0
- package/scripts/statusline-cache-path.js +17 -2
- package/scripts/statusline-cache-read.js +57 -0
- package/scripts/statusline-local-stats.js +9 -1
- package/scripts/statusline-meta.js +28 -2
- package/scripts/statusline.sh +20 -4
- package/scripts/sync-telemetry-from-prod.js +374 -0
- package/scripts/telemetry-analytics.js +357 -0
- package/scripts/thompson-sampling.js +31 -10
- package/scripts/thumbgate-bench.js +16 -1
- package/scripts/thumbgate-search.js +85 -19
- package/scripts/tool-contract-validator.js +76 -0
- package/scripts/tool-registry.js +169 -1
- package/scripts/trajectory-scorer.js +63 -0
- package/scripts/vector-store.js +45 -0
- package/scripts/verify-marketing-pages-deployed.js +212 -0
- package/scripts/visitor-journey.js +172 -0
- package/scripts/workflow-sentinel.js +286 -53
- package/scripts/workspace-evolver.js +62 -2
- package/src/api/server.js +2683 -319
- package/.claude-plugin/marketplace.json +0 -85
- package/adapters/chatgpt/openapi.yaml +0 -1695
- package/scripts/bot-detector.js +0 -50
|
@@ -0,0 +1,293 @@
|
|
|
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
|
+
};
|
|
@@ -2,6 +2,38 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
const REQUIRED_SCOPE_FIELDS = ['entityId', 'projectId', 'processId', 'sessionId'];
|
|
5
|
+
const MEMORY_OS_LAYERS = Object.freeze([
|
|
6
|
+
{
|
|
7
|
+
id: 'file_layer',
|
|
8
|
+
name: 'File Layer',
|
|
9
|
+
purpose: 'Raw feedback, tool receipts, sessions, and memory rows are durably stored before interpretation.',
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
id: 'vector_db_layer',
|
|
13
|
+
name: 'Vector DB Layer',
|
|
14
|
+
purpose: 'Semantic retrieval can find related lessons without stuffing every raw memory into context.',
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
id: 'structured_facts_layer',
|
|
18
|
+
name: 'Structured Facts Layer',
|
|
19
|
+
purpose: 'Confirmed account, project, policy, and budget facts are typed separately from fuzzy memories.',
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
id: 'auto_curation_layer',
|
|
23
|
+
name: 'Auto Curation Layer',
|
|
24
|
+
purpose: 'Duplicate, stale, contradictory, and unscoped memories are consolidated before retrieval quality decays.',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: 'context_layer',
|
|
28
|
+
name: 'Context Layer',
|
|
29
|
+
purpose: 'Only relevant scoped memories enter a given tool call, PR, deployment, or support session.',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: 'interface_layer',
|
|
33
|
+
name: 'Interface Layer',
|
|
34
|
+
purpose: 'The memory contract is exposed through CLI, MCP, hooks, dashboards, and agent adapters without model lock-in.',
|
|
35
|
+
},
|
|
36
|
+
]);
|
|
5
37
|
|
|
6
38
|
const FIELD_ALIASES = {
|
|
7
39
|
entityId: [
|
|
@@ -228,6 +260,128 @@ function buildRecommendations({ unscopedRecords, crossScopeDuplicates }) {
|
|
|
228
260
|
return recommendations;
|
|
229
261
|
}
|
|
230
262
|
|
|
263
|
+
function hasEmbeddingEvidence(record = {}) {
|
|
264
|
+
return Boolean(
|
|
265
|
+
record.embedding
|
|
266
|
+
|| record.vector
|
|
267
|
+
|| record.embeddingId
|
|
268
|
+
|| record.metadata?.embedding
|
|
269
|
+
|| record.metadata?.embeddingId
|
|
270
|
+
|| record.metadata?.vectorId
|
|
271
|
+
|| record.semanticKey
|
|
272
|
+
|| record.metadata?.semanticKey
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function hasStructuredFactEvidence(record = {}) {
|
|
277
|
+
const type = String(record.type || record.kind || record.memoryType || record.metadata?.type || '').toLowerCase();
|
|
278
|
+
return type === 'fact'
|
|
279
|
+
|| type === 'structured_fact'
|
|
280
|
+
|| Boolean(record.factKey || record.fact || record.metadata?.factKey || record.metadata?.fact);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function hasContextEvidence(record = {}) {
|
|
284
|
+
return Boolean(
|
|
285
|
+
record.contextPackId
|
|
286
|
+
|| record.contextPack
|
|
287
|
+
|| record.metadata?.contextPackId
|
|
288
|
+
|| record.metadata?.contextPack
|
|
289
|
+
|| record.retrievalQuery
|
|
290
|
+
|| record.metadata?.retrievalQuery
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function boolCapability(capabilities = {}, ...keys) {
|
|
295
|
+
return keys.some((key) => capabilities[key] === true);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function buildMemoryOsLayerReport(records = [], capabilities = {}) {
|
|
299
|
+
const scopeReport = buildMemoryScopeReadinessReport(records);
|
|
300
|
+
const semanticRecords = records.filter(hasEmbeddingEvidence);
|
|
301
|
+
const structuredFactRecords = records.filter(hasStructuredFactEvidence);
|
|
302
|
+
const contextRecords = records.filter(hasContextEvidence);
|
|
303
|
+
const curationReady = scopeReport.unscopedRecords === 0 && scopeReport.crossScopeDuplicates.length === 0;
|
|
304
|
+
|
|
305
|
+
const checks = [
|
|
306
|
+
{
|
|
307
|
+
id: 'file_layer',
|
|
308
|
+
ok: records.length > 0 || boolCapability(capabilities, 'rawStorage', 'fileLayer'),
|
|
309
|
+
evidence: {
|
|
310
|
+
records: records.length,
|
|
311
|
+
durableStore: Boolean(records.length > 0 || capabilities.rawStorage || capabilities.fileLayer),
|
|
312
|
+
},
|
|
313
|
+
recommendation: 'Capture raw feedback, action receipts, and tool outcomes before promoting memories.',
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
id: 'vector_db_layer',
|
|
317
|
+
ok: semanticRecords.length > 0 || boolCapability(capabilities, 'semanticSearch', 'vectorDbLayer'),
|
|
318
|
+
evidence: {
|
|
319
|
+
semanticRecords: semanticRecords.length,
|
|
320
|
+
semanticSearch: Boolean(capabilities.semanticSearch || capabilities.vectorDbLayer),
|
|
321
|
+
},
|
|
322
|
+
recommendation: 'Index lessons with semantic keys or embeddings so related failures are retrieved before action.',
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
id: 'structured_facts_layer',
|
|
326
|
+
ok: structuredFactRecords.length > 0 || boolCapability(capabilities, 'structuredFacts', 'structuredFactsLayer'),
|
|
327
|
+
evidence: {
|
|
328
|
+
structuredFactRecords: structuredFactRecords.length,
|
|
329
|
+
structuredFacts: Boolean(capabilities.structuredFacts || capabilities.structuredFactsLayer),
|
|
330
|
+
},
|
|
331
|
+
recommendation: 'Store confirmed customer, project, policy, and budget facts as typed records, not just prose.',
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
id: 'auto_curation_layer',
|
|
335
|
+
ok: curationReady && boolCapability(capabilities, 'autoCuration', 'dedupe', 'autoCurationLayer'),
|
|
336
|
+
evidence: {
|
|
337
|
+
unscopedRecords: scopeReport.unscopedRecords,
|
|
338
|
+
crossScopeDuplicates: scopeReport.crossScopeDuplicates.length,
|
|
339
|
+
autoCuration: Boolean(capabilities.autoCuration || capabilities.dedupe || capabilities.autoCurationLayer),
|
|
340
|
+
},
|
|
341
|
+
recommendation: 'Run dedupe, contradiction, stale-memory, and scope-isolation checks before memories can become gates.',
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
id: 'context_layer',
|
|
345
|
+
ok: contextRecords.length > 0 || boolCapability(capabilities, 'contextPacks', 'contextLayer', 'scopedRetrieval'),
|
|
346
|
+
evidence: {
|
|
347
|
+
contextRecords: contextRecords.length,
|
|
348
|
+
scopedRetrieval: Boolean(capabilities.contextPacks || capabilities.contextLayer || capabilities.scopedRetrieval),
|
|
349
|
+
},
|
|
350
|
+
recommendation: 'Inject scoped context packs per task instead of loading every memory into the model window.',
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
id: 'interface_layer',
|
|
354
|
+
ok: boolCapability(capabilities, 'mcp', 'cli', 'hooks', 'dashboard', 'interfaceLayer'),
|
|
355
|
+
evidence: {
|
|
356
|
+
cli: Boolean(capabilities.cli),
|
|
357
|
+
mcp: Boolean(capabilities.mcp),
|
|
358
|
+
hooks: Boolean(capabilities.hooks),
|
|
359
|
+
dashboard: Boolean(capabilities.dashboard),
|
|
360
|
+
},
|
|
361
|
+
recommendation: 'Expose the same memory contract through CLI, MCP, hooks, dashboard, and agent adapters.',
|
|
362
|
+
},
|
|
363
|
+
].map((check) => {
|
|
364
|
+
const layer = MEMORY_OS_LAYERS.find((candidate) => candidate.id === check.id);
|
|
365
|
+
return {
|
|
366
|
+
...layer,
|
|
367
|
+
...check,
|
|
368
|
+
};
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
const missingLayers = checks.filter((check) => !check.ok).map((check) => check.id);
|
|
372
|
+
|
|
373
|
+
return {
|
|
374
|
+
ready: missingLayers.length === 0,
|
|
375
|
+
riskLevel: missingLayers.length === 0 ? 'low' : missingLayers.length <= 2 ? 'medium' : 'high',
|
|
376
|
+
layers: checks,
|
|
377
|
+
missingLayers,
|
|
378
|
+
scopeReport,
|
|
379
|
+
recommendations: checks
|
|
380
|
+
.filter((check) => !check.ok)
|
|
381
|
+
.map((check) => check.recommendation),
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
231
385
|
function selectRecordsForScope(records = [], requestedScope = {}, options = {}) {
|
|
232
386
|
const requested = normalizeScope(requestedScope);
|
|
233
387
|
const requestedKey = memoryScopeKey(requested);
|
|
@@ -265,6 +419,7 @@ function buildMemoriStyleBenchmarkRecords() {
|
|
|
265
419
|
projectId: 'thumbgate',
|
|
266
420
|
processId: 'agent-a',
|
|
267
421
|
sessionId: 'session-1',
|
|
422
|
+
metadata: { semanticKey: 'checkout-readiness', contextPackId: 'checkout-pro' },
|
|
268
423
|
content: 'Use the paid sprint checklist before changing checkout code.',
|
|
269
424
|
},
|
|
270
425
|
{
|
|
@@ -298,14 +453,18 @@ function buildMemoriStyleBenchmarkRecords() {
|
|
|
298
453
|
processId: 'agent-a',
|
|
299
454
|
sessionId: 'session-1',
|
|
300
455
|
visibility: 'shared',
|
|
456
|
+
type: 'fact',
|
|
457
|
+
factKey: 'checkout.mutation_policy',
|
|
301
458
|
content: 'Shared rule: checkout mutations require audit evidence.',
|
|
302
459
|
},
|
|
303
460
|
];
|
|
304
461
|
}
|
|
305
462
|
|
|
306
463
|
module.exports = {
|
|
464
|
+
MEMORY_OS_LAYERS,
|
|
307
465
|
REQUIRED_SCOPE_FIELDS,
|
|
308
466
|
buildMemoriStyleBenchmarkRecords,
|
|
467
|
+
buildMemoryOsLayerReport,
|
|
309
468
|
buildMemoryScopeReadinessReport,
|
|
310
469
|
isSharedMemory,
|
|
311
470
|
memoryScopeKey,
|
|
@@ -282,6 +282,8 @@ function buildPromotedGate(candidate, metrics, runId) {
|
|
|
282
282
|
occurrences: metrics.hits,
|
|
283
283
|
promotedAt: new Date().toISOString(),
|
|
284
284
|
source: 'meta-agent',
|
|
285
|
+
// origin distinguishes silent-failure-clustered candidates from feedback-derived ones
|
|
286
|
+
origin: candidate.origin || 'user-feedback',
|
|
285
287
|
runId,
|
|
286
288
|
score: parseFloat(metrics.score.toFixed(3)),
|
|
287
289
|
hitRate: parseFloat(metrics.hitRate.toFixed(3)),
|
|
@@ -371,6 +373,38 @@ async function runMetaAgentLoop({ dryRun = false, verbose = false } = {}) {
|
|
|
371
373
|
candidates = generateCandidatesHeuristic(failures, blockPatterns);
|
|
372
374
|
}
|
|
373
375
|
|
|
376
|
+
// Tag existing-pipeline candidates with their origin so downstream precision
|
|
377
|
+
// measurement (silentFailureDerivedGates vs user-feedback-derived) is possible.
|
|
378
|
+
candidates = candidates.map((c) => (c.origin ? c : { ...c, origin: 'user-feedback' }));
|
|
379
|
+
|
|
380
|
+
// Step 3b: Silent-failure clustering — DEFAULT-ON as of 2026-05-21
|
|
381
|
+
// (flipped from opt-in by PR #2289). Opt out via
|
|
382
|
+
// THUMBGATE_SILENT_FAILURE_CLUSTERING=0 (or NODE_ENV=test). Candidates flow
|
|
383
|
+
// through the SAME scoring / fp-rate eval below; we do not bypass any
|
|
384
|
+
// guardrail. The point of this clustering is to cover the case where users
|
|
385
|
+
// are lazy and never give thumbs-down — keeping it opt-in meant the users
|
|
386
|
+
// who need it most never got the benefit.
|
|
387
|
+
let silentFailureStats = null;
|
|
388
|
+
const { isSilentFailureClusteringEnabled, generateSilentFailureCandidates } = require('./silent-failure-cluster');
|
|
389
|
+
if (isSilentFailureClusteringEnabled()) {
|
|
390
|
+
try {
|
|
391
|
+
const sfResult = generateSilentFailureCandidates({ feedbackLogPath });
|
|
392
|
+
silentFailureStats = sfResult.stats;
|
|
393
|
+
if (sfResult.candidates && sfResult.candidates.length > 0) {
|
|
394
|
+
candidates = candidates.concat(sfResult.candidates);
|
|
395
|
+
}
|
|
396
|
+
if (verbose) {
|
|
397
|
+
process.stdout.write(
|
|
398
|
+
`[meta-agent] silent-failure-cluster: candidates=${sfResult.candidates.length} `
|
|
399
|
+
+ `failed=${sfResult.stats.failedCalls} clusters=${sfResult.stats.clusters} `
|
|
400
|
+
+ `skipped=${sfResult.stats.skippedReason || 'none'}\n`
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
} catch (err) {
|
|
404
|
+
if (verbose) process.stdout.write(`[meta-agent] silent-failure-cluster failed (non-fatal): ${err.message}\n`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
374
408
|
if (verbose) {
|
|
375
409
|
process.stdout.write(`[meta-agent] candidates generated: ${candidates.length} (mode=${analysisMode})\n`);
|
|
376
410
|
}
|
|
@@ -507,6 +541,8 @@ async function runMetaAgentLoop({ dryRun = false, verbose = false } = {}) {
|
|
|
507
541
|
skipped: evolutionResult.skipped || false,
|
|
508
542
|
}
|
|
509
543
|
: null,
|
|
544
|
+
silentFailureCluster: silentFailureStats,
|
|
545
|
+
silentFailureDerivedGates: promotedGates.filter((g) => g.origin === 'silent-failure-cluster').length,
|
|
510
546
|
};
|
|
511
547
|
|
|
512
548
|
if (!dryRun) {
|