thumbgate 1.30.0 → 1.34.0
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/README.md +54 -16
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/forge/forge.yaml +3 -3
- package/adapters/mcp/server-stdio.js +105 -10
- package/adapters/opencode/opencode.json +1 -1
- package/bench/observability-eval-suite.json +2 -2
- package/bin/cli.js +168 -31
- package/config/evals/generation-quality-golden.json +95 -0
- package/config/evals/rag-answer-quality-golden.json +91 -0
- package/config/evals/retrieval-hybrid-ablation.json +66 -0
- package/config/evals/retrieval-ranking-golden.json +522 -0
- package/config/gates/claim-verifiers.example.json +42 -0
- package/config/gates/claim-verifiers.json +25 -0
- package/config/gates/default.json +217 -50
- package/config/mcp-allowlists.json +233 -206
- package/config/model-tiers.json +7 -2
- package/glama.json +6 -0
- package/hooks/hooks.json +1 -1
- package/package.json +69 -12
- package/public/assets/diagrams/before-after.svg +17 -16
- package/public/assets/diagrams/hero-thumbs.svg +68 -0
- package/public/assets/diagrams/loop.svg +19 -13
- package/public/assets/diagrams/self-improving-thumbs-loop.svg +105 -0
- package/public/compare.html +1 -0
- package/public/dashboard.html +126 -28
- package/public/evaluations.html +1 -1
- package/public/index.html +142 -13
- package/public/numbers.html +3 -2
- package/public/pricing.html +143 -30
- package/scripts/a-plus-evidence-scorecard.js +303 -0
- package/scripts/agent-readiness.js +110 -0
- package/scripts/async-eval-observability.js +36 -11
- package/scripts/audit-trail.js +37 -1
- package/scripts/auto-promote-gates.js +149 -34
- package/scripts/auto-wire-hooks.js +20 -8
- package/scripts/cli-schema.js +14 -0
- package/scripts/colbert-style-maxsim.js +236 -0
- package/scripts/cross-encoder-reranker.js +356 -126
- package/scripts/dashboard-chat.js +350 -17
- package/scripts/document-intake.js +283 -7
- package/scripts/eval-quality-suite.js +204 -0
- package/scripts/feedback-loop.js +115 -7
- package/scripts/feedback-paths.js +32 -13
- package/scripts/feedback-quality.js +53 -0
- package/scripts/feedback-schema.js +3 -0
- package/scripts/file-ledger-lock.js +130 -0
- package/scripts/filesystem-search.js +17 -7
- package/scripts/financial-control-plane.js +1514 -0
- package/scripts/gates-engine.js +202 -7
- package/scripts/gemini-embedding-policy.js +1 -0
- package/scripts/harness-tool-names.js +70 -0
- package/scripts/hook-runtime.js +15 -3
- package/scripts/hook-stop-anti-claim.js +63 -3
- package/scripts/human-escalation.js +353 -41
- package/scripts/lesson-db.js +16 -5
- package/scripts/lesson-embedding-index.js +67 -20
- package/scripts/lesson-embedding-maintenance.js +177 -0
- package/scripts/lesson-reranker.js +55 -9
- package/scripts/lesson-retrieval.js +305 -29
- package/scripts/lesson-search.js +22 -8
- package/scripts/llm-client.js +304 -15
- package/scripts/model-tier-router.js +593 -0
- package/scripts/pragmatic-hybrid-search.js +379 -0
- package/scripts/provider-action-normalizer.js +11 -4
- package/scripts/rag-document-pipeline.js +461 -0
- package/scripts/rag-structured-output.js +441 -0
- package/scripts/ragas-style-metrics.js +351 -0
- package/scripts/request-envelope.js +178 -0
- package/scripts/rerank-pipeline.js +370 -0
- package/scripts/rerank-quality-eval.js +155 -0
- package/scripts/retrieval-hybrid-ablation.js +120 -0
- package/scripts/retrieval-quality-tier.js +118 -0
- package/scripts/secret-scanner.js +395 -4
- package/scripts/self-distill-agent.js +7 -1
- package/scripts/self-healing-check.js +25 -0
- package/scripts/skill-packs.js +183 -0
- package/scripts/slow-loop.js +72 -0
- package/scripts/statusline-links.js +1 -1
- package/scripts/statusline.sh +8 -1
- package/scripts/telemetry-analytics.js +13 -1
- package/scripts/thumbgate-search.js +98 -6
- package/scripts/tier-budget-guard.js +186 -0
- package/scripts/tool-registry.js +141 -5
- package/scripts/universal-claim-evaluator.js +767 -0
- package/scripts/vector-store.js +154 -17
- package/scripts/verify-marketing-pages-deployed.js +85 -3
- package/scripts/workflow-sentinel.js +77 -11
- package/server.json +44 -0
- package/smithery.yaml +17 -0
- package/src/api/server.js +196 -13
package/server.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
+
"name": "io.github.IgorGanapolsky/thumbgate",
|
|
4
|
+
"description": "ThumbGate pre-action gates for AI coding agents. Captures thumbs-up/down feedback as lessons, promotes repeated failures into prevention rules, and evaluates tool calls over stdio MCP before they run.",
|
|
5
|
+
"title": "ThumbGate",
|
|
6
|
+
"websiteUrl": "https://thumbgate.ai",
|
|
7
|
+
"repository": {
|
|
8
|
+
"source": "github",
|
|
9
|
+
"url": "https://github.com/IgorGanapolsky/ThumbGate"
|
|
10
|
+
},
|
|
11
|
+
"version": "1.34.0",
|
|
12
|
+
"packages": [
|
|
13
|
+
{
|
|
14
|
+
"registryType": "npm",
|
|
15
|
+
"registryBaseUrl": "https://registry.npmjs.org",
|
|
16
|
+
"identifier": "thumbgate",
|
|
17
|
+
"version": "1.34.0",
|
|
18
|
+
"runtimeHint": "npx",
|
|
19
|
+
"runtimeArguments": [
|
|
20
|
+
{
|
|
21
|
+
"type": "positional",
|
|
22
|
+
"value": "-y"
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"packageArguments": [
|
|
26
|
+
{
|
|
27
|
+
"type": "positional",
|
|
28
|
+
"value": "serve"
|
|
29
|
+
}
|
|
30
|
+
],
|
|
31
|
+
"transport": {
|
|
32
|
+
"type": "stdio"
|
|
33
|
+
},
|
|
34
|
+
"environmentVariables": [
|
|
35
|
+
{
|
|
36
|
+
"name": "THUMBGATE_MCP_PROFILE",
|
|
37
|
+
"description": "Optional MCP tool profile override (essential | default | readonly | locked). Omit to keep the factory least-privilege essential profile; only set when expanded tools are intentional.",
|
|
38
|
+
"isRequired": false,
|
|
39
|
+
"format": "string"
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
}
|
package/smithery.yaml
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
startCommand:
|
|
2
|
+
type: "stdio"
|
|
3
|
+
configSchema:
|
|
4
|
+
type: "object"
|
|
5
|
+
properties:
|
|
6
|
+
mcpProfile:
|
|
7
|
+
type: "string"
|
|
8
|
+
description: "MCP profile to use (default, readonly, locked)"
|
|
9
|
+
default: "default"
|
|
10
|
+
required: []
|
|
11
|
+
commandFunction:
|
|
12
|
+
command: "npx"
|
|
13
|
+
args:
|
|
14
|
+
- "-y"
|
|
15
|
+
- "thumbgate"
|
|
16
|
+
- "serve"
|
|
17
|
+
description: "ThumbGate — pre-action gates for AI coding agents. Turns thumbs-down feedback into prevention rules and evaluates tool calls over stdio MCP (npx -y thumbgate serve) before they run. Not the HTTP API (npm start). Works with Claude Code, Cursor, Codex, Gemini CLI, Amp, OpenCode, and any MCP-compatible agent."
|
package/src/api/server.js
CHANGED
|
@@ -210,7 +210,7 @@ const {
|
|
|
210
210
|
getSettingsStatus,
|
|
211
211
|
} = require('../../scripts/settings-hierarchy');
|
|
212
212
|
const {
|
|
213
|
-
|
|
213
|
+
searchThumbgateAsync,
|
|
214
214
|
} = require('../../scripts/thumbgate-search');
|
|
215
215
|
const {
|
|
216
216
|
appendTelemetryPing,
|
|
@@ -480,6 +480,25 @@ const TRACKED_LINK_TARGETS = Object.freeze({
|
|
|
480
480
|
},
|
|
481
481
|
allowCustomerEmail: true,
|
|
482
482
|
},
|
|
483
|
+
// Human skip from Pro interstitial: same-origin hop so crawlers never see buy.stripe.com
|
|
484
|
+
// in the interstitial HTML (bot-guard contract). Bots are deflected to /checkout/pro.
|
|
485
|
+
'pro-direct': {
|
|
486
|
+
fallbackHref: PRO_CHECKOUT_URL,
|
|
487
|
+
external: true,
|
|
488
|
+
botDeflectPath: '/checkout/pro',
|
|
489
|
+
ctaId: 'pro_checkout_direct_stripe',
|
|
490
|
+
ctaPlacement: 'checkout_interstitial',
|
|
491
|
+
eventType: 'pro_checkout_direct_stripe',
|
|
492
|
+
defaults: {
|
|
493
|
+
utm_source: 'website',
|
|
494
|
+
utm_medium: 'checkout_interstitial',
|
|
495
|
+
utm_campaign: 'pro_upgrade',
|
|
496
|
+
plan_id: 'pro',
|
|
497
|
+
billing_cycle: 'monthly',
|
|
498
|
+
},
|
|
499
|
+
allowCustomerEmail: true,
|
|
500
|
+
prefillStripeEmail: true,
|
|
501
|
+
},
|
|
483
502
|
// 2026-06-02: Teams/Aiventyx deprecated. Redirect legacy links to Pro.
|
|
484
503
|
teams: {
|
|
485
504
|
path: '/go/pro?utm_source=legacy_teams&utm_medium=redirect',
|
|
@@ -837,6 +856,67 @@ function getRequestFeedbackPaths(req, parsed) {
|
|
|
837
856
|
});
|
|
838
857
|
}
|
|
839
858
|
|
|
859
|
+
const DATA_IDENTITY_CACHE = new Map();
|
|
860
|
+
const DATA_IDENTITY_CACHE_LIMIT = 2048;
|
|
861
|
+
|
|
862
|
+
function hashDataIdentity(prefix, value) {
|
|
863
|
+
const normalized = String(value || '');
|
|
864
|
+
const cacheKey = `${prefix}\0${normalized}`;
|
|
865
|
+
const cached = DATA_IDENTITY_CACHE.get(cacheKey);
|
|
866
|
+
if (cached) return cached;
|
|
867
|
+
|
|
868
|
+
// Billing identifiers can be email-like and API keys must never be made
|
|
869
|
+
// guessable through a fast digest. A memory-hard KDF gives us a stable,
|
|
870
|
+
// path-safe pseudonym without persisting the source identifier.
|
|
871
|
+
const derived = `${prefix}_${crypto.scryptSync(
|
|
872
|
+
normalized,
|
|
873
|
+
`thumbgate-${prefix}-data-identity-v1`,
|
|
874
|
+
16,
|
|
875
|
+
{ N: 16384, r: 8, p: 1 }
|
|
876
|
+
).toString('hex').slice(0, 24)}`;
|
|
877
|
+
|
|
878
|
+
if (DATA_IDENTITY_CACHE.size >= DATA_IDENTITY_CACHE_LIMIT) {
|
|
879
|
+
DATA_IDENTITY_CACHE.clear();
|
|
880
|
+
}
|
|
881
|
+
DATA_IDENTITY_CACHE.set(cacheKey, derived);
|
|
882
|
+
return derived;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
function buildHostedTenantIdentity(validation = {}) {
|
|
886
|
+
if (!validation || validation.valid !== true || !validation.customerId) return null;
|
|
887
|
+
const tenantSeed = validation.customerId;
|
|
888
|
+
const principalSeed = validation.installId || tenantSeed;
|
|
889
|
+
const tenantId = hashDataIdentity('tenant', tenantSeed);
|
|
890
|
+
return {
|
|
891
|
+
partition: tenantId,
|
|
892
|
+
accessContext: {
|
|
893
|
+
tenantId,
|
|
894
|
+
principalId: hashDataIdentity('principal', principalSeed),
|
|
895
|
+
isAdmin: false,
|
|
896
|
+
},
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
function resolveRequestDataIdentity(req, expectedApiKey) {
|
|
901
|
+
const token = extractApiKey(req);
|
|
902
|
+
if (!token || (expectedApiKey && safeKeyEqual(token, expectedApiKey))) {
|
|
903
|
+
return { partition: null, accessContext: null };
|
|
904
|
+
}
|
|
905
|
+
const validation = validateApiKey(token);
|
|
906
|
+
const identity = buildHostedTenantIdentity(validation);
|
|
907
|
+
if (validation.valid === true && !identity) {
|
|
908
|
+
return { partition: null, accessContext: null, invalidHostedIdentity: true };
|
|
909
|
+
}
|
|
910
|
+
return identity || { partition: null, accessContext: null };
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
function partitionFeedbackPaths(paths, dataIdentity) {
|
|
914
|
+
if (!dataIdentity?.partition) return paths;
|
|
915
|
+
return getFeedbackPaths({
|
|
916
|
+
feedbackDir: path.join(paths.FEEDBACK_DIR, 'tenants', dataIdentity.partition),
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
|
|
840
920
|
function getSafeDataDir(req, parsed) {
|
|
841
921
|
const { FEEDBACK_LOG_PATH } = getRequestFeedbackPaths(req, parsed);
|
|
842
922
|
return path.resolve(path.dirname(FEEDBACK_LOG_PATH));
|
|
@@ -2501,8 +2581,9 @@ ${hiddenInputs}
|
|
|
2501
2581
|
<p class="email-note">Required so the checkout has attributable buyer intent and your receipt can be recovered.</p>
|
|
2502
2582
|
<button type="submit" class="primary">Pay $19/mo with Stripe →</button>
|
|
2503
2583
|
</form>
|
|
2584
|
+
<a class="secondary" data-i="pro_checkout_direct_stripe" rel="nofollow" href="/go/pro-direct?utm_source=website&utm_medium=checkout_interstitial&utm_campaign=pro_upgrade&utm_content=skip_form&cta_id=pro_checkout_direct_stripe&cta_placement=checkout_interstitial">Skip form — open Stripe checkout directly →</a>
|
|
2504
2585
|
<a class="secondary" data-i="workflow_sprint_intake" href="/#workflow-sprint-intake">Not sure yet? Send the workflow first</a>
|
|
2505
|
-
<p class="choice-note">Cancel anytime. 7-day refund, no questions. Diagnostics and sprints have their own pages.</p>
|
|
2586
|
+
<p class="choice-note">Cancel anytime. 7-day refund, no questions. Diagnostics and sprints have their own pages. Email path keeps receipts attributable; skip uses a same-origin hop that bot-deflects crawlers.</p>
|
|
2506
2587
|
<div class="objection-box" aria-label="Checkout feedback">
|
|
2507
2588
|
<strong>Not buying today?</strong>
|
|
2508
2589
|
<p class="objection-note">Tap one reason so we know what to fix. This does not sign you up.</p>
|
|
@@ -2825,6 +2906,37 @@ function serveTrackedLinkRedirect({ req, res, parsed, hostedConfig, isHeadReques
|
|
|
2825
2906
|
|
|
2826
2907
|
const { FEEDBACK_DIR } = getFeedbackPaths();
|
|
2827
2908
|
const journeyState = resolveJourneyState(req, parsed);
|
|
2909
|
+
const botClassification = classifyRequester(req.headers);
|
|
2910
|
+
if (target.botDeflectPath && botClassification.isBot) {
|
|
2911
|
+
const deflectUrl = new URL(target.botDeflectPath, hostedConfig.appOrigin);
|
|
2912
|
+
appendTrackedLinkQueryParams(deflectUrl, parsed, target);
|
|
2913
|
+
if (!isHeadRequest) {
|
|
2914
|
+
appendBestEffortTelemetry(
|
|
2915
|
+
FEEDBACK_DIR,
|
|
2916
|
+
{
|
|
2917
|
+
eventType: 'checkout_bot_deflected',
|
|
2918
|
+
clientType: 'web',
|
|
2919
|
+
page: `/go/${target.slug}`,
|
|
2920
|
+
reason: botClassification.reason || 'bot',
|
|
2921
|
+
destinationSlug: target.slug,
|
|
2922
|
+
acquisitionId: journeyState.acquisitionId,
|
|
2923
|
+
visitorId: journeyState.visitorId,
|
|
2924
|
+
sessionId: journeyState.sessionId,
|
|
2925
|
+
},
|
|
2926
|
+
req.headers,
|
|
2927
|
+
`tracked_link_bot_deflect:${target.slug}`
|
|
2928
|
+
);
|
|
2929
|
+
}
|
|
2930
|
+
res.writeHead(302, {
|
|
2931
|
+
...(journeyState.setCookieHeaders.length ? { 'Set-Cookie': journeyState.setCookieHeaders } : {}),
|
|
2932
|
+
'Cache-Control': 'no-store',
|
|
2933
|
+
'X-Robots-Tag': 'noindex,nofollow',
|
|
2934
|
+
'X-ThumbGate-Link-Slug': target.slug,
|
|
2935
|
+
Location: deflectUrl.toString(),
|
|
2936
|
+
});
|
|
2937
|
+
res.end();
|
|
2938
|
+
return;
|
|
2939
|
+
}
|
|
2828
2940
|
const destinationUrl = buildTrackedLinkDestination(target, hostedConfig, parsed);
|
|
2829
2941
|
const attribution = buildTrackedLinkAttribution(target, parsed, req, journeyState, destinationUrl);
|
|
2830
2942
|
if (target.external) {
|
|
@@ -5224,9 +5336,10 @@ function createApiServer() {
|
|
|
5224
5336
|
sendJson(res, 403, { error: 'project selection is only available on localhost requests' });
|
|
5225
5337
|
return;
|
|
5226
5338
|
}
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5339
|
+
let requestFeedbackPaths = getRequestFeedbackPaths(req, parsed);
|
|
5340
|
+
let requestFeedbackDir = requestFeedbackPaths.FEEDBACK_DIR;
|
|
5341
|
+
let requestSafeDataDir = getSafeDataDir(req, parsed);
|
|
5342
|
+
let requestDocumentAccessContext = null;
|
|
5230
5343
|
|
|
5231
5344
|
// PostHog reverse proxy -- bypasses ad blockers.
|
|
5232
5345
|
// Only allow known PostHog API paths to prevent SSRF (CodeQL js/request-forgery).
|
|
@@ -8645,6 +8758,25 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
8645
8758
|
return;
|
|
8646
8759
|
}
|
|
8647
8760
|
|
|
8761
|
+
// Provisioned hosted keys are authenticated identities, not merely valid
|
|
8762
|
+
// booleans. Bind every private data path to a hash of the billing customer
|
|
8763
|
+
// before any endpoint reads or writes feedback, documents, traces, or jobs.
|
|
8764
|
+
// Static admin/local operation retains the existing single-tenant path.
|
|
8765
|
+
const requestDataIdentity = resolveRequestDataIdentity(req, expectedApiKey);
|
|
8766
|
+
if (requestDataIdentity.invalidHostedIdentity) {
|
|
8767
|
+
sendProblem(res, {
|
|
8768
|
+
type: PROBLEM_TYPES.UNAUTHORIZED,
|
|
8769
|
+
title: 'Unauthorized',
|
|
8770
|
+
status: 403,
|
|
8771
|
+
detail: 'The hosted API key is not bound to a customer identity.',
|
|
8772
|
+
});
|
|
8773
|
+
return;
|
|
8774
|
+
}
|
|
8775
|
+
requestFeedbackPaths = partitionFeedbackPaths(requestFeedbackPaths, requestDataIdentity);
|
|
8776
|
+
requestFeedbackDir = requestFeedbackPaths.FEEDBACK_DIR;
|
|
8777
|
+
requestSafeDataDir = path.resolve(requestFeedbackDir);
|
|
8778
|
+
requestDocumentAccessContext = requestDataIdentity.accessContext;
|
|
8779
|
+
|
|
8648
8780
|
// Usage metering — record request for billing keys (not static THUMBGATE_API_KEY)
|
|
8649
8781
|
const _token = extractBearerToken(req);
|
|
8650
8782
|
if (_token && _token !== expectedApiKey) {
|
|
@@ -8665,6 +8797,33 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
8665
8797
|
stats.tier = 'Pro';
|
|
8666
8798
|
}
|
|
8667
8799
|
|
|
8800
|
+
// Active gates for the dashboard hero cards. Connect only fetches
|
|
8801
|
+
// /v1/feedback/stats first; without these fields Active Gates stays
|
|
8802
|
+
// stuck on the HTML placeholder "—" until a later /v1/dashboard call
|
|
8803
|
+
// succeeds (and never updates if that call fails).
|
|
8804
|
+
try {
|
|
8805
|
+
const { computeGateStats } = require('../../scripts/dashboard');
|
|
8806
|
+
const gateStats = computeGateStats();
|
|
8807
|
+
stats.activeGateCount = Number(gateStats.totalGates) || 0;
|
|
8808
|
+
stats.gateStats = {
|
|
8809
|
+
totalGates: Number(gateStats.totalGates) || 0,
|
|
8810
|
+
manualCount: Number(gateStats.manualCount) || 0,
|
|
8811
|
+
autoCount: Number(gateStats.autoCount) || 0,
|
|
8812
|
+
blocked: Number(gateStats.blocked) || 0,
|
|
8813
|
+
warned: Number(gateStats.warned) || 0,
|
|
8814
|
+
};
|
|
8815
|
+
} catch (error) {
|
|
8816
|
+
debugApiFallback('gate stats unavailable for feedback/stats', error);
|
|
8817
|
+
stats.activeGateCount = 0;
|
|
8818
|
+
stats.gateStats = {
|
|
8819
|
+
totalGates: 0,
|
|
8820
|
+
manualCount: 0,
|
|
8821
|
+
autoCount: 0,
|
|
8822
|
+
blocked: 0,
|
|
8823
|
+
warned: 0,
|
|
8824
|
+
};
|
|
8825
|
+
}
|
|
8826
|
+
|
|
8668
8827
|
const projectChatSettings = readProjectChatSettings(req, parsed);
|
|
8669
8828
|
|
|
8670
8829
|
stats.geminiConfigured = Boolean(
|
|
@@ -9294,6 +9453,7 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
9294
9453
|
escalationId,
|
|
9295
9454
|
}, {
|
|
9296
9455
|
authenticatedActor,
|
|
9456
|
+
approvalSigningKey: humanReviewerConfig.key,
|
|
9297
9457
|
feedbackDir: requestFeedbackDir,
|
|
9298
9458
|
}));
|
|
9299
9459
|
} catch (error) {
|
|
@@ -9364,13 +9524,19 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
9364
9524
|
const signal = parsed.searchParams.get('signal') || null;
|
|
9365
9525
|
let results;
|
|
9366
9526
|
try {
|
|
9367
|
-
|
|
9368
|
-
results = searchThumbgate({
|
|
9527
|
+
results = await searchThumbgateAsync({
|
|
9369
9528
|
query,
|
|
9370
9529
|
limit: Number.isFinite(limit) ? limit : 10,
|
|
9371
9530
|
source,
|
|
9372
9531
|
signal,
|
|
9373
|
-
feedbackDir:
|
|
9532
|
+
feedbackDir: requestFeedbackDir,
|
|
9533
|
+
metadataFilters: {
|
|
9534
|
+
tags: (parsed.searchParams.get('tags') || '').split(',').map((tag) => tag.trim()).filter(Boolean),
|
|
9535
|
+
sourceFormat: parsed.searchParams.get('sourceFormat') || undefined,
|
|
9536
|
+
sourceType: parsed.searchParams.get('sourceType') || undefined,
|
|
9537
|
+
},
|
|
9538
|
+
queryRewrite: parsed.searchParams.get('queryRewrite') !== 'false',
|
|
9539
|
+
accessContext: requestDocumentAccessContext,
|
|
9374
9540
|
});
|
|
9375
9541
|
} catch (err) {
|
|
9376
9542
|
throw createHttpError(400, err.message || 'Invalid ThumbGate search request');
|
|
@@ -9388,13 +9554,15 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
9388
9554
|
const body = await parseJsonBody(req);
|
|
9389
9555
|
let results;
|
|
9390
9556
|
try {
|
|
9391
|
-
|
|
9392
|
-
results = searchThumbgate({
|
|
9557
|
+
results = await searchThumbgateAsync({
|
|
9393
9558
|
query: body.query || body.q || '',
|
|
9394
9559
|
limit: body.limit,
|
|
9395
9560
|
source: body.source,
|
|
9396
9561
|
signal: body.signal,
|
|
9397
|
-
feedbackDir:
|
|
9562
|
+
feedbackDir: requestFeedbackDir,
|
|
9563
|
+
metadataFilters: body.filters,
|
|
9564
|
+
queryRewrite: body.queryRewrite !== false,
|
|
9565
|
+
accessContext: requestDocumentAccessContext,
|
|
9398
9566
|
});
|
|
9399
9567
|
} catch (err) {
|
|
9400
9568
|
throw createHttpError(400, err.message || 'Invalid ThumbGate search request');
|
|
@@ -9412,6 +9580,7 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
9412
9580
|
limit: Number.isFinite(limit) ? limit : 20,
|
|
9413
9581
|
query,
|
|
9414
9582
|
tag,
|
|
9583
|
+
accessContext: requestDocumentAccessContext,
|
|
9415
9584
|
});
|
|
9416
9585
|
sendJson(res, 200, results);
|
|
9417
9586
|
return;
|
|
@@ -9425,6 +9594,7 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
9425
9594
|
}
|
|
9426
9595
|
const document = readImportedDocument(documentId, {
|
|
9427
9596
|
feedbackDir: requestFeedbackDir,
|
|
9597
|
+
accessContext: requestDocumentAccessContext,
|
|
9428
9598
|
});
|
|
9429
9599
|
if (!document) {
|
|
9430
9600
|
throw createHttpError(404, `Imported document not found: ${escapeHtml(documentId)}`);
|
|
@@ -9557,6 +9727,11 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
9557
9727
|
tags: extractTags(body.tags),
|
|
9558
9728
|
proposeGates: body.proposeGates !== false,
|
|
9559
9729
|
feedbackDir: requestFeedbackDir,
|
|
9730
|
+
accessContext: requestDocumentAccessContext,
|
|
9731
|
+
visibility: body.visibility === 'private' ? 'private' : 'tenant',
|
|
9732
|
+
allowedPrincipals: Array.isArray(body.allowedPrincipals)
|
|
9733
|
+
? body.allowedPrincipals.map((value) => String(value || '').trim()).filter(Boolean)
|
|
9734
|
+
: [],
|
|
9560
9735
|
});
|
|
9561
9736
|
sendJson(res, 201, {
|
|
9562
9737
|
ok: true,
|
|
@@ -9607,14 +9782,19 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
9607
9782
|
fs.writeFileSync(paths.outputPath, result.jsonl);
|
|
9608
9783
|
}
|
|
9609
9784
|
|
|
9610
|
-
|
|
9785
|
+
const response = {
|
|
9611
9786
|
pairs: result.pairs.length,
|
|
9787
|
+
pairCount: result.pairs.length,
|
|
9612
9788
|
errors: result.errors.length,
|
|
9613
9789
|
learnings: result.learnings.length,
|
|
9614
9790
|
unpairedErrors: result.unpairedErrors.length,
|
|
9615
9791
|
unpairedLearnings: result.unpairedLearnings.length,
|
|
9616
9792
|
outputPath: paths.outputPath,
|
|
9617
|
-
}
|
|
9793
|
+
};
|
|
9794
|
+
if (body.includePairs === true) {
|
|
9795
|
+
response.records = result.pairs;
|
|
9796
|
+
}
|
|
9797
|
+
sendJson(res, 200, response);
|
|
9618
9798
|
return;
|
|
9619
9799
|
}
|
|
9620
9800
|
|
|
@@ -10398,6 +10578,7 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
10398
10578
|
params: body.params,
|
|
10399
10579
|
mcp: body.mcp,
|
|
10400
10580
|
mcpToolCall: body.mcpToolCall,
|
|
10581
|
+
financialControl: body.financialControl || body.financial_control,
|
|
10401
10582
|
budget: body.budget,
|
|
10402
10583
|
usage: body.usage,
|
|
10403
10584
|
};
|
|
@@ -10640,6 +10821,8 @@ module.exports = {
|
|
|
10640
10821
|
buildLossAnalyticsResponse,
|
|
10641
10822
|
buildIntakeAlertRateLimitKey,
|
|
10642
10823
|
sanitizeHtmlUnsafeJsonValue,
|
|
10824
|
+
buildHostedTenantIdentity,
|
|
10825
|
+
partitionFeedbackPaths,
|
|
10643
10826
|
},
|
|
10644
10827
|
};
|
|
10645
10828
|
|