thumbgate 1.29.1 → 1.30.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/commands/dashboard.md +11 -1
- package/.claude/commands/thumbgate-dashboard.md +23 -8
- package/.claude-plugin/plugin.json +1 -1
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +61 -1
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/forge/forge.yaml +3 -3
- package/adapters/mcp/server-stdio.js +164 -7
- package/adapters/opencode/opencode.json +1 -1
- package/bin/cli.js +7 -5
- package/commands/dashboard.md +11 -1
- package/commands/thumbgate-dashboard.md +23 -8
- package/config/agent-outcome-monitor-thresholds.json +63 -0
- package/config/evals/agent-outcomes-baseline.json +17 -0
- package/config/evals/agent-outcomes-golden.json +412 -0
- package/config/evals/prompt-eval-baseline.json +23 -0
- package/config/mcp-allowlists.json +26 -2
- package/config/post-deploy-marketing-pages.json +26 -1
- package/config/schemas/task-outcome-receipt.schema.json +296 -0
- package/openapi/openapi.yaml +235 -0
- package/package.json +55 -11
- package/public/architecture.html +130 -0
- package/public/assets/diagrams/agent-integration.png +0 -0
- package/public/assets/diagrams/before-after.svg +21 -0
- package/public/assets/diagrams/decision.svg +36 -0
- package/public/assets/diagrams/feedback-pipeline.png +0 -0
- package/public/assets/diagrams/loop.svg +34 -0
- package/public/assets/diagrams/plugin-topology.png +0 -0
- package/public/assets/diagrams/pre-action-gate-loop.svg +59 -0
- package/public/assets/diagrams/stack.svg +18 -0
- package/public/assets/diagrams/thumbgate-architecture.png +0 -0
- package/public/case-studies.html +151 -0
- package/public/eval-scorecard.html +195 -0
- package/public/eval-scorecard.json +18 -0
- package/public/evaluations.html +168 -0
- package/public/index.html +6 -3
- package/public/numbers.html +2 -2
- package/public/whitepaper.html +189 -0
- package/scripts/activation-quickstart.js +1 -0
- package/scripts/agent-outcome-eval.js +130 -0
- package/scripts/agent-outcome-monitor.js +331 -0
- package/scripts/agent-reasoning-traces.js +8 -9
- package/scripts/async-job-runner.js +107 -13
- package/scripts/billing.js +3 -1
- package/scripts/claude-feedback-sync.js +3 -2
- package/scripts/cli-feedback.js +13 -7
- package/scripts/cross-encoder-reranker.js +3 -0
- package/scripts/durability/step.js +121 -12
- package/scripts/feedback-aggregate.js +5 -2
- package/scripts/feedback-loop.js +244 -182
- package/scripts/gates-engine.js +512 -22
- package/scripts/generate-case-study-outreach.js +253 -0
- package/scripts/generate-eval-scorecard.js +276 -0
- package/scripts/growth-campaigns.js +183 -0
- package/scripts/human-escalation.js +265 -0
- package/scripts/hybrid-feedback-context.js +93 -50
- package/scripts/jsonl-watcher.js +1 -0
- package/scripts/judge-reward-function.js +30 -18
- package/scripts/lesson-inference.js +23 -4
- package/scripts/lesson-retrieval.js +71 -4
- package/scripts/lesson-search.js +26 -3
- package/scripts/mcp-config.js +26 -5
- package/scripts/mcp-oauth.js +37 -2
- package/scripts/model-eval.js +308 -0
- package/scripts/parallel-workflow-orchestrator.js +86 -22
- package/scripts/prompt-eval.js +81 -4
- package/scripts/published-cli.js +11 -1
- package/scripts/refresh-proof-pack.js +261 -0
- package/scripts/risk-scorer.js +144 -15
- package/scripts/schedule-manager.js +249 -0
- package/scripts/statusline-local-stats.js +1 -1
- package/scripts/task-outcomes.js +425 -0
- package/scripts/thumbgate-bench.js +13 -0
- package/scripts/tool-contract-validator.js +287 -59
- package/scripts/tool-kpi-tracker.js +124 -0
- package/scripts/tool-registry.js +192 -1
- package/src/api/server.js +355 -89
package/src/api/server.js
CHANGED
|
@@ -166,6 +166,20 @@ const {
|
|
|
166
166
|
const {
|
|
167
167
|
evaluateWorkflowSentinel,
|
|
168
168
|
} = require('../../scripts/workflow-sentinel');
|
|
169
|
+
const {
|
|
170
|
+
calculateTaskOutcomeMetrics,
|
|
171
|
+
getTaskOutcome,
|
|
172
|
+
readTaskOutcomes,
|
|
173
|
+
recordTaskOutcome,
|
|
174
|
+
} = require('../../scripts/task-outcomes');
|
|
175
|
+
const { monitorProductionSignals } = require('../../scripts/agent-outcome-monitor');
|
|
176
|
+
const { computeToolKpis, recordToolCall } = require('../../scripts/tool-kpi-tracker');
|
|
177
|
+
const {
|
|
178
|
+
calculateEscalationMetrics,
|
|
179
|
+
decideEscalation,
|
|
180
|
+
listEscalations,
|
|
181
|
+
requestEscalation,
|
|
182
|
+
} = require('../../scripts/human-escalation');
|
|
169
183
|
const {
|
|
170
184
|
normalizeProviderAction,
|
|
171
185
|
} = require('../../scripts/provider-action-normalizer');
|
|
@@ -224,6 +238,7 @@ const { sendProblem, PROBLEM_TYPES } = require('../../scripts/problem-detail');
|
|
|
224
238
|
const { TOOLS: MCP_TOOLS } = require('../../scripts/tool-registry');
|
|
225
239
|
const mcpOauth = require('../../scripts/mcp-oauth');
|
|
226
240
|
const { packCheckoutReference } = require('../../scripts/checkout-attribution-reference');
|
|
241
|
+
const { normalizeCampaignId } = require('../../scripts/growth-campaigns');
|
|
227
242
|
// OAuth 2.1 (PKCE) authorization-server state for the remote MCP connector
|
|
228
243
|
// (Claude Connectors Directory requires OAuth for authenticated services).
|
|
229
244
|
const oauthStore = mcpOauth.createStore();
|
|
@@ -253,6 +268,11 @@ const CODEX_PLUGIN_PAGE_PATH = path.resolve(__dirname, '../../public/codex-plugi
|
|
|
253
268
|
const COMPARE_PAGE_PATH = path.resolve(__dirname, '../../public/compare.html');
|
|
254
269
|
const LEARN_PAGE_PATH = path.resolve(__dirname, '../../public/learn.html');
|
|
255
270
|
const NUMBERS_PAGE_PATH = path.resolve(__dirname, '../../public/numbers.html');
|
|
271
|
+
const ARCHITECTURE_PAGE_PATH = path.resolve(__dirname, '../../public/architecture.html');
|
|
272
|
+
const WHITEPAPER_PAGE_PATH = path.resolve(__dirname, '../../public/whitepaper.html');
|
|
273
|
+
const EVAL_SCORECARD_PAGE_PATH = path.resolve(__dirname, '../../public/eval-scorecard.html');
|
|
274
|
+
const EVALUATIONS_PAGE_PATH = path.resolve(__dirname, '../../public/evaluations.html');
|
|
275
|
+
const CASE_STUDIES_PAGE_PATH = path.resolve(__dirname, '../../public/case-studies.html');
|
|
256
276
|
const FEDERAL_PAGE_PATH = path.resolve(__dirname, '../../public/federal.html');
|
|
257
277
|
const PRICING_PAGE_PATH = path.resolve(__dirname, '../../public/pricing.html');
|
|
258
278
|
const ABOUT_PAGE_PATH = path.resolve(__dirname, '../../public/about.html');
|
|
@@ -404,7 +424,7 @@ const BUILD_METADATA = resolveBuildMetadata();
|
|
|
404
424
|
const TERMINAL_JOB_STATUSES = new Set(['completed', 'failed', 'cancelled']);
|
|
405
425
|
const IDLE_JOB_STATUSES = new Set(['queued', 'paused', 'resume_requested']);
|
|
406
426
|
const JOB_CONTROL_ACTIONS = new Set(['pause', 'cancel', 'resume']);
|
|
407
|
-
const TRACKED_LINK_QUERY_KEYS = [
|
|
427
|
+
const TRACKED_LINK_QUERY_KEYS = new Set([
|
|
408
428
|
'utm_source',
|
|
409
429
|
'utm_medium',
|
|
410
430
|
'utm_campaign',
|
|
@@ -432,7 +452,7 @@ const TRACKED_LINK_QUERY_KEYS = [
|
|
|
432
452
|
'seat_count',
|
|
433
453
|
'landing_path',
|
|
434
454
|
'referrer_host',
|
|
435
|
-
];
|
|
455
|
+
]);
|
|
436
456
|
const TRACKED_LINK_TARGETS = Object.freeze({
|
|
437
457
|
gpt: {
|
|
438
458
|
href: 'https://chatgpt.com/g/g-69dcfd1cd5f881918ae31874631d6f08-thumbgate',
|
|
@@ -863,11 +883,13 @@ function updateLessonRecord(feedbackDir, lessonId, updater) {
|
|
|
863
883
|
}
|
|
864
884
|
|
|
865
885
|
function getPublicMcpTools() {
|
|
866
|
-
|
|
886
|
+
const { getExposedTools } = require('../../adapters/mcp/server-stdio');
|
|
887
|
+
return getExposedTools().map((tool) => ({
|
|
867
888
|
name: tool.name,
|
|
868
889
|
...(tool.title ? { title: tool.title } : {}),
|
|
869
890
|
description: tool.description,
|
|
870
891
|
inputSchema: tool.inputSchema,
|
|
892
|
+
...(tool.outputSchema ? { outputSchema: tool.outputSchema } : {}),
|
|
871
893
|
// Serve the tool-registry annotations (readOnlyHint/destructiveHint). Required
|
|
872
894
|
// by the Claude Connectors Directory (missing annotations = the #1 rejection
|
|
873
895
|
// cause) and used by MCP clients for permission prompts. Was being dropped here.
|
|
@@ -876,13 +898,7 @@ function getPublicMcpTools() {
|
|
|
876
898
|
}
|
|
877
899
|
|
|
878
900
|
function getServerCardTools() {
|
|
879
|
-
return
|
|
880
|
-
name: tool.name,
|
|
881
|
-
...(tool.title ? { title: tool.title } : {}),
|
|
882
|
-
description: tool.description,
|
|
883
|
-
inputSchema: tool.inputSchema,
|
|
884
|
-
...(tool.annotations ? { annotations: tool.annotations } : {}),
|
|
885
|
-
}));
|
|
901
|
+
return getPublicMcpTools();
|
|
886
902
|
}
|
|
887
903
|
|
|
888
904
|
function buildPublicUrl(hostedConfig, pathname) {
|
|
@@ -892,7 +908,7 @@ function buildPublicUrl(hostedConfig, pathname) {
|
|
|
892
908
|
const VERIFICATION_EVIDENCE_URL = 'https://github.com/IgorGanapolsky/ThumbGate/blob/main/docs/VERIFICATION_EVIDENCE.md';
|
|
893
909
|
|
|
894
910
|
function getToolDiscoveryIndex(hostedConfig) {
|
|
895
|
-
return
|
|
911
|
+
return getPublicMcpTools().map((tool) => ({
|
|
896
912
|
name: tool.name,
|
|
897
913
|
description: tool.description,
|
|
898
914
|
annotations: tool.annotations || {},
|
|
@@ -902,7 +918,7 @@ function getToolDiscoveryIndex(hostedConfig) {
|
|
|
902
918
|
|
|
903
919
|
function getContextFootprintReport(hostedConfig) {
|
|
904
920
|
return buildContextFootprintReport({
|
|
905
|
-
tools:
|
|
921
|
+
tools: getPublicMcpTools(),
|
|
906
922
|
schemaUrlTemplate: buildPublicUrl(hostedConfig, '/.well-known/mcp/tools/{name}.json'),
|
|
907
923
|
});
|
|
908
924
|
}
|
|
@@ -1260,7 +1276,8 @@ function inferSearchQuery(referrer) {
|
|
|
1260
1276
|
|
|
1261
1277
|
function getAttributionValue(params, key, fallbackValue) {
|
|
1262
1278
|
const value = params.get(key);
|
|
1263
|
-
|
|
1279
|
+
const resolved = value?.trim() || fallbackValue;
|
|
1280
|
+
return key === 'utm_campaign' ? normalizeCampaignId(resolved) : resolved;
|
|
1264
1281
|
}
|
|
1265
1282
|
|
|
1266
1283
|
function parseUrlSearchParams(urlValue) {
|
|
@@ -1632,17 +1649,28 @@ function projectWorkflowIntakeQueueLead(lead = {}, options = {}) {
|
|
|
1632
1649
|
page: attribution.page || null,
|
|
1633
1650
|
landingPath: attribution.landingPath || null,
|
|
1634
1651
|
},
|
|
1635
|
-
nextOperatorStep: options
|
|
1636
|
-
? 'request_action_time_approval'
|
|
1637
|
-
: options.discoveryPacket?.status === 'approval_ready_not_authorized'
|
|
1638
|
-
? 'request_action_time_approval_for_discovery'
|
|
1639
|
-
: lead.status === 'new' ? 'review_and_qualify' : 'prepare_scope_or_hold',
|
|
1652
|
+
nextOperatorStep: resolveNextOperatorStep(lead, options),
|
|
1640
1653
|
qualificationCard: options.qualificationCard || buildIntakeQualificationCard(lead, options),
|
|
1641
1654
|
discoveryPacket: options.discoveryPacket || buildIntakeDiscoveryPacket(lead, options),
|
|
1642
1655
|
closePacket: options.closePacket || buildIntakeClosePacket(lead, options),
|
|
1643
1656
|
};
|
|
1644
1657
|
}
|
|
1645
1658
|
|
|
1659
|
+
/**
|
|
1660
|
+
* Which action the operator should take next on an intake lead. Extracted from a three-level
|
|
1661
|
+
* nested ternary: the precedence (close packet, then discovery packet, then lead status) is the
|
|
1662
|
+
* actual logic and is far easier to check when it reads top to bottom.
|
|
1663
|
+
*/
|
|
1664
|
+
function resolveNextOperatorStep(lead, options) {
|
|
1665
|
+
if (options.closePacket?.status === 'approval_ready_not_authorized') {
|
|
1666
|
+
return 'request_action_time_approval';
|
|
1667
|
+
}
|
|
1668
|
+
if (options.discoveryPacket?.status === 'approval_ready_not_authorized') {
|
|
1669
|
+
return 'request_action_time_approval_for_discovery';
|
|
1670
|
+
}
|
|
1671
|
+
return lead.status === 'new' ? 'review_and_qualify' : 'prepare_scope_or_hold';
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1646
1674
|
function buildWorkflowIntakeQueue(parsed, workflowSprintIntake, feedbackDir, options = {}) {
|
|
1647
1675
|
const { limit, statuses } = resolveWorkflowIntakeQueueOptions(parsed, workflowSprintIntake);
|
|
1648
1676
|
const leads = workflowSprintIntake.loadWorkflowSprintLeads(feedbackDir);
|
|
@@ -1721,7 +1749,7 @@ function sendInvalidAnalyticsWindowProblem(res, title, err) {
|
|
|
1721
1749
|
}
|
|
1722
1750
|
|
|
1723
1751
|
function sendWorkflowIntakeQueueProblem(res, err) {
|
|
1724
|
-
const status = Number(err
|
|
1752
|
+
const status = Number(err?.statusCode) || 400;
|
|
1725
1753
|
sendProblem(res, {
|
|
1726
1754
|
type: status >= 500 ? PROBLEM_TYPES.INTERNAL : PROBLEM_TYPES.INVALID_REQUEST,
|
|
1727
1755
|
title: status >= 500
|
|
@@ -2676,7 +2704,10 @@ function appendTrackedLinkQueryParams(destinationUrl, parsed, target) {
|
|
|
2676
2704
|
for (const key of TRACKED_LINK_QUERY_KEYS) {
|
|
2677
2705
|
const value = params.get(key);
|
|
2678
2706
|
if (value && value.trim()) {
|
|
2679
|
-
|
|
2707
|
+
const normalizedValue = key === 'utm_campaign'
|
|
2708
|
+
? normalizeCampaignId(value)
|
|
2709
|
+
: value.trim();
|
|
2710
|
+
destinationUrl.searchParams.set(key, normalizedValue);
|
|
2680
2711
|
}
|
|
2681
2712
|
}
|
|
2682
2713
|
if (target.allowCustomerEmail) {
|
|
@@ -2717,7 +2748,7 @@ function buildTrackedLinkAttribution(target, parsed, req, journeyState, destinat
|
|
|
2717
2748
|
const source = pickFirstText(
|
|
2718
2749
|
params.get('source'),
|
|
2719
2750
|
params.get('utm_source'),
|
|
2720
|
-
target.defaults
|
|
2751
|
+
target.defaults?.utm_source,
|
|
2721
2752
|
inferSource(referrerHost)
|
|
2722
2753
|
);
|
|
2723
2754
|
|
|
@@ -2731,19 +2762,21 @@ function buildTrackedLinkAttribution(target, parsed, req, journeyState, destinat
|
|
|
2731
2762
|
traceId: pickFirstText(params.get('trace_id')),
|
|
2732
2763
|
source,
|
|
2733
2764
|
utmSource: pickFirstText(params.get('utm_source'), source),
|
|
2734
|
-
utmMedium: pickFirstText(params.get('utm_medium'), target.defaults
|
|
2735
|
-
utmCampaign:
|
|
2765
|
+
utmMedium: pickFirstText(params.get('utm_medium'), target.defaults?.utm_medium, 'link_router'),
|
|
2766
|
+
utmCampaign: normalizeCampaignId(
|
|
2767
|
+
pickFirstText(params.get('utm_campaign'), target.defaults?.utm_campaign, 'first_party_redirect')
|
|
2768
|
+
),
|
|
2736
2769
|
utmContent: pickFirstText(params.get('utm_content')),
|
|
2737
2770
|
utmTerm: pickFirstText(params.get('utm_term')),
|
|
2738
2771
|
creator: pickFirstText(params.get('creator'), params.get('creator_handle')),
|
|
2739
2772
|
community: pickFirstText(params.get('community'), params.get('subreddit')),
|
|
2740
2773
|
postId: pickFirstText(params.get('post_id')),
|
|
2741
2774
|
commentId: pickFirstText(params.get('comment_id')),
|
|
2742
|
-
campaignVariant: pickFirstText(params.get('campaign_variant'), target.defaults
|
|
2775
|
+
campaignVariant: pickFirstText(params.get('campaign_variant'), target.defaults?.campaign_variant),
|
|
2743
2776
|
offerCode: pickFirstText(params.get('offer_code')),
|
|
2744
2777
|
ctaId: pickFirstText(params.get('cta_id'), target.ctaId),
|
|
2745
2778
|
ctaPlacement: pickFirstText(params.get('cta_placement'), target.ctaPlacement),
|
|
2746
|
-
planId: pickFirstText(params.get('plan_id'), target.defaults
|
|
2779
|
+
planId: pickFirstText(params.get('plan_id'), target.defaults?.plan_id),
|
|
2747
2780
|
landingPath: pickFirstText(params.get('landing_path'), `/go/${target.slug}`),
|
|
2748
2781
|
page: `/go/${target.slug}`,
|
|
2749
2782
|
referrer,
|
|
@@ -3809,6 +3842,12 @@ function renderSitemapXml(runtimeConfig) {
|
|
|
3809
3842
|
{ path: '/codex-enterprise', changefreq: 'weekly', priority: '0.85' },
|
|
3810
3843
|
{ path: '/agents-cost-savings', changefreq: 'weekly', priority: '0.85' },
|
|
3811
3844
|
{ path: '/ai-malpractice-prevention', changefreq: 'weekly', priority: '0.9' },
|
|
3845
|
+
{ path: '/whitepaper', changefreq: 'weekly', priority: '0.9' },
|
|
3846
|
+
{ path: '/architecture', changefreq: 'weekly', priority: '0.85' },
|
|
3847
|
+
{ path: '/eval-scorecard', changefreq: 'weekly', priority: '0.9' },
|
|
3848
|
+
{ path: '/evaluations', changefreq: 'weekly', priority: '0.85' },
|
|
3849
|
+
{ path: '/case-studies', changefreq: 'weekly', priority: '0.9' },
|
|
3850
|
+
{ path: '/numbers', changefreq: 'weekly', priority: '0.8' },
|
|
3812
3851
|
{ path: '/learn/background-agent-control-layer', changefreq: 'weekly', priority: '0.85' },
|
|
3813
3852
|
{ path: '/learn/ac-dc-runtime-enforcement', changefreq: 'weekly', priority: '0.85' },
|
|
3814
3853
|
{ path: '/learn/feedback-loop-vs-decision-layer', changefreq: 'weekly', priority: '0.9' },
|
|
@@ -3875,6 +3914,8 @@ function buildHostedRuntimePresence(hostedConfig, { expectedApiKey, expectedOper
|
|
|
3875
3914
|
THUMBGATE_FEEDBACK_DIR: Boolean(process.env.THUMBGATE_FEEDBACK_DIR),
|
|
3876
3915
|
THUMBGATE_OPERATOR_KEY: Boolean(expectedOperatorKey),
|
|
3877
3916
|
THUMBGATE_API_KEY: Boolean(expectedApiKey),
|
|
3917
|
+
THUMBGATE_HUMAN_REVIEWER_KEY: Boolean(process.env.THUMBGATE_HUMAN_REVIEWER_KEY),
|
|
3918
|
+
THUMBGATE_HUMAN_REVIEWER_ID: Boolean(process.env.THUMBGATE_HUMAN_REVIEWER_ID),
|
|
3878
3919
|
THUMBGATE_PUBLIC_APP_ORIGIN: Boolean(hostedConfig.appOrigin),
|
|
3879
3920
|
THUMBGATE_BILLING_API_BASE_URL: Boolean(hostedConfig.billingApiBaseUrl),
|
|
3880
3921
|
THUMBGATE_GA_MEASUREMENT_ID: Boolean(hostedConfig.gaMeasurementId),
|
|
@@ -4890,6 +4931,15 @@ function getExpectedOperatorKey() {
|
|
|
4890
4931
|
return key || null;
|
|
4891
4932
|
}
|
|
4892
4933
|
|
|
4934
|
+
function getHumanReviewerConfig(expectedApiKey, expectedOperatorKey) {
|
|
4935
|
+
const key = String(process.env.THUMBGATE_HUMAN_REVIEWER_KEY || '').trim();
|
|
4936
|
+
const id = String(process.env.THUMBGATE_HUMAN_REVIEWER_ID || '').trim();
|
|
4937
|
+
const independent = Boolean(key)
|
|
4938
|
+
&& !safeKeyEqual(key, expectedApiKey)
|
|
4939
|
+
&& !safeKeyEqual(key, expectedOperatorKey);
|
|
4940
|
+
return { key: key || null, id: id || null, independent };
|
|
4941
|
+
}
|
|
4942
|
+
|
|
4893
4943
|
function isAuthorized(req, expected) {
|
|
4894
4944
|
if (!expected) return true;
|
|
4895
4945
|
const token = extractApiKey(req);
|
|
@@ -4957,6 +5007,21 @@ function safeKeyEqual(a, b) {
|
|
|
4957
5007
|
return require('crypto').timingSafeEqual(x, y);
|
|
4958
5008
|
}
|
|
4959
5009
|
|
|
5010
|
+
function authenticateHumanReviewer(req, reviewerConfig) {
|
|
5011
|
+
if (!reviewerConfig.key || !reviewerConfig.id || !reviewerConfig.independent) {
|
|
5012
|
+
throw createHttpError(503, 'Human escalation decisions are unavailable until an independent reviewer key and identity are configured');
|
|
5013
|
+
}
|
|
5014
|
+
const header = req.headers['x-thumbgate-human-reviewer-key'];
|
|
5015
|
+
const presented = Array.isArray(header) ? header[0] : header;
|
|
5016
|
+
if (!safeKeyEqual(presented, reviewerConfig.key)) {
|
|
5017
|
+
throw createHttpError(403, 'A valid human reviewer credential is required to decide an escalation');
|
|
5018
|
+
}
|
|
5019
|
+
return {
|
|
5020
|
+
id: reviewerConfig.id,
|
|
5021
|
+
kind: 'human',
|
|
5022
|
+
};
|
|
5023
|
+
}
|
|
5024
|
+
|
|
4960
5025
|
function resolveKeyRole(key) {
|
|
4961
5026
|
const k = String(key || '').trim();
|
|
4962
5027
|
const adminKey = String(process.env.THUMBGATE_API_KEY || '').trim();
|
|
@@ -5143,6 +5208,7 @@ function resolveDocumentImportFilePath(inputPath, options = {}) {
|
|
|
5143
5208
|
function createApiServer() {
|
|
5144
5209
|
const expectedApiKey = getExpectedApiKey();
|
|
5145
5210
|
const expectedOperatorKey = getExpectedOperatorKey();
|
|
5211
|
+
const humanReviewerConfig = getHumanReviewerConfig(expectedApiKey, expectedOperatorKey);
|
|
5146
5212
|
|
|
5147
5213
|
const eventBus = new EventEmitter();
|
|
5148
5214
|
eventBus.setMaxListeners(200);
|
|
@@ -5233,6 +5299,14 @@ function createApiServer() {
|
|
|
5233
5299
|
? mcpOauth.tokenAudienceValid(oauthSession, resourceUrl)
|
|
5234
5300
|
: rawKeyValid;
|
|
5235
5301
|
if (!authed) {
|
|
5302
|
+
recordToolCall({
|
|
5303
|
+
toolName: msg.params?.name || 'unknown',
|
|
5304
|
+
serverName: 'mcp-http',
|
|
5305
|
+
latencyMs: 0,
|
|
5306
|
+
success: false,
|
|
5307
|
+
agentId: 'unauthenticated',
|
|
5308
|
+
metadata: { denied: true, deniedReason: 'authentication_required' },
|
|
5309
|
+
});
|
|
5236
5310
|
res.writeHead(401, {
|
|
5237
5311
|
'Content-Type': 'application/json',
|
|
5238
5312
|
// RFC 9728: point unauthenticated clients at the resource metadata.
|
|
@@ -5255,6 +5329,14 @@ function createApiServer() {
|
|
|
5255
5329
|
const tool = MCP_TOOLS.find((t) => t.name === name);
|
|
5256
5330
|
const readOnly = Boolean(tool && tool.annotations && tool.annotations.readOnlyHint === true);
|
|
5257
5331
|
if (!readOnly) {
|
|
5332
|
+
recordToolCall({
|
|
5333
|
+
toolName: name || 'unknown',
|
|
5334
|
+
serverName: 'mcp-http',
|
|
5335
|
+
latencyMs: 0,
|
|
5336
|
+
success: false,
|
|
5337
|
+
agentId: 'reviewer',
|
|
5338
|
+
metadata: { denied: true, deniedReason: 'reviewer_read_only' },
|
|
5339
|
+
});
|
|
5258
5340
|
sendJson(res, 200, {
|
|
5259
5341
|
jsonrpc: '2.0', id: msg.id,
|
|
5260
5342
|
error: { code: -32002, message: `Tool "${name}" requires write access; the reviewer credential is read-only.` },
|
|
@@ -5262,15 +5344,39 @@ function createApiServer() {
|
|
|
5262
5344
|
return;
|
|
5263
5345
|
}
|
|
5264
5346
|
}
|
|
5347
|
+
if (oauthSession) {
|
|
5348
|
+
const name = msg.params && msg.params.name;
|
|
5349
|
+
const tool = MCP_TOOLS.find((candidate) => candidate.name === name);
|
|
5350
|
+
const requiredScope = tool?.annotations?.readOnlyHint === true ? 'mcp:read' : 'mcp:write';
|
|
5351
|
+
if (!mcpOauth.scopeAllows(oauthSession, requiredScope)) {
|
|
5352
|
+
recordToolCall({
|
|
5353
|
+
toolName: name || 'unknown',
|
|
5354
|
+
serverName: 'mcp-http',
|
|
5355
|
+
latencyMs: 0,
|
|
5356
|
+
success: false,
|
|
5357
|
+
agentId: oauthSession.clientId || 'oauth-client',
|
|
5358
|
+
metadata: {
|
|
5359
|
+
denied: true,
|
|
5360
|
+
deniedReason: 'insufficient_scope',
|
|
5361
|
+
requiredScope,
|
|
5362
|
+
},
|
|
5363
|
+
});
|
|
5364
|
+
sendJson(res, 200, {
|
|
5365
|
+
jsonrpc: '2.0', id: msg.id,
|
|
5366
|
+
error: { code: -32003, message: `OAuth token is missing required scope "${requiredScope}".` },
|
|
5367
|
+
});
|
|
5368
|
+
return;
|
|
5369
|
+
}
|
|
5370
|
+
}
|
|
5265
5371
|
(async () => {
|
|
5266
5372
|
try {
|
|
5267
5373
|
const { callTool } = require('../../adapters/mcp/server-stdio');
|
|
5268
|
-
const name = msg.params
|
|
5374
|
+
const name = msg.params?.name;
|
|
5269
5375
|
const args = (msg.params && msg.params.arguments) || {};
|
|
5270
5376
|
const result = await callTool(name, args);
|
|
5271
5377
|
sendJson(res, 200, {
|
|
5272
5378
|
jsonrpc: '2.0', id: msg.id,
|
|
5273
|
-
result
|
|
5379
|
+
result,
|
|
5274
5380
|
});
|
|
5275
5381
|
} catch (err) {
|
|
5276
5382
|
sendJson(res, 200, {
|
|
@@ -5553,7 +5659,7 @@ function createApiServer() {
|
|
|
5553
5659
|
let trackedParsed = parsed;
|
|
5554
5660
|
if (req.method === 'POST') {
|
|
5555
5661
|
const target = getTrackedLinkTarget(trackedLinkMatch[1]);
|
|
5556
|
-
if (!target
|
|
5662
|
+
if (!target?.requiresPost) {
|
|
5557
5663
|
sendJson(res, target ? 405 : 404, target
|
|
5558
5664
|
? { error: 'POST is not supported for this tracked link' }
|
|
5559
5665
|
: { error: 'Tracked link not found', allowed: Object.keys(TRACKED_LINK_TARGETS) }, {
|
|
@@ -5572,7 +5678,7 @@ function createApiServer() {
|
|
|
5572
5678
|
const form = await parseFormBody(req, 16 * 1024);
|
|
5573
5679
|
trackedParsed = new URL(parsed.toString());
|
|
5574
5680
|
for (const [key, value] of Object.entries(form)) {
|
|
5575
|
-
if (key === 'customer_email' || TRACKED_LINK_QUERY_KEYS.
|
|
5681
|
+
if (key === 'customer_email' || TRACKED_LINK_QUERY_KEYS.has(key)) {
|
|
5576
5682
|
trackedParsed.searchParams.set(key, String(value || '').trim());
|
|
5577
5683
|
}
|
|
5578
5684
|
}
|
|
@@ -5644,10 +5750,31 @@ function createApiServer() {
|
|
|
5644
5750
|
return;
|
|
5645
5751
|
}
|
|
5646
5752
|
|
|
5647
|
-
//
|
|
5648
|
-
|
|
5753
|
+
// GET/HEAD only render a confirmation. Capture requires an authenticated
|
|
5754
|
+
// POST so prefetchers and link checkers cannot create human feedback.
|
|
5755
|
+
if ((isGetLikeRequest || req.method === 'POST') && pathname === '/feedback/quick') {
|
|
5649
5756
|
const signal = parsed.searchParams.get('signal');
|
|
5650
5757
|
if (signal === 'up' || signal === 'down') {
|
|
5758
|
+
const emoji = signal === 'up' ? '👍' : '👎';
|
|
5759
|
+
const label = signal === 'up' ? 'Positive' : 'Negative';
|
|
5760
|
+
if (isHeadRequest) {
|
|
5761
|
+
sendHtml(res, 200, '', {}, { headOnly: true });
|
|
5762
|
+
return;
|
|
5763
|
+
}
|
|
5764
|
+
if (req.method === 'GET') {
|
|
5765
|
+
sendHtml(res, 200, `<!DOCTYPE html><html><head><meta charset="utf-8"><title>ThumbGate feedback</title></head>
|
|
5766
|
+
<body style="background:#0a0a0a;color:#fff;font-family:system-ui;display:grid;place-items:center;height:100vh;margin:0">
|
|
5767
|
+
<form method="post" action="/feedback/quick?signal=${signal}" style="text-align:center;background:#141414;padding:40px;border-radius:16px">
|
|
5768
|
+
<div style="font-size:64px">${emoji}</div><h1>Record ${label.toLowerCase()} feedback?</h1>
|
|
5769
|
+
<p>This confirmation prevents previews and crawlers from recording feedback.</p>
|
|
5770
|
+
<button type="submit" style="padding:10px 20px;font-weight:700">Record ${emoji} feedback</button>
|
|
5771
|
+
</form></body></html>`);
|
|
5772
|
+
return;
|
|
5773
|
+
}
|
|
5774
|
+
if (!isAuthorized(req, expectedApiKey)) {
|
|
5775
|
+
sendJson(res, 401, { error: 'Authentication required to record human feedback' });
|
|
5776
|
+
return;
|
|
5777
|
+
}
|
|
5651
5778
|
const chatHistory = readRecentConversationWindow({
|
|
5652
5779
|
feedbackDir: requestSafeDataDir,
|
|
5653
5780
|
limit: 10,
|
|
@@ -5657,35 +5784,17 @@ function createApiServer() {
|
|
|
5657
5784
|
context: 'Quick capture from Claude Code statusline',
|
|
5658
5785
|
chatHistory,
|
|
5659
5786
|
tags: ['statusline', 'quick-capture'],
|
|
5787
|
+
reviewOrigin: 'human',
|
|
5660
5788
|
});
|
|
5661
|
-
const emoji = signal === 'up' ? '👍' : '👎';
|
|
5662
|
-
const color = signal === 'up' ? '#22c55e' : '#ef4444';
|
|
5663
|
-
const label = signal === 'up' ? 'Positive' : 'Negative';
|
|
5664
5789
|
const opposite = signal === 'up' ? 'down' : 'up';
|
|
5665
5790
|
const oppEmoji = signal === 'up' ? '👎' : '👍';
|
|
5666
5791
|
const feedbackId = result.feedbackEvent?.id || 'saved';
|
|
5667
5792
|
const promoted = result.accepted ? 'Promoted to memory' : 'Stored';
|
|
5668
5793
|
sendHtml(res, 200, `<!DOCTYPE html><html><head><meta charset="utf-8"><title>ThumbGate — ${label} feedback</title>
|
|
5669
5794
|
<style>
|
|
5670
|
-
*{box-sizing:border-box}
|
|
5671
5795
|
body{background:#0a0a0a;color:#fff;font-family:system-ui,-apple-system,sans-serif;display:flex;align-items:center;justify-content:center;height:100vh;margin:0}
|
|
5672
5796
|
.card{text-align:center;background:#141414;border:1px solid #222;border-radius:16px;padding:48px 40px;max-width:420px;width:90%;box-shadow:0 8px 32px rgba(0,0,0,.5)}
|
|
5673
|
-
.emoji{font-size:
|
|
5674
|
-
@keyframes pop{0%{transform:scale(0)}50%{transform:scale(1.2)}100%{transform:scale(1)}}
|
|
5675
|
-
.msg{font-size:22px;color:${color};font-weight:700;margin-bottom:4px}
|
|
5676
|
-
.sub{font-size:13px;color:#666;margin-top:6px;font-family:ui-monospace,monospace}
|
|
5677
|
-
.context-form{margin-top:20px;text-align:left}
|
|
5678
|
-
.context-form label{font-size:12px;color:#888;display:block;margin-bottom:6px}
|
|
5679
|
-
.context-form textarea{width:100%;background:#1a1a1a;border:1px solid #333;border-radius:8px;color:#ccc;padding:10px;font-size:13px;resize:vertical;min-height:60px;font-family:system-ui}
|
|
5680
|
-
.context-form textarea:focus{outline:none;border-color:${color}}
|
|
5681
|
-
.context-form button{margin-top:8px;background:${color};color:#000;border:none;border-radius:8px;padding:8px 20px;font-size:13px;font-weight:600;cursor:pointer}
|
|
5682
|
-
.context-form button:hover{opacity:.85}
|
|
5683
|
-
.actions{margin-top:24px;display:flex;gap:12px;justify-content:center;flex-wrap:wrap}
|
|
5684
|
-
.actions a{color:#22d3ee;text-decoration:none;font-size:13px;padding:8px 16px;border:1px solid #333;border-radius:8px;transition:all .15s}
|
|
5685
|
-
.actions a:hover{background:#1a2a2e;border-color:#22d3ee}
|
|
5686
|
-
.toast{position:fixed;bottom:24px;left:50%;transform:translateX(-50%);background:#22c55e;color:#000;padding:10px 24px;border-radius:8px;font-size:14px;font-weight:600;display:none;animation:slideUp .3s ease-out}
|
|
5687
|
-
@keyframes slideUp{from{opacity:0;transform:translateX(-50%) translateY(12px)}to{opacity:1;transform:translateX(-50%) translateY(0)}}
|
|
5688
|
-
.badge{display:inline-block;font-size:11px;padding:2px 8px;border-radius:4px;background:#1a1a1a;border:1px solid #333;color:#888;margin-top:8px}
|
|
5797
|
+
.emoji{font-size:64px}.sub,label{color:#888}textarea{width:100%;min-height:60px}.actions{margin-top:20px}.actions a{color:#22d3ee;margin:8px}
|
|
5689
5798
|
</style></head><body>
|
|
5690
5799
|
<div class="card">
|
|
5691
5800
|
<div class="emoji">${emoji}</div>
|
|
@@ -5698,7 +5807,7 @@ body{background:#0a0a0a;color:#fff;font-family:system-ui,-apple-system,sans-seri
|
|
|
5698
5807
|
</div>
|
|
5699
5808
|
<div class="actions">
|
|
5700
5809
|
<a href="/lessons/${feedbackId}" title="View the full lesson and edit it">📋 View Lesson</a>
|
|
5701
|
-
<a href="/feedback/quick?signal=${opposite}" title="Meant to click ${oppEmoji}?">
|
|
5810
|
+
<a href="/feedback/quick?signal=${opposite}" title="Meant to click ${oppEmoji}?">Record ${oppEmoji} instead</a>
|
|
5702
5811
|
<a href="/dashboard">Dashboard →</a>
|
|
5703
5812
|
</div>
|
|
5704
5813
|
</div>
|
|
@@ -5722,6 +5831,10 @@ async function addContext(){
|
|
|
5722
5831
|
}
|
|
5723
5832
|
|
|
5724
5833
|
if (req.method === 'POST' && pathname === '/feedback/quick/context') {
|
|
5834
|
+
if (!isAuthorized(req, expectedApiKey)) {
|
|
5835
|
+
sendJson(res, 401, { error: 'Authentication required to update human feedback' });
|
|
5836
|
+
return;
|
|
5837
|
+
}
|
|
5725
5838
|
const body = await parseJsonBody(req);
|
|
5726
5839
|
const signal = body.signal;
|
|
5727
5840
|
const context = typeof body.context === 'string' ? body.context.trim() : '';
|
|
@@ -6166,6 +6279,74 @@ async function addContext(){
|
|
|
6166
6279
|
return;
|
|
6167
6280
|
}
|
|
6168
6281
|
|
|
6282
|
+
if (isGetLikeRequest && (pathname === '/architecture' || pathname === '/architecture.html')) {
|
|
6283
|
+
try {
|
|
6284
|
+
servePublicMarketingPage({
|
|
6285
|
+
req,
|
|
6286
|
+
res,
|
|
6287
|
+
parsed,
|
|
6288
|
+
hostedConfig,
|
|
6289
|
+
isHeadRequest,
|
|
6290
|
+
renderHtml: () => fs.readFileSync(ARCHITECTURE_PAGE_PATH, 'utf-8'),
|
|
6291
|
+
extraTelemetry: { pageType: 'architecture' },
|
|
6292
|
+
});
|
|
6293
|
+
} catch {
|
|
6294
|
+
sendJson(res, 404, { error: 'Architecture page not found' });
|
|
6295
|
+
}
|
|
6296
|
+
return;
|
|
6297
|
+
}
|
|
6298
|
+
|
|
6299
|
+
if (isGetLikeRequest && (pathname === '/whitepaper' || pathname === '/whitepaper.html' || pathname === '/how-we-know' || pathname === '/how-we-know-it-works')) {
|
|
6300
|
+
try {
|
|
6301
|
+
servePublicMarketingPage({
|
|
6302
|
+
req,
|
|
6303
|
+
res,
|
|
6304
|
+
parsed,
|
|
6305
|
+
hostedConfig,
|
|
6306
|
+
isHeadRequest,
|
|
6307
|
+
renderHtml: () => fs.readFileSync(WHITEPAPER_PAGE_PATH, 'utf-8'),
|
|
6308
|
+
extraTelemetry: { pageType: 'whitepaper' },
|
|
6309
|
+
});
|
|
6310
|
+
} catch {
|
|
6311
|
+
sendJson(res, 404, { error: 'White paper page not found' });
|
|
6312
|
+
}
|
|
6313
|
+
return;
|
|
6314
|
+
}
|
|
6315
|
+
|
|
6316
|
+
if (isGetLikeRequest && (pathname === '/eval-scorecard' || pathname === '/eval-scorecard.html' || pathname === '/scorecard' || pathname === '/bench')) {
|
|
6317
|
+
try {
|
|
6318
|
+
servePublicMarketingPage({
|
|
6319
|
+
req,
|
|
6320
|
+
res,
|
|
6321
|
+
parsed,
|
|
6322
|
+
hostedConfig,
|
|
6323
|
+
isHeadRequest,
|
|
6324
|
+
renderHtml: () => fs.readFileSync(EVAL_SCORECARD_PAGE_PATH, 'utf-8'),
|
|
6325
|
+
extraTelemetry: { pageType: 'eval_scorecard' },
|
|
6326
|
+
});
|
|
6327
|
+
} catch {
|
|
6328
|
+
sendJson(res, 404, { error: 'Eval scorecard page not found' });
|
|
6329
|
+
}
|
|
6330
|
+
return;
|
|
6331
|
+
}
|
|
6332
|
+
|
|
6333
|
+
if (isGetLikeRequest && (pathname === '/evaluations' || pathname === '/evaluations.html')) {
|
|
6334
|
+
try {
|
|
6335
|
+
servePublicMarketingPage({
|
|
6336
|
+
req,
|
|
6337
|
+
res,
|
|
6338
|
+
parsed,
|
|
6339
|
+
hostedConfig,
|
|
6340
|
+
isHeadRequest,
|
|
6341
|
+
renderHtml: () => fs.readFileSync(EVALUATIONS_PAGE_PATH, 'utf-8'),
|
|
6342
|
+
extraTelemetry: { pageType: 'evaluations' },
|
|
6343
|
+
});
|
|
6344
|
+
} catch {
|
|
6345
|
+
sendJson(res, 404, { error: 'Evaluations page not found' });
|
|
6346
|
+
}
|
|
6347
|
+
return;
|
|
6348
|
+
}
|
|
6349
|
+
|
|
6169
6350
|
if (isGetLikeRequest && (pathname === '/federal' || pathname === '/federal.html' || pathname === '/government' || pathname === '/gov')) {
|
|
6170
6351
|
// Federal lead-gen page. Routed through servePublicMarketingPage so agency
|
|
6171
6352
|
// arrivals via SBIR / GSA / outbound channels capture UTM attribution and
|
|
@@ -6430,7 +6611,7 @@ async function addContext(){
|
|
|
6430
6611
|
if (
|
|
6431
6612
|
key === 'confirm'
|
|
6432
6613
|
|| key === 'customer_email'
|
|
6433
|
-
|| TRACKED_LINK_QUERY_KEYS.
|
|
6614
|
+
|| TRACKED_LINK_QUERY_KEYS.has(key)
|
|
6434
6615
|
) {
|
|
6435
6616
|
parsed.searchParams.set(key, String(value || '').trim());
|
|
6436
6617
|
}
|
|
@@ -6946,7 +7127,8 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
6946
7127
|
// are configured (production) the key MUST match a configured admin /
|
|
6947
7128
|
// operator / reviewer key — otherwise OAuth would authenticate nobody.
|
|
6948
7129
|
// In insecure/dev mode (no keys configured) any non-empty key is accepted.
|
|
6949
|
-
|
|
7130
|
+
const keyRole = resolveKeyRole(form.get('api_key') || '');
|
|
7131
|
+
if (!keyRole) {
|
|
6950
7132
|
sendJson(res, 400, { error: 'access_denied', error_description: 'invalid ThumbGate API key' });
|
|
6951
7133
|
return;
|
|
6952
7134
|
}
|
|
@@ -6956,6 +7138,7 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
6956
7138
|
codeChallenge: authorizationParams.codeChallenge,
|
|
6957
7139
|
codeChallengeMethod: authorizationParams.codeChallengeMethod,
|
|
6958
7140
|
scope: authorizationParams.scope,
|
|
7141
|
+
allowedScopes: keyRole === 'reviewer' ? ['mcp:read'] : mcpOauth.SUPPORTED_SCOPES,
|
|
6959
7142
|
resource: authorizationParams.resource || buildPublicUrl(hostedConfig, '/mcp'),
|
|
6960
7143
|
boundKey: form.get('api_key') || '',
|
|
6961
7144
|
state,
|
|
@@ -6999,7 +7182,7 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
6999
7182
|
sendJson(res, 200, {
|
|
7000
7183
|
name: 'thumbgate',
|
|
7001
7184
|
version: pkg.version,
|
|
7002
|
-
count:
|
|
7185
|
+
count: getPublicMcpTools().length,
|
|
7003
7186
|
tools: getToolDiscoveryIndex(hostedConfig),
|
|
7004
7187
|
}, {}, {
|
|
7005
7188
|
headOnly: isHeadRequest,
|
|
@@ -7032,7 +7215,7 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
7032
7215
|
});
|
|
7033
7216
|
return;
|
|
7034
7217
|
}
|
|
7035
|
-
const tool =
|
|
7218
|
+
const tool = getPublicMcpTools().find((candidate) => candidate.name === toolName);
|
|
7036
7219
|
if (!tool) {
|
|
7037
7220
|
sendJson(res, 404, {
|
|
7038
7221
|
error: 'tool_not_found',
|
|
@@ -7048,6 +7231,7 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
7048
7231
|
description: tool.description,
|
|
7049
7232
|
annotations: tool.annotations || {},
|
|
7050
7233
|
inputSchema: tool.inputSchema,
|
|
7234
|
+
...(tool.outputSchema ? { outputSchema: tool.outputSchema } : {}),
|
|
7051
7235
|
}, {}, {
|
|
7052
7236
|
headOnly: isHeadRequest,
|
|
7053
7237
|
});
|
|
@@ -8038,29 +8222,25 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
8038
8222
|
return;
|
|
8039
8223
|
}
|
|
8040
8224
|
|
|
8041
|
-
// Public case studies — proof
|
|
8042
|
-
|
|
8043
|
-
|
|
8044
|
-
|
|
8045
|
-
|
|
8046
|
-
|
|
8047
|
-
|
|
8048
|
-
|
|
8049
|
-
|
|
8050
|
-
|
|
8051
|
-
|
|
8052
|
-
|
|
8053
|
-
|
|
8054
|
-
|
|
8055
|
-
|
|
8056
|
-
<p><a href="https://thumbgate.ai">Home</a> · <a href="/pricing">Pricing</a> · <a href="/privacy">Privacy</a> · <a href="/terms">Terms</a> · <a href="/support">Support</a></p>
|
|
8057
|
-
</footer>
|
|
8058
|
-
</body></html>`, {}, {
|
|
8059
|
-
headOnly: isHeadRequest,
|
|
8060
|
-
});
|
|
8225
|
+
// Public case studies — dogfood proof narratives (static HTML; no fabricated logos).
|
|
8226
|
+
if (isGetLikeRequest && (pathname === '/case-studies' || pathname === '/case-studies.html')) {
|
|
8227
|
+
try {
|
|
8228
|
+
servePublicMarketingPage({
|
|
8229
|
+
req,
|
|
8230
|
+
res,
|
|
8231
|
+
parsed,
|
|
8232
|
+
hostedConfig,
|
|
8233
|
+
isHeadRequest,
|
|
8234
|
+
renderHtml: () => fs.readFileSync(CASE_STUDIES_PAGE_PATH, 'utf-8'),
|
|
8235
|
+
extraTelemetry: { pageType: 'case_studies' },
|
|
8236
|
+
});
|
|
8237
|
+
} catch {
|
|
8238
|
+
sendJson(res, 404, { error: 'Case studies page not found' });
|
|
8239
|
+
}
|
|
8061
8240
|
return;
|
|
8062
8241
|
}
|
|
8063
8242
|
|
|
8243
|
+
|
|
8064
8244
|
// Public canonical pricing page. The audit flagged "pricing schizophrenia":
|
|
8065
8245
|
// sales/pricing.json said $49 / $299, COMMERCIAL_TRUTH.md said $19 / $149,
|
|
8066
8246
|
// and there was no buyer-facing surface to reconcile the two. This is now
|
|
@@ -8215,7 +8395,7 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
8215
8395
|
});
|
|
8216
8396
|
return;
|
|
8217
8397
|
}
|
|
8218
|
-
if (result
|
|
8398
|
+
if (result?.retryable === true) {
|
|
8219
8399
|
sendProblem(res, {
|
|
8220
8400
|
type: PROBLEM_TYPES.INTERNAL,
|
|
8221
8401
|
title: 'Fulfillment temporarily unavailable',
|
|
@@ -8403,14 +8583,15 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
8403
8583
|
}
|
|
8404
8584
|
|
|
8405
8585
|
const resolvedTraceId = result.traceId || requestedTraceId;
|
|
8406
|
-
|
|
8407
|
-
|
|
8586
|
+
let nextSteps = {};
|
|
8587
|
+
if (result.apiKey) {
|
|
8588
|
+
nextSteps = {
|
|
8408
8589
|
env: `THUMBGATE_API_KEY=${result.apiKey}\nTHUMBGATE_API_BASE_URL=${hostedConfig.billingApiBaseUrl}`,
|
|
8409
8590
|
curl: `curl -X POST ${hostedConfig.billingApiBaseUrl}/v1/feedback/capture \\\n+ -H 'Authorization: Bearer ${result.apiKey}' \\\n+ -H 'Content-Type: application/json' \\\n+ -d '{"signal":"down","context":"example","whatWentWrong":"example","whatToChange":"example"}'`,
|
|
8410
|
-
}
|
|
8411
|
-
|
|
8412
|
-
|
|
8413
|
-
|
|
8591
|
+
};
|
|
8592
|
+
} else if (result.offerKind === 'workflow_hardening_diagnostic') {
|
|
8593
|
+
nextSteps = { fulfillment: 'Payment confirmed. Diagnostic fulfillment is processing; an email will arrive when it completes.' };
|
|
8594
|
+
}
|
|
8414
8595
|
|
|
8415
8596
|
sendJson(res, 200, {
|
|
8416
8597
|
...result,
|
|
@@ -8442,14 +8623,19 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
8442
8623
|
return;
|
|
8443
8624
|
}
|
|
8444
8625
|
|
|
8445
|
-
// Operator key is allowed to bypass the general admin gate for
|
|
8626
|
+
// Operator key is allowed to bypass the general admin gate for dedicated
|
|
8627
|
+
// read-only operational endpoints.
|
|
8446
8628
|
const _reqToken = extractApiKey(req);
|
|
8447
|
-
const
|
|
8448
|
-
&& _reqToken
|
|
8629
|
+
const isOperatorReadRequest = Boolean(expectedOperatorKey)
|
|
8630
|
+
&& safeKeyEqual(_reqToken, expectedOperatorKey)
|
|
8449
8631
|
&& req.method === 'GET'
|
|
8450
|
-
&& [
|
|
8632
|
+
&& [
|
|
8633
|
+
'/v1/billing/summary',
|
|
8634
|
+
'/v1/intake/workflow-sprint/queue',
|
|
8635
|
+
'/v1/task-outcomes/monitor',
|
|
8636
|
+
].includes(pathname);
|
|
8451
8637
|
|
|
8452
|
-
if (!
|
|
8638
|
+
if (!isOperatorReadRequest && !isAuthorized(req, expectedApiKey)) {
|
|
8453
8639
|
sendProblem(res, {
|
|
8454
8640
|
type: PROBLEM_TYPES.UNAUTHORIZED,
|
|
8455
8641
|
title: 'Unauthorized',
|
|
@@ -8469,7 +8655,7 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
8469
8655
|
if (req.method === 'GET' && pathname === '/v1/feedback/stats') {
|
|
8470
8656
|
const stats = shouldAggregateFeedback()
|
|
8471
8657
|
? computeAggregateFeedbackStats({ feedbackDir: requestFeedbackPaths.FEEDBACK_DIR })
|
|
8472
|
-
: analyzeFeedback(requestFeedbackPaths.FEEDBACK_LOG_PATH);
|
|
8658
|
+
: analyzeFeedback(requestFeedbackPaths.FEEDBACK_LOG_PATH, { humanOnly: true });
|
|
8473
8659
|
try {
|
|
8474
8660
|
const { getStatuslineMeta } = require('../../scripts/statusline-meta');
|
|
8475
8661
|
const meta = getStatuslineMeta({ env: process.env });
|
|
@@ -8989,6 +9175,7 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
8989
9175
|
workflowContract: body.workflowContract,
|
|
8990
9176
|
repoPath: body.repoPath,
|
|
8991
9177
|
localOnly: body.localOnly === true,
|
|
9178
|
+
ttlMs: body.ttlMs,
|
|
8992
9179
|
clear: body.clear === true,
|
|
8993
9180
|
});
|
|
8994
9181
|
sendJson(res, 200, { scope });
|
|
@@ -9037,6 +9224,84 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
9037
9224
|
return;
|
|
9038
9225
|
}
|
|
9039
9226
|
|
|
9227
|
+
if (req.method === 'POST' && pathname === '/v1/task-outcomes') {
|
|
9228
|
+
const body = await parseJsonBody(req);
|
|
9229
|
+
try {
|
|
9230
|
+
const result = recordTaskOutcome(body, { feedbackDir: requestFeedbackDir });
|
|
9231
|
+
sendJson(res, result.recorded ? 201 : 200, result);
|
|
9232
|
+
} catch (error) {
|
|
9233
|
+
throw createHttpError(400, error.message);
|
|
9234
|
+
}
|
|
9235
|
+
return;
|
|
9236
|
+
}
|
|
9237
|
+
|
|
9238
|
+
if (req.method === 'GET' && pathname === '/v1/task-outcomes') {
|
|
9239
|
+
const taskId = parsed.searchParams.get('taskId');
|
|
9240
|
+
const limit = Math.max(1, Math.min(100, Number(parsed.searchParams.get('limit') || 20)));
|
|
9241
|
+
if (taskId) {
|
|
9242
|
+
const outcome = getTaskOutcome(taskId, { feedbackDir: requestFeedbackDir });
|
|
9243
|
+
if (!outcome) throw createHttpError(404, `Task outcome not found: ${taskId}`);
|
|
9244
|
+
sendJson(res, 200, outcome);
|
|
9245
|
+
} else {
|
|
9246
|
+
sendJson(res, 200, {
|
|
9247
|
+
outcomes: readTaskOutcomes({ feedbackDir: requestFeedbackDir }).slice(-limit),
|
|
9248
|
+
});
|
|
9249
|
+
}
|
|
9250
|
+
return;
|
|
9251
|
+
}
|
|
9252
|
+
|
|
9253
|
+
if (req.method === 'GET' && pathname === '/v1/task-outcomes/metrics') {
|
|
9254
|
+
const outcomes = readTaskOutcomes({ feedbackDir: requestFeedbackDir });
|
|
9255
|
+
sendJson(res, 200, calculateTaskOutcomeMetrics(outcomes));
|
|
9256
|
+
return;
|
|
9257
|
+
}
|
|
9258
|
+
|
|
9259
|
+
if (req.method === 'GET' && pathname === '/v1/task-outcomes/monitor') {
|
|
9260
|
+
const outcomes = readTaskOutcomes({ feedbackDir: requestFeedbackDir });
|
|
9261
|
+
const toolKpis = computeToolKpis({ feedbackDir: requestFeedbackDir });
|
|
9262
|
+
sendJson(res, 200, monitorProductionSignals(outcomes, toolKpis));
|
|
9263
|
+
return;
|
|
9264
|
+
}
|
|
9265
|
+
|
|
9266
|
+
if (req.method === 'POST' && pathname === '/v1/escalations') {
|
|
9267
|
+
const body = await parseJsonBody(req);
|
|
9268
|
+
try {
|
|
9269
|
+
const result = requestEscalation(body, { feedbackDir: requestFeedbackDir });
|
|
9270
|
+
sendJson(res, result.recorded ? 201 : 200, result);
|
|
9271
|
+
} catch (error) {
|
|
9272
|
+
throw createHttpError(400, error.message);
|
|
9273
|
+
}
|
|
9274
|
+
return;
|
|
9275
|
+
}
|
|
9276
|
+
|
|
9277
|
+
if (req.method === 'GET' && pathname === '/v1/escalations') {
|
|
9278
|
+
const status = parsed.searchParams.get('status') || undefined;
|
|
9279
|
+
const escalations = listEscalations({ feedbackDir: requestFeedbackDir, status });
|
|
9280
|
+
sendJson(res, 200, {
|
|
9281
|
+
escalations,
|
|
9282
|
+
metrics: calculateEscalationMetrics(escalations),
|
|
9283
|
+
});
|
|
9284
|
+
return;
|
|
9285
|
+
}
|
|
9286
|
+
|
|
9287
|
+
if (req.method === 'POST' && /^\/v1\/escalations\/[^/]+\/decision$/.test(pathname)) {
|
|
9288
|
+
const escalationId = decodeURIComponent(pathname.split('/')[3]);
|
|
9289
|
+
const authenticatedActor = authenticateHumanReviewer(req, humanReviewerConfig);
|
|
9290
|
+
const body = await parseJsonBody(req);
|
|
9291
|
+
try {
|
|
9292
|
+
sendJson(res, 200, decideEscalation({
|
|
9293
|
+
...body,
|
|
9294
|
+
escalationId,
|
|
9295
|
+
}, {
|
|
9296
|
+
authenticatedActor,
|
|
9297
|
+
feedbackDir: requestFeedbackDir,
|
|
9298
|
+
}));
|
|
9299
|
+
} catch (error) {
|
|
9300
|
+
throw createHttpError(400, error.message);
|
|
9301
|
+
}
|
|
9302
|
+
return;
|
|
9303
|
+
}
|
|
9304
|
+
|
|
9040
9305
|
if (req.method === 'GET' && pathname === '/v1/ops/integrity') {
|
|
9041
9306
|
const command = parsed.searchParams.get('command') || undefined;
|
|
9042
9307
|
const baseBranch = parsed.searchParams.get('baseBranch') || undefined;
|
|
@@ -9207,6 +9472,7 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
|
|
|
9207
9472
|
guardrails: body.guardrails,
|
|
9208
9473
|
tags: extractTags(body.tags),
|
|
9209
9474
|
skill: body.skill,
|
|
9475
|
+
reviewOrigin: 'human',
|
|
9210
9476
|
});
|
|
9211
9477
|
const actionIntegration = inferActionIntegration(body, req.headers);
|
|
9212
9478
|
appendBestEffortTelemetry(requestFeedbackDir, {
|