thumbgate 1.35.0 → 1.37.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/.agents/skills/cyberstrike-compare-not-clone/SKILL.md +36 -0
  2. package/.agents/skills/gitlab-sandbox-allowlist-not-trust/SKILL.md +77 -0
  3. package/.agents/skills/jit-harness-compare-not-clone/SKILL.md +34 -0
  4. package/.agents/skills/openui-catalog-compose-honesty/SKILL.md +64 -0
  5. package/.agents/skills/zvec-grep-compare-not-clone/SKILL.md +34 -0
  6. package/.claude-plugin/plugin.json +1 -1
  7. package/.well-known/llms.txt +1 -0
  8. package/.well-known/mcp/server-card.json +1 -1
  9. package/CONTRIBUTING.md +95 -0
  10. package/README.md +195 -632
  11. package/THIRD_PARTY_NOTICES.md +89 -0
  12. package/adapters/claude/.mcp.json +2 -2
  13. package/adapters/forge/forge.yaml +3 -3
  14. package/adapters/future-agi/.mcp.json +8 -0
  15. package/adapters/future-agi/FUTURE_AGI.md +23 -0
  16. package/adapters/future-agi/config.toml +3 -0
  17. package/adapters/future-agi/future-agi-bridge.js +9 -0
  18. package/adapters/future-agi/opencode.json +8 -0
  19. package/adapters/herdr/herdr-plugin.toml +18 -0
  20. package/adapters/mcp/server-stdio.js +238 -25
  21. package/adapters/opencode/opencode.json +1 -1
  22. package/adapters/workos/WORKOS.md +52 -0
  23. package/bin/cli.js +336 -3
  24. package/bin/futureagi-bridge +9 -0
  25. package/config/gate-templates.json +581 -4
  26. package/config/gates/actor-critic-audit.json +34 -0
  27. package/config/gates/default.json +9 -3
  28. package/config/gates/five-walls-governance.json +34 -0
  29. package/config/gates/future-agi-guardrails.json +34 -0
  30. package/config/gates/radware-threat-defense-2026.json +61 -0
  31. package/config/gates/simatree-data-governance.json +33 -0
  32. package/config/mcp-allowlists.json +4 -0
  33. package/config/merge-quality-checks.json +6 -0
  34. package/config/model-candidates.json +312 -29
  35. package/config/model-tiers.json +18 -0
  36. package/config/post-deploy-marketing-pages.json +10 -0
  37. package/config/progressive/01-wire-only.json +11 -0
  38. package/config/progressive/02-dashboard-empty-ok.json +10 -0
  39. package/config/progressive/03-one-lesson.json +10 -0
  40. package/config/progressive/04-warn-fires.json +11 -0
  41. package/config/progressive/05-strict-optional.json +11 -0
  42. package/config/progressive/README.md +15 -0
  43. package/config/schemas/broker-execution-receipt.schema.json +139 -0
  44. package/config/schemas/provider-execution-attestation-v1.schema.json +58 -0
  45. package/conformance/provider-attestation/vectors.json +320 -0
  46. package/docs/specs/provider-execution-attestation-v1.md +69 -0
  47. package/openapi/openapi.yaml +15 -0
  48. package/package.json +390 -148
  49. package/public/about.html +2 -2
  50. package/public/ai-malpractice-prevention.html +7 -7
  51. package/public/blog/a-10-dollar-vps-is-not-a-computer.html +143 -0
  52. package/public/blog/a-receipt-is-not-world-state.html +388 -0
  53. package/public/blog/git-at-agent-scale.html +374 -0
  54. package/public/blog/no-llm-in-the-gate.html +133 -0
  55. package/public/blog.html +80 -0
  56. package/public/case-studies.html +16 -1
  57. package/public/compare.html +28 -0
  58. package/public/diagnostic.html +216 -7
  59. package/public/docs/connectors.html +39 -0
  60. package/public/federal.html +2 -2
  61. package/public/founders.html +639 -0
  62. package/public/index.html +87 -9
  63. package/public/install.html +8 -8
  64. package/public/learn.html +39 -0
  65. package/public/numbers.html +2 -2
  66. package/public/peter.html +310 -0
  67. package/public/platform-partners.html +119 -0
  68. package/public/pricing.html +24 -3
  69. package/public/privacy.html +117 -0
  70. package/public/pro.html +17 -0
  71. package/public/support.html +62 -0
  72. package/public/terms.html +130 -0
  73. package/public/third-party-notices.html +95 -0
  74. package/public/yt.html +351 -0
  75. package/scripts/action-receipts.js +133 -3
  76. package/scripts/adaptive-governance-arena.js +349 -0
  77. package/scripts/admin-override.js +205 -0
  78. package/scripts/agent-action-inventory.js +869 -0
  79. package/scripts/agent-audit-trace.js +42 -2
  80. package/scripts/agent-egress-policy.js +1117 -0
  81. package/scripts/agent-memory-lifecycle.js +141 -2
  82. package/scripts/agent-operations-planner.js +441 -1
  83. package/scripts/agent-readiness.js +68 -0
  84. package/scripts/agent-security-central.js +647 -0
  85. package/scripts/allowlist-bridge-honesty.js +417 -0
  86. package/scripts/async-job-runner.js +102 -11
  87. package/scripts/audit-trail.js +212 -0
  88. package/scripts/billing.js +1 -1
  89. package/scripts/broker-execution-receipts.js +719 -0
  90. package/scripts/budget-aware-gates-proof.js +423 -0
  91. package/scripts/claude-feedback-sync.js +29 -3
  92. package/scripts/claw-harness-production.js +237 -0
  93. package/scripts/cli-schema.js +163 -1
  94. package/scripts/codex-runbook-flywheel.js +318 -0
  95. package/scripts/context-footprint.js +186 -0
  96. package/scripts/contextfs.js +143 -61
  97. package/scripts/dashboard.js +251 -32
  98. package/scripts/deepseek-v4-runtime-guardrails.js +72 -6
  99. package/scripts/docker-sandbox-planner.js +18 -0
  100. package/scripts/double-blind-eval-protocol.js +252 -0
  101. package/scripts/edotenv-rl-gateway.js +259 -0
  102. package/scripts/ensure-production-search-corpus.js +162 -0
  103. package/scripts/eval-holdout.js +311 -0
  104. package/scripts/feedback-aggregate.js +21 -2
  105. package/scripts/feedback-loop.js +87 -5
  106. package/scripts/feedback-quality.js +9 -0
  107. package/scripts/file-ledger-lock.js +4 -1
  108. package/scripts/financial-control-plane.js +41 -1
  109. package/scripts/find-dormant-requires.js +118 -0
  110. package/scripts/fs-utils.js +84 -8
  111. package/scripts/gates-engine.js +810 -63
  112. package/scripts/generate-case-study-outreach.js +24 -15
  113. package/scripts/git-at-scale.js +628 -0
  114. package/scripts/governance-conflict-audit.js +1650 -0
  115. package/scripts/governance-difficulty-curriculum.js +328 -0
  116. package/scripts/graphrag-retrieval.js +275 -0
  117. package/scripts/gurobi-optimizer.js +324 -0
  118. package/scripts/gurobi_optimizer.py +485 -0
  119. package/scripts/harness-selector.js +82 -1
  120. package/scripts/hidden-entry-points.js +284 -0
  121. package/scripts/human-escalation.js +199 -1
  122. package/scripts/hybrid-feedback-context.js +152 -19
  123. package/scripts/intent-governed-execution.js +602 -0
  124. package/scripts/intervention-policy.js +123 -20
  125. package/scripts/jit-harness-compose.js +628 -0
  126. package/scripts/jsonl-watcher.js +10 -0
  127. package/scripts/lesson-embedding-index.js +95 -12
  128. package/scripts/lesson-retrieval.js +105 -19
  129. package/scripts/local-model-profile.js +19 -2
  130. package/scripts/mailer/resend-mailer.js +1 -1
  131. package/scripts/matryoshka-embedding.js +235 -0
  132. package/scripts/mcp-oauth.js +42 -4
  133. package/scripts/mcp-session-handles.js +1016 -0
  134. package/scripts/mcp-wiring-doctor.js +314 -0
  135. package/scripts/memory-firewall.js +115 -2
  136. package/scripts/memory-scope-readiness.js +299 -0
  137. package/scripts/memory-vs-rag-route.js +161 -0
  138. package/scripts/model-tier-router.js +148 -21
  139. package/scripts/nvidia-specdecode-al-doctor.js +536 -0
  140. package/scripts/openui-catalog-compose-honesty.js +593 -0
  141. package/scripts/operational-integrity.js +19 -1
  142. package/scripts/override-audit.js +213 -0
  143. package/scripts/package-manager-honesty-doctor.js +458 -0
  144. package/scripts/pr-manager.js +63 -1
  145. package/scripts/prove-herdr-adapter.js +52 -0
  146. package/scripts/prove-memory-pyramid-and-symbolic-canvas.js +95 -0
  147. package/scripts/prove-workos.js +73 -0
  148. package/scripts/provider-attestation-conformance.js +192 -0
  149. package/scripts/provider-receipt-contract.js +136 -0
  150. package/scripts/qwen38-max-cost-optimizer.js +401 -0
  151. package/scripts/radware-threat-defense.js +280 -0
  152. package/scripts/rag-embedding-identity.js +221 -0
  153. package/scripts/rag-precision-guardrails.js +112 -2
  154. package/scripts/remote-feedback-capture.js +159 -0
  155. package/scripts/research-agent-harness.js +256 -0
  156. package/scripts/rsi-safety-hillclimb.js +200 -0
  157. package/scripts/rule-sprawl.js +188 -0
  158. package/scripts/schedule-manager.js +147 -0
  159. package/scripts/self-heal.js +8 -0
  160. package/scripts/session-lease.js +415 -0
  161. package/scripts/simatree-data-governance.js +347 -0
  162. package/scripts/slo-alert-engine.js +172 -7
  163. package/scripts/solver-parity.js +539 -0
  164. package/scripts/stealth-memory-injection-gate.js +333 -0
  165. package/scripts/switchyard-router.js +366 -0
  166. package/scripts/telemetry-analytics.js +84 -27
  167. package/scripts/temporal-decay-weighting.js +138 -0
  168. package/scripts/test-all.js +165 -0
  169. package/scripts/token-savings.js +42 -0
  170. package/scripts/tool-kpi-tracker.js +108 -5
  171. package/scripts/tool-registry.js +193 -5
  172. package/scripts/universal-claim-evaluator.js +14 -2
  173. package/scripts/vector-store.js +279 -9
  174. package/scripts/workflow-notebook.js +391 -0
  175. package/scripts/workflow-sentinel.js +111 -12
  176. package/scripts/workos-production-guard.js +260 -0
  177. package/scripts/workspace-search-route.js +515 -0
  178. package/server.json +2 -2
  179. package/src/agent-identity-boundary.js +76 -0
  180. package/src/agent-retrieval-cache.js +155 -0
  181. package/src/alert-noise-ledger.js +502 -0
  182. package/src/api/server.js +724 -153
  183. package/src/git-fast-cache.js +220 -0
  184. package/src/git-wal-sync.js +156 -0
  185. package/src/hash-anchored-edit.js +82 -0
  186. package/src/hermes-platform-protocol.js +475 -0
  187. package/src/hermes-sync-plane.js +241 -0
  188. package/src/index.js +30 -1
  189. package/src/iso42001-compliance-guard.js +97 -0
  190. package/src/latency-budget.js +244 -0
  191. package/src/mcp-writeguard.js +316 -0
  192. package/src/miminions-adapter.js +106 -0
  193. package/src/pipeline-compass.js +104 -0
  194. package/src/ppl-alert-pipeline.js +284 -0
  195. package/src/rendezvous-router.js +90 -0
  196. package/src/security-questionnaire.js +195 -0
package/src/api/server.js CHANGED
@@ -16,6 +16,10 @@ const {
16
16
  buildIntakeDiscoveryPacket,
17
17
  buildIntakeQualificationCard,
18
18
  } = require('../../scripts/revenue-offer-system');
19
+ const {
20
+ getSecurityQuestionnaire,
21
+ renderSecurityOverviewHtml,
22
+ } = require('../security-questionnaire');
19
23
 
20
24
  const POSTHOG_API_PATHS = new Set(['/capture', '/batch', '/decide', '/e', '/engage']);
21
25
  const POSTHOG_INGEST_HOST = 'us.i.posthog.com';
@@ -139,6 +143,10 @@ const {
139
143
  getBillingSummaryLive,
140
144
  getBillingSummariesLive,
141
145
  } = require('../../scripts/billing');
146
+ const {
147
+ ensureProductionSearchCorpus,
148
+ shouldEnsureProductionSearchCorpus,
149
+ } = require('../../scripts/ensure-production-search-corpus');
142
150
  const {
143
151
  DEFAULT_PUBLIC_APP_ORIGIN,
144
152
  resolveHostedBillingConfig,
@@ -188,6 +196,49 @@ const {
188
196
  recordDecisionOutcome,
189
197
  computeDecisionMetrics,
190
198
  } = require('../../scripts/decision-journal');
199
+ const {
200
+ HermesPlatformProtocol,
201
+ } = require('../hermes-platform-protocol');
202
+ const {
203
+ HermesSyncPlane,
204
+ } = require('../hermes-sync-plane');
205
+
206
+ // Partition Hermes protocol/sync state by hosted tenant so one customer's
207
+ // connection/thread/approval maps cannot be read or mutated by another.
208
+ const hostedHermesByTenant = new Map();
209
+
210
+ function resolveHermesTenantKey(requestDataIdentity) {
211
+ return requestDataIdentity?.partition || '__default__';
212
+ }
213
+
214
+ function getHostedHermes(requestDataIdentity) {
215
+ const key = resolveHermesTenantKey(requestDataIdentity);
216
+ let entry = hostedHermesByTenant.get(key);
217
+ if (!entry) {
218
+ entry = {
219
+ protocol: new HermesPlatformProtocol(),
220
+ syncPlane: new HermesSyncPlane(),
221
+ };
222
+ hostedHermesByTenant.set(key, entry);
223
+ }
224
+ return entry;
225
+ }
226
+
227
+ function resolveHermesApproverId(req, humanReviewerConfig, requestDataIdentity) {
228
+ if (humanReviewerConfig?.key && humanReviewerConfig?.id) {
229
+ const header = req.headers['x-thumbgate-human-reviewer-key'];
230
+ const presented = Array.isArray(header) ? header[0] : header;
231
+ if (safeKeyEqual(presented, humanReviewerConfig.key)) {
232
+ return humanReviewerConfig.id;
233
+ }
234
+ }
235
+ if (requestDataIdentity?.accessContext?.principalId) {
236
+ return requestDataIdentity.accessContext.principalId;
237
+ }
238
+ const role = resolveKeyRole(extractApiKey(req));
239
+ if (role) return `thumbgate:${role}`;
240
+ return null;
241
+ }
191
242
  const {
192
243
  generateDashboard,
193
244
  buildReviewSnapshot,
@@ -218,6 +269,7 @@ const {
218
269
  } = require('../../scripts/thumbgate-search');
219
270
  const {
220
271
  appendTelemetryPing,
272
+ headerHasPrivacyOptOut,
221
273
  } = require('../../scripts/telemetry-analytics');
222
274
  const {
223
275
  buildProductIssueTitle,
@@ -278,10 +330,13 @@ const EVAL_SCORECARD_PAGE_PATH = path.resolve(__dirname, '../../public/eval-scor
278
330
  const EVALUATIONS_PAGE_PATH = path.resolve(__dirname, '../../public/evaluations.html');
279
331
  const CASE_STUDIES_PAGE_PATH = path.resolve(__dirname, '../../public/case-studies.html');
280
332
  const FEDERAL_PAGE_PATH = path.resolve(__dirname, '../../public/federal.html');
333
+ const YT_PAGE_PATH = path.resolve(__dirname, '../../public/yt.html');
281
334
  const PRICING_PAGE_PATH = path.resolve(__dirname, '../../public/pricing.html');
282
335
  const ABOUT_PAGE_PATH = path.resolve(__dirname, '../../public/about.html');
283
336
  const DIAGNOSTIC_PAGE_PATH = path.resolve(__dirname, '../../public/diagnostic.html');
337
+ const FOUNDERS_PAGE_PATH = path.resolve(__dirname, '../../public/founders.html');
284
338
  const PARTNER_INTAKE_PAGE_PATH = path.resolve(__dirname, '../../public/partner-intake.html');
339
+ const PETER_PAGE_PATH = path.resolve(__dirname, '../../public/peter.html');
285
340
  const INSTALL_PAGE_PATH = path.resolve(__dirname, '../../public/install.html');
286
341
  const LEARN_DIR = path.resolve(__dirname, '../../public/learn');
287
342
  const GUIDES_DIR = path.resolve(__dirname, '../../public/guides');
@@ -296,6 +351,7 @@ const COMPARE_PAGE_PATHS_BY_SLUG = buildPublicHtmlFileMap(COMPARE_DIR);
296
351
  const USE_CASE_PAGE_PATHS_BY_SLUG = buildPublicHtmlFileMap(USE_CASES_DIR);
297
352
  const BLOG_PAGE_PATHS_BY_SLUG = buildPublicHtmlFileMap(BLOG_DIR);
298
353
  const BUYER_INTENT_SCRIPT_PATH = path.resolve(__dirname, '../../public/js/buyer-intent.js');
354
+ const WEBMCP_SCRIPT_PATH = path.resolve(__dirname, '../../public/js/webmcp.js');
299
355
  const STATIC_MIME_BY_EXT = Object.freeze({
300
356
  '.png': 'image/png',
301
357
  '.jpg': 'image/jpeg',
@@ -644,6 +700,30 @@ const TRACKED_LINK_TARGETS = Object.freeze({
644
700
  utm_campaign: 'github_repo',
645
701
  },
646
702
  },
703
+ npm: {
704
+ href: 'https://www.npmjs.com/package/thumbgate',
705
+ external: true,
706
+ ctaId: 'go_npm',
707
+ ctaPlacement: 'link_router',
708
+ eventType: 'cta_click',
709
+ defaults: {
710
+ utm_source: 'website',
711
+ utm_medium: 'link_router',
712
+ utm_campaign: 'npm_package',
713
+ },
714
+ },
715
+ marketplace: {
716
+ href: 'https://github.com/marketplace/actions/thumbgate-agent-governance',
717
+ external: true,
718
+ ctaId: 'go_marketplace',
719
+ ctaPlacement: 'link_router',
720
+ eventType: 'cta_click',
721
+ defaults: {
722
+ utm_source: 'website',
723
+ utm_medium: 'link_router',
724
+ utm_campaign: 'github_marketplace_action',
725
+ },
726
+ },
647
727
  });
648
728
 
649
729
  // ---------------------------------------------------------------------------
@@ -1837,7 +1917,7 @@ function sendInvalidAnalyticsWindowProblem(res, title, err) {
1837
1917
  type: PROBLEM_TYPES.INVALID_REQUEST,
1838
1918
  title,
1839
1919
  status: 400,
1840
- detail: err && err.message ? err.message : 'Invalid analytics window request.',
1920
+ detail: err?.message || 'Invalid analytics window request.',
1841
1921
  });
1842
1922
  }
1843
1923
 
@@ -1864,19 +1944,103 @@ function resolveBillingSummaryOptionsOrRespondProblem(res, parsed, invalidTitle)
1864
1944
 
1865
1945
  async function buildLiveDashboardData(parsed, feedbackDir) {
1866
1946
  const summaryOptions = resolveBillingSummaryOptions(parsed);
1867
- const billingSummary = await getBillingSummaryLive(summaryOptions);
1947
+ // Stripe/live billing can hang longer than the authenticated deploy proof
1948
+ // timeout (12s). Prefer live, but fail closed to local summary so /v1/dashboard
1949
+ // remains usable for operational proof.
1950
+ const liveTimeoutMs = Number(process.env.THUMBGATE_DASHBOARD_BILLING_TIMEOUT_MS || 4000);
1951
+ let billingSummary;
1952
+ let billingSource = 'live';
1953
+ try {
1954
+ billingSummary = await Promise.race([
1955
+ getBillingSummaryLive(summaryOptions),
1956
+ new Promise((_, reject) => {
1957
+ const timer = setTimeout(() => reject(new Error('billing_summary_timeout')), liveTimeoutMs);
1958
+ if (typeof timer.unref === 'function') timer.unref();
1959
+ }),
1960
+ ]);
1961
+ } catch {
1962
+ billingSummary = getBillingSummary(summaryOptions);
1963
+ billingSource = 'local';
1964
+ }
1868
1965
  const data = generateDashboard(feedbackDir, {
1869
1966
  analyticsWindow: summaryOptions,
1870
1967
  billingSummary,
1871
- billingSource: 'live',
1968
+ billingSource,
1872
1969
  authContext: { tier: 'pro' },
1873
1970
  });
1874
1971
  return { summaryOptions, data };
1875
1972
  }
1876
1973
 
1974
+ const DEFAULT_DASHBOARD_CACHE_TTL_MS = 30_000;
1975
+ const DEFAULT_DASHBOARD_CACHE_MAX_ENTRIES = 32;
1976
+ const liveDashboardCache = new Map();
1977
+
1978
+ function parseNonNegativeInteger(value, fallback) {
1979
+ const parsed = Number(value);
1980
+ return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
1981
+ }
1982
+
1983
+ function buildLiveDashboardCacheKey(parsed, feedbackDir) {
1984
+ const query = new URLSearchParams(parsed.searchParams);
1985
+ query.sort();
1986
+ return `${path.resolve(feedbackDir)}?${query.toString()}`;
1987
+ }
1988
+
1989
+ async function loadCachedLiveDashboardData(parsed, feedbackDir, options = {}) {
1990
+ const now = options.now || Date.now;
1991
+ const build = options.build || buildLiveDashboardData;
1992
+ const cache = options.cache || liveDashboardCache;
1993
+ const ttlMs = parseNonNegativeInteger(
1994
+ options.ttlMs ?? process.env.THUMBGATE_DASHBOARD_CACHE_TTL_MS,
1995
+ DEFAULT_DASHBOARD_CACHE_TTL_MS,
1996
+ );
1997
+ const maxEntries = parseNonNegativeInteger(
1998
+ options.maxEntries ?? process.env.THUMBGATE_DASHBOARD_CACHE_MAX_ENTRIES,
1999
+ DEFAULT_DASHBOARD_CACHE_MAX_ENTRIES,
2000
+ );
2001
+ const key = buildLiveDashboardCacheKey(parsed, feedbackDir);
2002
+ const currentTime = now();
2003
+ const cached = cache.get(key);
2004
+ if (cached && (cached.promise || cached.expiresAt > currentTime)) {
2005
+ return cached.promise || cached.value;
2006
+ }
2007
+
2008
+ if (maxEntries === 0) {
2009
+ return build(parsed, feedbackDir);
2010
+ }
2011
+
2012
+ if (cached) cache.delete(key);
2013
+ for (const [cacheKey, entry] of cache) {
2014
+ if (!entry.promise && entry.expiresAt <= currentTime) cache.delete(cacheKey);
2015
+ }
2016
+ while (cache.size >= maxEntries) {
2017
+ const evictableKey = [...cache].find(([, entry]) => !entry.promise)?.[0];
2018
+ if (!evictableKey) {
2019
+ const error = new Error('Live dashboard cache is at concurrent build capacity.');
2020
+ error.code = 'DASHBOARD_BUILD_CAPACITY';
2021
+ throw error;
2022
+ }
2023
+ cache.delete(evictableKey);
2024
+ }
2025
+
2026
+ const promise = Promise.resolve().then(() => build(parsed, feedbackDir));
2027
+ cache.set(key, { promise, expiresAt: 0, value: null });
2028
+ try {
2029
+ const value = await promise;
2030
+ cache.set(key, { promise: null, expiresAt: now() + ttlMs, value });
2031
+ while (cache.size > maxEntries && cache.size > 0) {
2032
+ cache.delete(cache.keys().next().value);
2033
+ }
2034
+ return value;
2035
+ } catch (error) {
2036
+ cache.delete(key);
2037
+ throw error;
2038
+ }
2039
+ }
2040
+
1877
2041
  async function loadLiveDashboardDataOrRespondProblem(res, parsed, feedbackDir, invalidTitle) {
1878
2042
  try {
1879
- return await buildLiveDashboardData(parsed, feedbackDir);
2043
+ return await loadCachedLiveDashboardData(parsed, feedbackDir);
1880
2044
  } catch (err) {
1881
2045
  sendInvalidAnalyticsWindowProblem(res, invalidTitle, err);
1882
2046
  return null;
@@ -1993,14 +2157,22 @@ function readRecentFeedbackEntries(feedbackDir, signal, windowMs, limit = 5, opt
1993
2157
  try {
1994
2158
  if (!feedbackDir) return [];
1995
2159
  const rows = shouldAggregateFeedback()
1996
- ? collectAggregateLogEntries('feedback-log.jsonl', { feedbackDir }).entries
2160
+ ? collectAggregateLogEntries('feedback-log.jsonl', {
2161
+ feedbackDir,
2162
+ maxLines: 20_000,
2163
+ maxBytes: 4 * 1024 * 1024,
2164
+ }).entries
1997
2165
  : (() => {
1998
2166
  const fsLocal = require('node:fs');
1999
2167
  const pathLocal = require('node:path');
2000
2168
  const logPath = pathLocal.join(feedbackDir, 'feedback-log.jsonl');
2001
2169
  if (!fsLocal.existsSync(logPath)) return [];
2002
2170
  const { readJsonl } = require('../../scripts/fs-utils');
2003
- return readJsonl(logPath) || [];
2171
+ return readJsonl(logPath, {
2172
+ maxLines: 20_000,
2173
+ maxBytes: 4 * 1024 * 1024,
2174
+ tail: true,
2175
+ }) || [];
2004
2176
  })();
2005
2177
  const cutoff = windowMs ? Date.now() - windowMs : 0;
2006
2178
  const filtered = rows
@@ -2799,7 +2971,7 @@ function applyTrackedLinkDefaults(destinationUrl, target) {
2799
2971
  function applyTrackedLinkQueryKeys(destinationUrl, params) {
2800
2972
  for (const key of TRACKED_LINK_QUERY_KEYS) {
2801
2973
  const value = params.get(key);
2802
- if (!value || !value.trim()) continue;
2974
+ if (!value?.trim()) continue;
2803
2975
  const normalizedValue = key === 'utm_campaign'
2804
2976
  ? normalizeCampaignId(value)
2805
2977
  : value.trim();
@@ -3100,8 +3272,8 @@ function sendPublicBillingPreflight(res) {
3100
3272
 
3101
3273
  function appendBestEffortTelemetry(feedbackDir, payload, headers, context) {
3102
3274
  try {
3103
- appendTelemetryPing(feedbackDir, payload, headers);
3104
- return true;
3275
+ const written = appendTelemetryPing(feedbackDir, payload, headers);
3276
+ return Boolean(written);
3105
3277
  } catch (err) {
3106
3278
  try {
3107
3279
  appendDiagnosticRecord({
@@ -3288,7 +3460,8 @@ function loadPublicMarketingTemplateHtml(templatePath, runtimeConfig, pageContex
3288
3460
  const googleSiteVerificationMeta = runtimeConfig.googleSiteVerification
3289
3461
  ? ` <meta name="google-site-verification" content="${escapeHtmlAttribute(runtimeConfig.googleSiteVerification)}" />`
3290
3462
  : '';
3291
- const gaBootstrap = runtimeConfig.gaMeasurementId
3463
+ const privacyOptOut = Boolean(pageContext.privacyOptOut);
3464
+ const gaBootstrap = (!privacyOptOut && runtimeConfig.gaMeasurementId)
3292
3465
  ? [
3293
3466
  ` <script async src="https://www.googletagmanager.com/gtag/js?id=${runtimeConfig.gaMeasurementId}"></script>`,
3294
3467
  ' <script>',
@@ -3299,7 +3472,7 @@ function loadPublicMarketingTemplateHtml(templatePath, runtimeConfig, pageContex
3299
3472
  ' </script>',
3300
3473
  ].join('\n')
3301
3474
  : '';
3302
- return normalizePublicMarketingHtml(fillTemplate(template, {
3475
+ let rendered = fillTemplate(template, {
3303
3476
  '__PACKAGE_VERSION__': pkg.version,
3304
3477
  '__APP_ORIGIN__': runtimeConfig.appOrigin,
3305
3478
  '__CHECKOUT_ENDPOINT__': runtimeConfig.checkoutEndpoint,
@@ -3328,7 +3501,34 @@ function loadPublicMarketingTemplateHtml(templatePath, runtimeConfig, pageContex
3328
3501
  '__GTM_PLAN_URL__': 'https://github.com/IgorGanapolsky/ThumbGate/blob/main/docs/GO_TO_MARKET_REVENUE_WEDGE_2026-03.md',
3329
3502
  '__GITHUB_URL__': 'https://github.com/IgorGanapolsky/ThumbGate',
3330
3503
  '__POSTHOG_API_KEY__': runtimeConfig.posthogApiKey || '',
3331
- }), runtimeConfig, pageContext.requestHost);
3504
+ });
3505
+ const normalized = normalizePublicMarketingHtml(rendered, runtimeConfig, pageContext.requestHost);
3506
+ if (!privacyOptOut) return normalized;
3507
+ let rewrittenDomain = '';
3508
+ try {
3509
+ rewrittenDomain = normalizePublicRequestHost(pageContext.requestHost)
3510
+ || new URL(runtimeConfig.appOrigin).host;
3511
+ } catch {
3512
+ rewrittenDomain = '';
3513
+ }
3514
+ return removeKnownPlausibleScriptTags(normalized, rewrittenDomain);
3515
+ }
3516
+
3517
+ function removeKnownPlausibleScriptTags(html, extraDomain) {
3518
+ const domains = ['thumbgate.ai', 'thumbgate-production.up.railway.app'];
3519
+ if (extraDomain) domains.push(extraDomain);
3520
+ const sources = [
3521
+ 'https://plausible.io/js/script.tagged-events.js',
3522
+ 'https://plausible.io/js/script.js',
3523
+ ];
3524
+ let out = html;
3525
+ for (const domain of domains) {
3526
+ for (const src of sources) {
3527
+ const tag = `<script defer data-domain="${domain}" src="${src}"></script>`;
3528
+ if (out.includes(tag)) out = out.split(tag).join('');
3529
+ }
3530
+ }
3531
+ return out;
3332
3532
  }
3333
3533
 
3334
3534
  function loadLandingPageHtml(runtimeConfig, pageContext = {}) {
@@ -3343,10 +3543,18 @@ function loadDiagnosticPageHtml(runtimeConfig, pageContext = {}) {
3343
3543
  return loadPublicMarketingTemplateHtml(DIAGNOSTIC_PAGE_PATH, runtimeConfig, pageContext);
3344
3544
  }
3345
3545
 
3546
+ function loadFoundersPageHtml(runtimeConfig, pageContext = {}) {
3547
+ return loadPublicMarketingTemplateHtml(FOUNDERS_PAGE_PATH, runtimeConfig, pageContext);
3548
+ }
3549
+
3346
3550
  function loadPartnerIntakePageHtml(runtimeConfig, pageContext = {}) {
3347
3551
  return loadPublicMarketingTemplateHtml(PARTNER_INTAKE_PAGE_PATH, runtimeConfig, pageContext);
3348
3552
  }
3349
3553
 
3554
+ function loadPeterPageHtml(runtimeConfig, pageContext = {}) {
3555
+ return loadPublicMarketingTemplateHtml(PETER_PAGE_PATH, runtimeConfig, pageContext);
3556
+ }
3557
+
3350
3558
  function loadInstallPageHtml(runtimeConfig, pageContext = {}) {
3351
3559
  return loadPublicMarketingTemplateHtml(INSTALL_PAGE_PATH, runtimeConfig, pageContext);
3352
3560
  }
@@ -3985,8 +4193,20 @@ function renderSitemapXml(runtimeConfig) {
3985
4193
  { path: '/', changefreq: 'weekly', priority: '1.0' },
3986
4194
  { path: '/pro', changefreq: 'weekly', priority: '0.9' },
3987
4195
  { path: '/diagnostic', changefreq: 'weekly', priority: '0.9' },
4196
+ { path: '/founders', changefreq: 'weekly', priority: '0.95' },
4197
+ { path: '/peter', changefreq: 'weekly', priority: '0.9' },
3988
4198
  { path: '/workflow-hardening-sprint', changefreq: 'weekly', priority: '0.9' },
3989
4199
  { path: '/install', changefreq: 'weekly', priority: '0.9' },
4200
+ { path: '/terms', changefreq: 'monthly', priority: '0.7' },
4201
+ { path: '/privacy', changefreq: 'monthly', priority: '0.7' },
4202
+ { path: '/support', changefreq: 'monthly', priority: '0.7' },
4203
+ { path: '/legal', changefreq: 'monthly', priority: '0.7' },
4204
+ { path: '/security', changefreq: 'monthly', priority: '0.65' },
4205
+ { path: '/trust', changefreq: 'monthly', priority: '0.65' },
4206
+ { path: '/security.json', changefreq: 'monthly', priority: '0.55' },
4207
+ { path: '/legal/data-flow', changefreq: 'monthly', priority: '0.7' },
4208
+ { path: '/legal/licensing', changefreq: 'monthly', priority: '0.65' },
4209
+ { path: '/legal/msa-sow', changefreq: 'monthly', priority: '0.6' },
3990
4210
  { path: '/agent-manager', changefreq: 'weekly', priority: '0.9' },
3991
4211
  { path: '/llm-context.md', changefreq: 'weekly', priority: '0.8' },
3992
4212
  { path: '/chatgpt-app', changefreq: 'weekly', priority: '0.85' },
@@ -4000,6 +4220,7 @@ function renderSitemapXml(runtimeConfig) {
4000
4220
  { path: '/evaluations', changefreq: 'weekly', priority: '0.85' },
4001
4221
  { path: '/case-studies', changefreq: 'weekly', priority: '0.9' },
4002
4222
  { path: '/numbers', changefreq: 'weekly', priority: '0.8' },
4223
+ { path: '/yt', changefreq: 'weekly', priority: '0.8' },
4003
4224
  { path: '/learn/background-agent-control-layer', changefreq: 'weekly', priority: '0.85' },
4004
4225
  { path: '/learn/ac-dc-runtime-enforcement', changefreq: 'weekly', priority: '0.85' },
4005
4226
  { path: '/learn/feedback-loop-vs-decision-layer', changefreq: 'weekly', priority: '0.9' },
@@ -4009,10 +4230,14 @@ function renderSitemapXml(runtimeConfig) {
4009
4230
  { path: '/learn/agentic-os-team-governance', changefreq: 'weekly', priority: '0.85' },
4010
4231
  { path: '/learn/cost-aware-agent-gate-routing', changefreq: 'weekly', priority: '0.85' },
4011
4232
  { path: '/learn/pretix-stripe-connect-marketplaces', changefreq: 'weekly', priority: '0.9' },
4233
+ { path: '/learn/netflix-oci-actor-critic-process-audits', changefreq: 'weekly', priority: '0.85' },
4234
+ { path: '/learn/future-agi-self-healing-agent-firewall', changefreq: 'weekly', priority: '0.85' },
4235
+ { path: '/learn/five-walls-enterprise-agent-deployment', changefreq: 'weekly', priority: '0.85' },
4012
4236
  { path: '/compare/claude-code-hooks', changefreq: 'weekly', priority: '0.85' },
4013
4237
  { path: '/compare/bumblebee', changefreq: 'weekly', priority: '0.85' },
4014
4238
  { path: '/compare/anthropic-containment', changefreq: 'weekly', priority: '0.85' },
4015
4239
  { path: '/compare/oak-and-sparrow-gatekeeper', changefreq: 'weekly', priority: '0.85' },
4240
+ { path: '/compare/cloudflare-writeguard', changefreq: 'weekly', priority: '0.85' },
4016
4241
  { path: '/compare/arcjet', changefreq: 'weekly', priority: '0.85' },
4017
4242
  { path: '/compare/anthropic-claude-for-legal', changefreq: 'weekly', priority: '0.9' },
4018
4243
  ...THUMBGATE_SEO_SITEMAP_ENTRIES,
@@ -4181,6 +4406,7 @@ function servePublicMarketingPage({
4181
4406
  serverUtmCampaign: landingAttribution.utmCampaign,
4182
4407
  serverUtmContent: landingAttribution.utmContent,
4183
4408
  requestHost,
4409
+ privacyOptOut: headerHasPrivacyOptOut(req.headers),
4184
4410
  });
4185
4411
 
4186
4412
  sendHtml(
@@ -5171,6 +5397,7 @@ function authenticateHumanReviewer(req, reviewerConfig) {
5171
5397
  return {
5172
5398
  id: reviewerConfig.id,
5173
5399
  kind: 'human',
5400
+ role: 'admin',
5174
5401
  };
5175
5402
  }
5176
5403
 
@@ -5469,13 +5696,22 @@ function createApiServer() {
5469
5696
  ? mcpOauth.tokenAudienceValid(oauthSession, resourceUrl)
5470
5697
  : rawKeyValid;
5471
5698
  if (!authed) {
5699
+ const deniedName = msg.params?.name || 'unknown';
5700
+ const deniedTool = MCP_TOOLS.find((t) => t.name === deniedName);
5472
5701
  recordToolCall({
5473
- toolName: msg.params?.name || 'unknown',
5702
+ toolName: deniedName,
5474
5703
  serverName: 'mcp-http',
5475
5704
  latencyMs: 0,
5476
5705
  success: false,
5706
+ outcome: 'blocked',
5707
+ blocked: true,
5477
5708
  agentId: 'unauthenticated',
5478
- metadata: { denied: true, deniedReason: 'authentication_required' },
5709
+ writeRiskTier: deniedTool?.annotations?.writeRiskTier || null,
5710
+ metadata: {
5711
+ denied: true,
5712
+ deniedReason: 'authentication_required',
5713
+ writeRiskTier: deniedTool?.annotations?.writeRiskTier || null,
5714
+ },
5479
5715
  });
5480
5716
  res.writeHead(401, {
5481
5717
  'Content-Type': 'application/json',
@@ -5504,8 +5740,15 @@ function createApiServer() {
5504
5740
  serverName: 'mcp-http',
5505
5741
  latencyMs: 0,
5506
5742
  success: false,
5743
+ outcome: 'blocked',
5744
+ blocked: true,
5507
5745
  agentId: 'reviewer',
5508
- metadata: { denied: true, deniedReason: 'reviewer_read_only' },
5746
+ writeRiskTier: tool?.annotations?.writeRiskTier || null,
5747
+ metadata: {
5748
+ denied: true,
5749
+ deniedReason: 'reviewer_read_only',
5750
+ writeRiskTier: tool?.annotations?.writeRiskTier || null,
5751
+ },
5509
5752
  });
5510
5753
  sendJson(res, 200, {
5511
5754
  jsonrpc: '2.0', id: msg.id,
@@ -5517,18 +5760,25 @@ function createApiServer() {
5517
5760
  if (oauthSession) {
5518
5761
  const name = msg.params && msg.params.name;
5519
5762
  const tool = MCP_TOOLS.find((candidate) => candidate.name === name);
5520
- const requiredScope = tool?.annotations?.readOnlyHint === true ? 'mcp:read' : 'mcp:write';
5763
+ // Hierarchy: mcp:write implies read/gates/feedback (WorkOS MCP Auth style).
5764
+ const requiredScope = mcpOauth.requiredScopeForTool(tool || {});
5521
5765
  if (!mcpOauth.scopeAllows(oauthSession, requiredScope)) {
5522
5766
  recordToolCall({
5523
5767
  toolName: name || 'unknown',
5524
5768
  serverName: 'mcp-http',
5525
5769
  latencyMs: 0,
5526
5770
  success: false,
5771
+ outcome: 'blocked',
5772
+ blocked: true,
5527
5773
  agentId: oauthSession.clientId || 'oauth-client',
5774
+ clientId: oauthSession.clientId || null,
5775
+ sessionId: oauthSession.sessionId || oauthSession.id || null,
5776
+ writeRiskTier: tool?.annotations?.writeRiskTier || null,
5528
5777
  metadata: {
5529
5778
  denied: true,
5530
5779
  deniedReason: 'insufficient_scope',
5531
5780
  requiredScope,
5781
+ writeRiskTier: tool?.annotations?.writeRiskTier || null,
5532
5782
  },
5533
5783
  });
5534
5784
  sendJson(res, 200, {
@@ -5543,7 +5793,14 @@ function createApiServer() {
5543
5793
  const { callTool } = require('../../adapters/mcp/server-stdio');
5544
5794
  const name = msg.params?.name;
5545
5795
  const args = (msg.params && msg.params.arguments) || {};
5546
- const result = await callTool(name, args);
5796
+ // Attribution rides as callTool options (not tool args) so KPI
5797
+ // can bind human OAuth client/session without polluting schemas.
5798
+ const result = await callTool(name, args, {
5799
+ clientId: oauthSession?.clientId || null,
5800
+ sessionId: oauthSession?.sessionId || oauthSession?.id || null,
5801
+ agentId: oauthSession?.clientId || (isReviewer ? 'reviewer' : 'mcp-http'),
5802
+ serverName: 'mcp-http',
5803
+ });
5547
5804
  sendJson(res, 200, {
5548
5805
  jsonrpc: '2.0', id: msg.id,
5549
5806
  result,
@@ -5626,6 +5883,27 @@ function createApiServer() {
5626
5883
  return;
5627
5884
  }
5628
5885
 
5886
+ // WebMCP read-only agent tools for the product pages. Absent from the
5887
+ // packaged npm runtime by design (tarball exclusion) — the route then
5888
+ // 404s and the deferred script tag degrades to a no-op.
5889
+ if (isGetLikeRequest && pathname === '/js/webmcp.js') {
5890
+ try {
5891
+ const script = fs.readFileSync(WEBMCP_SCRIPT_PATH, 'utf-8');
5892
+ res.writeHead(200, {
5893
+ 'Content-Type': 'application/javascript; charset=utf-8',
5894
+ 'Cache-Control': 'public, max-age=86400',
5895
+ });
5896
+ if (!isHeadRequest) {
5897
+ res.end(script);
5898
+ } else {
5899
+ res.end();
5900
+ }
5901
+ } catch {
5902
+ sendJson(res, 404, { error: 'WebMCP script not found' });
5903
+ }
5904
+ return;
5905
+ }
5906
+
5629
5907
 
5630
5908
  // User feedback → GitHub Issues
5631
5909
  if (req.method === 'POST' && pathname === '/api/feedback/submit') {
@@ -6393,6 +6671,57 @@ async function addContext(){
6393
6671
  return;
6394
6672
  }
6395
6673
 
6674
+
6675
+ // Partner co-branded case study (Peter Yang / high-ROI acquisition landing).
6676
+ if (isGetLikeRequest && (pathname === '/peter' || pathname === '/peter.html')) {
6677
+ try {
6678
+ servePublicMarketingPage({
6679
+ req,
6680
+ res,
6681
+ parsed,
6682
+ hostedConfig,
6683
+ isHeadRequest,
6684
+ renderHtml: loadPeterPageHtml,
6685
+ extraTelemetry: {
6686
+ pageType: 'partner_case_study',
6687
+ partner: 'peter-yang',
6688
+ },
6689
+ });
6690
+ } catch (err) {
6691
+ sendText(res, 500, err.message || 'Partner case study page unavailable');
6692
+ }
6693
+ return;
6694
+ }
6695
+
6696
+ // Oceans-style founder conversion landing (high-ROI cash path).
6697
+ // Mirrors personal operator pages (/peter): pain triad, timed process,
6698
+ // comparison table, refund fence, sticky diagnostic checkout.
6699
+ if (isGetLikeRequest && (
6700
+ pathname === '/founders'
6701
+ || pathname === '/founders.html'
6702
+ || pathname === '/operators'
6703
+ || pathname === '/operators.html'
6704
+ || pathname === '/agent-operators'
6705
+ || pathname === '/agent-operators.html'
6706
+ )) {
6707
+ try {
6708
+ servePublicMarketingPage({
6709
+ req,
6710
+ res,
6711
+ parsed,
6712
+ hostedConfig,
6713
+ isHeadRequest,
6714
+ renderHtml: loadFoundersPageHtml,
6715
+ extraTelemetry: {
6716
+ pageType: 'founders',
6717
+ },
6718
+ });
6719
+ } catch (err) {
6720
+ sendText(res, 500, err.message || 'Founders page unavailable');
6721
+ }
6722
+ return;
6723
+ }
6724
+
6396
6725
  // Natural marketing URLs for the Workflow Hardening Diagnostic/Sprint.
6397
6726
  // High-intent outreach refers to "the diagnostic", "the sprint", or
6398
6727
  // "workflow hardening"; serve a focused buyer page instead of sending
@@ -6539,6 +6868,35 @@ async function addContext(){
6539
6868
  return;
6540
6869
  }
6541
6870
 
6871
+ if (isGetLikeRequest && (
6872
+ pathname === '/yt'
6873
+ || pathname === '/yt.html'
6874
+ || pathname === '/aias-registration-yt'
6875
+ )) {
6876
+ // Dedicated YouTube/CPC destination (FORMAT steal from AIAS registration
6877
+ // landings). /aias-registration-yt is an alias so ad paths that copy the
6878
+ // summit slug still hit ThumbGate. Canonical is /yt. Free GitHub/npm/
6879
+ // Marketplace CTAs only — not a $499 hero, not a 6-agent OS SKU.
6880
+ try {
6881
+ servePublicMarketingPage({
6882
+ req,
6883
+ res,
6884
+ parsed,
6885
+ hostedConfig,
6886
+ isHeadRequest,
6887
+ renderHtml: (runtimeConfig, pageContext) => loadPublicMarketingTemplateHtml(
6888
+ YT_PAGE_PATH,
6889
+ runtimeConfig,
6890
+ pageContext
6891
+ ),
6892
+ extraTelemetry: { pageType: 'yt' },
6893
+ });
6894
+ } catch {
6895
+ sendJson(res, 404, { error: 'YouTube landing page not found' });
6896
+ }
6897
+ return;
6898
+ }
6899
+
6542
6900
  if (isGetLikeRequest && (pathname === '/agent-manager' || pathname === '/agent-manager.html')) {
6543
6901
  // ICP landing page for the role Anthropic named (Agent Manager —
6544
6902
  // hybrid PM/engineer DRI who owns CLAUDE.md hierarchy, plugin
@@ -8363,35 +8721,168 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
8363
8721
  // Public terms of service — required by Stripe / Stripe Checkout for the
8364
8722
  // "Terms of service URL" field in Business → Public details. Mirrors the
8365
8723
  // /privacy + /support pages: thin HTML, no external deps, no DB hit.
8366
- if (isGetLikeRequest && pathname === '/terms') {
8367
- sendHtml(res, 200, `<!DOCTYPE html><html><head><title>Terms of Service — ThumbGate</title></head><body>
8368
- <h1>Terms of Service</h1>
8369
- <p><strong>ThumbGate</strong> (npm: thumbgate)</p>
8370
- <p>Last updated: 2026-05-12</p>
8371
- <h2>The Service</h2>
8372
- <p>ThumbGate provides pre-action gates for AI coding agents: a local CLI (MIT-licensed) and an optional hosted tier at thumbgate-production.up.railway.app. By installing the CLI or paying for a subscription, you agree to these terms.</p>
8373
- <h2>Payment</h2>
8374
- <p>Paid tiers are billed through Stripe. Subscriptions auto-renew until cancelled. One-off purchases (Sprint Diagnostic, Workflow Sprint, Quick Read, Workflow Teardown, First Failure Rule) are charged once.</p>
8375
- <h2>Refunds</h2>
8376
- <p>Pro and Team subscriptions: cancel anytime; we issue a full refund within 7 days of the first charge, prorated thereafter. One-off purchases: refund on request if we cannot deliver the scoped artifact.</p>
8377
- <h2>Acceptable Use</h2>
8378
- <p>You may not use ThumbGate to (a) circumvent the safety controls of other AI providers, (b) generate malware or content that violates third-party terms, or (c) resell the hosted tier without written permission.</p>
8379
- <h2>Disclaimer of Warranty</h2>
8380
- <p>The service is provided "as is", without warranty of any kind. ThumbGate is a guard rail, not a guarantee. We do not warrant that every AI-agent mistake will be prevented.</p>
8381
- <h2>Limitation of Liability</h2>
8382
- <p>Total liability for any claim is limited to the amount you paid in the 12 months preceding the claim.</p>
8383
- <h2>Governing Law</h2>
8384
- <p>These terms are governed by the laws of the State of New York, United States.</p>
8385
- <h2>Changes</h2>
8386
- <p>We may update these terms; material changes will be announced via the email on file at least 14 days before they take effect.</p>
8387
- <h2>Contact</h2><p>igor.ganapolsky@gmail.com</p>
8388
- <p><a href="https://github.com/IgorGanapolsky/ThumbGate">GitHub</a> · <a href="/privacy">Privacy</a> · <a href="/support">Support</a></p>
8389
- </body></html>`, {}, {
8390
- headOnly: isHeadRequest,
8391
- });
8724
+ if (isGetLikeRequest && (pathname === '/terms' || pathname === '/terms.html' || pathname === '/eula' || pathname === '/eula.html')) {
8725
+ try {
8726
+ const termsHtml = fs.readFileSync(path.join(PUBLIC_DIR, 'terms.html'), 'utf-8');
8727
+ sendHtml(res, 200, termsHtml, {}, { headOnly: isHeadRequest });
8728
+ } catch {
8729
+ sendHtml(res, 200, `<!DOCTYPE html><html><head><title>Terms of Service — ThumbGate</title></head><body><h1>Terms of Service</h1><p>Contact: <a href="mailto:legal@thumbgate.ai">legal@thumbgate.ai</a></p></body></html>`, {}, { headOnly: isHeadRequest });
8730
+ }
8392
8731
  return;
8393
8732
  }
8394
8733
 
8734
+ if (isGetLikeRequest && (pathname === '/privacy' || pathname === '/privacy.html')) {
8735
+ try {
8736
+ const privacyHtml = fs.readFileSync(path.join(PUBLIC_DIR, 'privacy.html'), 'utf-8');
8737
+ sendHtml(res, 200, privacyHtml, {}, { headOnly: isHeadRequest });
8738
+ } catch {
8739
+ sendHtml(res, 200, `<!DOCTYPE html><html><head><title>Privacy Policy — ThumbGate</title></head><body><h1>Privacy Policy</h1><p>Contact: <a href="mailto:privacy@thumbgate.ai">privacy@thumbgate.ai</a></p></body></html>`, {}, { headOnly: isHeadRequest });
8740
+ }
8741
+ return;
8742
+ }
8743
+
8744
+ if (isGetLikeRequest && (pathname === '/support' || pathname === '/support.html')) {
8745
+ try {
8746
+ const supportHtml = fs.readFileSync(path.join(PUBLIC_DIR, 'support.html'), 'utf-8');
8747
+ sendHtml(res, 200, supportHtml, {}, { headOnly: isHeadRequest });
8748
+ } catch {
8749
+ sendHtml(res, 200, `<!DOCTYPE html><html><head><title>Support — ThumbGate</title></head><body><h1>Support</h1><p>Contact: <a href="mailto:support@thumbgate.ai">support@thumbgate.ai</a></p></body></html>`, {}, { headOnly: isHeadRequest });
8750
+ }
8751
+ return;
8752
+ }
8753
+
8754
+ if (isGetLikeRequest && (pathname === '/third-party-notices' || pathname === '/third-party-notices.html' || pathname === '/third-party-notices.md')) {
8755
+ try {
8756
+ const noticesHtml = fs.readFileSync(path.join(PUBLIC_DIR, 'third-party-notices.html'), 'utf-8');
8757
+ sendHtml(res, 200, noticesHtml, {}, { headOnly: isHeadRequest });
8758
+ } catch {
8759
+ sendHtml(res, 200, `<!DOCTYPE html><html><head><title>Third-Party Notices — ThumbGate</title></head><body><h1>Third-Party Notices</h1><p>See <a href="https://github.com/IgorGanapolsky/ThumbGate/blob/main/THIRD_PARTY_NOTICES.md">THIRD_PARTY_NOTICES.md</a>.</p></body></html>`, {}, { headOnly: isHeadRequest });
8760
+ }
8761
+ return;
8762
+ }
8763
+
8764
+ // Public security overview + copy-paste questionnaire (not a certification).
8765
+ if (isGetLikeRequest && (pathname === '/security.json' || pathname === '/v1/security-questionnaire')) {
8766
+ sendJson(res, 200, getSecurityQuestionnaire(), {
8767
+ 'Cache-Control': 'public, max-age=300',
8768
+ }, { headOnly: isHeadRequest });
8769
+ return;
8770
+ }
8771
+ if (isGetLikeRequest && (
8772
+ pathname === '/security'
8773
+ || pathname === '/security.html'
8774
+ || pathname === '/trust'
8775
+ || pathname === '/trust.html'
8776
+ )) {
8777
+ sendHtml(res, 200, renderSecurityOverviewHtml(), {}, { headOnly: isHeadRequest });
8778
+ return;
8779
+ }
8780
+
8781
+ // Legal index — entry point for counsel package + public legal URLs.
8782
+ if (isGetLikeRequest && (pathname === '/legal' || pathname === '/legal/')) {
8783
+ sendHtml(res, 200, `<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
8784
+ <title>Legal — ThumbGate</title>
8785
+ <meta name="description" content="ThumbGate legal index: Terms, Privacy, Security, licensing, data-flow, MSA/SOW.">
8786
+ <style>body{font-family:system-ui,-apple-system,sans-serif;max-width:820px;margin:0 auto;padding:32px 20px;line-height:1.55;color:#1f2937}
8787
+ h1{font-size:28px;margin:0 0 8px}h2{font-size:18px;margin:28px 0 8px}p,li{font-size:15px}.meta{color:#6b7280;font-size:14px;margin:0 0 24px}
8788
+ .draft{border-left:3px solid #f59e0b;background:#fffbeb;padding:10px 14px;border-radius:0 8px 8px 0;margin:16px 0}
8789
+ ul{padding-left:22px}li{margin:6px 0}a{color:#0066cc}
8790
+ footer{margin-top:40px;padding-top:20px;border-top:1px solid #e5e7eb;color:#6b7280;font-size:14px}</style></head><body>
8791
+ <h1>Legal</h1>
8792
+ <p class="meta">Public legal surfaces for ThumbGate paid and hosted paths.</p>
8793
+ <div class="draft"><strong>Highest-value first pass:</strong> short data-flow map + subscription terms + privacy policy + open-source boundary. Markdown counsel package lives in <code>docs/legal/</code>.</div>
8794
+ <ul>
8795
+ <li><a href="/terms">Terms of Service</a></li>
8796
+ <li><a href="/privacy">Privacy Policy</a></li>
8797
+ <li><a href="/support">Support</a> — <a href="mailto:support@thumbgate.ai">support@thumbgate.ai</a></li>
8798
+ <li><a href="/security">Security questionnaire</a> — <a href="/security.json">JSON</a></li>
8799
+ <li><a href="/legal/licensing">MIT vs paid licensing boundary</a></li>
8800
+ <li><a href="/legal/data-flow">Data-flow map</a></li>
8801
+ <li><a href="/legal/msa-sow">MSA / SOW template</a></li>
8802
+ <li><a href="https://github.com/IgorGanapolsky/ThumbGate/tree/main/docs/legal">docs/legal on GitHub</a></li>
8803
+ <li><a href="/third-party-notices">Third-party notices</a></li>
8804
+ </ul>
8805
+ <footer><a href="/">Home</a> · <a href="/pricing">Pricing</a> · <a href="mailto:legal@thumbgate.ai">legal@thumbgate.ai</a></footer>
8806
+ </body></html>`, {}, { headOnly: isHeadRequest });
8807
+ return;
8808
+ }
8809
+
8810
+ if (isGetLikeRequest && (pathname === '/legal/licensing' || pathname === '/legal/licensing/')) {
8811
+ sendHtml(res, 200, `<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
8812
+ <title>Licensing — ThumbGate</title>
8813
+ <style>body{font-family:system-ui,-apple-system,sans-serif;max-width:820px;margin:0 auto;padding:32px 20px;line-height:1.55;color:#1f2937}
8814
+ h1{font-size:28px;margin:0 0 8px}h2{font-size:18px;margin:28px 0 8px}p,li{font-size:15px}ul{padding-left:22px}a{color:#0066cc}
8815
+ footer{margin-top:40px;padding-top:20px;border-top:1px solid #e5e7eb;color:#6b7280;font-size:14px}</style></head><body>
8816
+ <h1>Commercial licensing boundary</h1>
8817
+ <p class="meta">MIT vs paid · Last updated 2026-08-12</p>
8818
+ <h2>What is MIT</h2>
8819
+ <p>Unless a file states otherwise, the public repository and published npm package <code>thumbgate</code> are offered under the <strong>MIT</strong> License: local CLI, hooks, public MCP tooling, docs, and tests shipped in that package.</p>
8820
+ <h2>What is not MIT</h2>
8821
+ <ul>
8822
+ <li>Hosted API / production cloud service — proprietary hosted service</li>
8823
+ <li>thumbgate.app pairing, mobile, and cloud runners as offered — proprietary product</li>
8824
+ <li><strong>Pro</strong> subscription entitlement, license keys, hosted dashboard state — subscription license, not transfer of ThumbGate IP</li>
8825
+ <li>Brand and marks — trademark, not MIT code rights to imply endorsement</li>
8826
+ <li>Managed services under an SOW — governed by MSA/SOW</li>
8827
+ </ul>
8828
+ <h2>Customer-specific rules</h2>
8829
+ <p><strong>Customer-specific rules</strong> and configs produced under a paid engagement are owned by the customer as deliverables under the SOW, except that ThumbGate retains pre-existing IP and may reuse generalized non-confidential know-how. <strong>Production approvals</strong> remain the customer’s responsibility; ThumbGate does not assume production change-control ownership by default.</p>
8830
+ <p>Questions: <a href="mailto:legal@thumbgate.ai">legal@thumbgate.ai</a>. Full text: <code>docs/legal/COMMERCIAL_LICENSING_BOUNDARY.md</code>.</p>
8831
+ <footer><a href="/legal">Legal index</a> · <a href="/terms">Terms</a></footer>
8832
+ </body></html>`, {}, { headOnly: isHeadRequest });
8833
+ return;
8834
+ }
8835
+
8836
+ if (isGetLikeRequest && (pathname === '/legal/data-flow' || pathname === '/legal/data-flow/')) {
8837
+ sendHtml(res, 200, `<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
8838
+ <title>Data-flow map — ThumbGate</title>
8839
+ <style>body{font-family:system-ui,-apple-system,sans-serif;max-width:820px;margin:0 auto;padding:32px 20px;line-height:1.55;color:#1f2937}
8840
+ h1{font-size:28px;margin:0 0 8px}h2{font-size:18px;margin:28px 0 8px}p,li{font-size:15px}ul{padding-left:22px}a{color:#0066cc}
8841
+ footer{margin-top:40px;padding-top:20px;border-top:1px solid #e5e7eb;color:#6b7280;font-size:14px}</style></head><body>
8842
+ <h1>Data-flow map</h1>
8843
+ <p>First-pass counsel input: what stays local vs what may leave the machine.</p>
8844
+ <h2>Local MIT CLI</h2>
8845
+ <p><strong>Local MIT CLI</strong> and engine: workspace source, local prevention rules, context packs, and lesson DB stay on disk under paths such as <code>~/.thumbgate/</code> and project <code>.claude/</code>. Workspace source does not leave the machine by default.</p>
8846
+ <h2>What may leave the machine</h2>
8847
+ <ul>
8848
+ <li>Account registration and license checks for Pro / hosted features</li>
8849
+ <li><strong>Stripe</strong> payment tokens and billing metadata when you check out</li>
8850
+ <li>Device pairing identifiers for thumbgate.app</li>
8851
+ <li>Cloud-runner operational logs (timestamps, success/fail, error summaries)</li>
8852
+ <li>Optional CLI telemetry (disable with THUMBGATE_NO_TELEMETRY=1)</li>
8853
+ <li>Support email content you voluntarily send</li>
8854
+ </ul>
8855
+ <p>Full map: <code>docs/legal/DATA_FLOW_MAP.md</code> · <a href="/privacy">Privacy Policy</a></p>
8856
+ <footer><a href="/legal">Legal index</a> · <a href="/privacy">Privacy</a></footer>
8857
+ </body></html>`, {}, { headOnly: isHeadRequest });
8858
+ return;
8859
+ }
8860
+
8861
+ if (isGetLikeRequest && (pathname === '/legal/msa-sow' || pathname === '/legal/msa-sow/')) {
8862
+ sendHtml(res, 200, `<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
8863
+ <title>MSA / SOW — ThumbGate</title>
8864
+ <style>body{font-family:system-ui,-apple-system,sans-serif;max-width:820px;margin:0 auto;padding:32px 20px;line-height:1.55;color:#1f2937}
8865
+ h1{font-size:28px;margin:0 0 8px}h2{font-size:18px;margin:28px 0 8px}p,li{font-size:15px}ul{padding-left:22px}a{color:#0066cc}
8866
+ footer{margin-top:40px;padding-top:20px;border-top:1px solid #e5e7eb;color:#6b7280;font-size:14px}</style></head><body>
8867
+ <h1>Master Services Agreement + Statement of Work</h1>
8868
+ <p>Template for counsel/product use. Not a signed contract. Self-serve Pro is covered by <a href="/terms">Terms</a> + checkout, not this SOW.</p>
8869
+ <h2>Master Services Agreement (summary)</h2>
8870
+ <ul>
8871
+ <li>Services only as described in an executed Statement of Work</li>
8872
+ <li>Fees and taxes as in the SOW; payment via Stripe or invoice</li>
8873
+ <li>Pre-existing ThumbGate IP remains ThumbGate’s; customer owns customer-specific configs under the SOW</li>
8874
+ <li>No guarantee all unsafe agent actions will be blocked</li>
8875
+ <li>Liability capped at fees paid under the applicable SOW in the prior 12 months</li>
8876
+ </ul>
8877
+ <h2>Statement of Work fields</h2>
8878
+ <p>Typical SOW fields: title, supported workflow, access/materials, acceptance criteria, deliverables, exclusions, change requests, approvals, fees, timeline, termination.</p>
8879
+ <p>Contact: <a href="mailto:legal@thumbgate.ai">legal@thumbgate.ai</a>. Full template: <code>docs/legal/MSA_SOW_TEMPLATE.md</code>.</p>
8880
+ <footer><a href="/legal">Legal index</a> · <a href="/terms">Terms</a></footer>
8881
+ </body></html>`, {}, { headOnly: isHeadRequest });
8882
+ return;
8883
+ }
8884
+
8885
+
8395
8886
  // Public case studies — dogfood proof narratives (static HTML; no fabricated logos).
8396
8887
  if (isGetLikeRequest && (pathname === '/case-studies' || pathname === '/case-studies.html')) {
8397
8888
  try {
@@ -8410,14 +8901,7 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
8410
8901
  return;
8411
8902
  }
8412
8903
 
8413
-
8414
- // Public canonical pricing page. The audit flagged "pricing schizophrenia":
8415
- // sales/pricing.json said $49 / $299, COMMERCIAL_TRUTH.md said $19 / $149,
8416
- // and there was no buyer-facing surface to reconcile the two. This is now
8417
- // the single source of truth for what ThumbGate sells, in priority order:
8418
- // Sprint (proof-pack, sales-led) → Pro (self-serve recurring) → Team
8419
- // (after qualification). Every paid CTA across the site should funnel
8420
- // here OR directly into Stripe checkout — never a different price.
8904
+ // Public canonical pricing page.
8421
8905
  if (isGetLikeRequest && (pathname === '/pricing' || pathname === '/pricing.html')) {
8422
8906
  try {
8423
8907
  servePublicMarketingPage({
@@ -8437,100 +8921,14 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
8437
8921
  return;
8438
8922
  }
8439
8923
 
8440
-
8441
- // Remote MCP connector documentation the `resource_documentation` target
8442
- // advertised by /.well-known/oauth-protected-resource. The Claude Connectors
8443
- // Directory requires this URL to resolve (200) for submission review.
8444
- if (isGetLikeRequest && (pathname === '/docs/connectors' || pathname === '/docs/connectors/')) {
8445
- const mcpUrl = buildPublicUrl(hostedConfig, '/mcp');
8446
- const cardUrl = buildPublicUrl(hostedConfig, '/.well-known/mcp/server-card.json');
8447
- const prmUrl = buildPublicUrl(hostedConfig, '/.well-known/oauth-protected-resource');
8448
- sendHtml(res, 200, `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Remote MCP Connector — ThumbGate</title><meta name="description" content="Connect ThumbGate to Claude as a remote MCP server: OAuth 2.1 (PKCE) authorization, available tools, and the read-only reviewer credential."><style>body{font-family:system-ui,-apple-system,sans-serif;max-width:780px;margin:0 auto;padding:32px 20px;line-height:1.55;color:#1f2937}h1{font-size:30px;margin:0 0 8px}.lede{color:#6b7280;font-size:18px;margin:0 0 28px}h2{font-size:20px;margin:28px 0 8px}code{background:#f3f4f6;padding:1px 6px;border-radius:4px;font-size:13.5px}pre{background:#0f172a;color:#e2e8f0;padding:14px 16px;border-radius:8px;overflow:auto;font-size:13px}a{color:#0066cc}ol,ul{padding-left:22px}li{margin:6px 0}.note{border-left:3px solid #22d3ee;background:#ecfeff;padding:10px 14px;border-radius:0 8px 8px 0;margin:16px 0}footer{margin-top:40px;padding-top:20px;border-top:1px solid #e5e7eb;color:#6b7280;font-size:14px}</style></head><body>
8449
- <h1>ThumbGate — Remote MCP Connector</h1>
8450
- <p class="lede">ThumbGate is a remote Model Context Protocol (MCP) server. Add it as a connector in Claude to give an agent governed access to ThumbGate's feedback capture, lesson retrieval, context assembly, and pre-action gate tools — over HTTP, authenticated with OAuth 2.1.</p>
8451
-
8452
- <h2>Connect URL</h2>
8453
- <pre>${esc(mcpUrl)}</pre>
8454
- <p>In Claude, add a custom connector and paste the URL above. Claude discovers the authorization server automatically via the protected-resource metadata at <a href="${esc(prmUrl)}">${esc(prmUrl)}</a>.</p>
8455
-
8456
- <h2>Authorization (OAuth 2.1 + PKCE)</h2>
8457
- <ol>
8458
- <li>Claude reads the <code>resource</code> and <code>authorization_servers</code> from the protected-resource metadata.</li>
8459
- <li>It runs the standard OAuth 2.1 authorization-code flow with PKCE (<code>S256</code> only — <code>plain</code> is rejected).</li>
8460
- <li>The issued access token is audience-bound to the <code>${esc(mcpUrl)}</code> resource (RFC 8707); a token for any other audience is rejected.</li>
8461
- <li>The bearer token is then sent on the <code>Authorization</code> header for every <code>tools/call</code>.</li>
8462
- </ol>
8463
-
8464
- <h2>Available tools</h2>
8465
- <p>The full, machine-readable tool registry — names, input schemas, and the <code>readOnlyHint</code> / <code>destructiveHint</code> annotations — is published at <a href="${esc(cardUrl)}">${esc(cardUrl)}</a>. Tools fall into these groups:</p>
8466
- <ul>
8467
- <li><strong>Feedback &amp; lessons</strong> — <code>capture_feedback</code>, <code>feedback_summary</code>, <code>search_lessons</code>, <code>retrieve_lessons</code>, <code>prevention_rules</code>.</li>
8468
- <li><strong>Context engineering</strong> — <code>construct_context_pack</code>, <code>evaluate_context_pack</code>, <code>unified_context</code>, <code>recall</code>.</li>
8469
- <li><strong>Pre-action gates &amp; governance</strong> — <code>satisfy_gate</code>, <code>track_action</code>, <code>approve_protected_action</code>, <code>verify_claim</code>, <code>enforcement_matrix</code>.</li>
8470
- <li><strong>Diagnostics &amp; planning</strong> — <code>diagnose_failure</code>, <code>suggest_fix</code>, <code>security_scan</code>, and the <code>plan_*</code> advisory tools.</li>
8471
- </ul>
8472
-
8473
- <h2>Reviewer credential (read-only)</h2>
8474
- <div class="note">For directory reviewers: ThumbGate issues a dedicated <strong>read-only</strong> reviewer credential. A token bound to that credential may invoke only tools annotated <code>readOnlyHint: true</code>; any write or mutating tool call is rejected. This makes the credential safe to share for review without granting the ability to mutate shared server state. Request it from the contact below.</div>
8475
-
8476
- <h2>Source &amp; contact</h2>
8477
- <p>Open-source CLI and server: <a href="https://github.com/IgorGanapolsky/ThumbGate">github.com/IgorGanapolsky/ThumbGate</a>. Questions or reviewer-credential requests: <a href="mailto:igor.ganapolsky@gmail.com">igor.ganapolsky@gmail.com</a>.</p>
8478
-
8479
- <footer><a href="/">ThumbGate</a> · <a href="/support">Support</a> · <a href="/privacy">Privacy</a> · <a href="/terms">Terms</a></footer>
8480
- </body></html>`, {}, { headOnly: isHeadRequest });
8481
- return;
8482
- }
8483
-
8484
- // Public support / contact page — required for Stripe Business → Public
8485
- // details "Customer support URL" field. Single source of truth for how
8486
- // customers reach us (email, GitHub issues, status page).
8487
- if (isGetLikeRequest && pathname === '/support') {
8488
- sendHtml(res, 200, `<!DOCTYPE html><html><head><title>Support — ThumbGate</title></head><body>
8489
- <h1>Support</h1>
8490
- <p><strong>ThumbGate</strong> support, billing, and contact paths.</p>
8491
- <h2>Email</h2>
8492
- <p>For billing questions, refunds, subscription changes, or technical issues with the hosted tier: <a href="mailto:igor.ganapolsky@gmail.com">igor.ganapolsky@gmail.com</a>. We reply within one business day.</p>
8493
- <h2>GitHub Issues</h2>
8494
- <p>For bugs, CLI questions, and feature requests in the open-source CLI: <a href="https://github.com/IgorGanapolsky/ThumbGate/issues">github.com/IgorGanapolsky/ThumbGate/issues</a>.</p>
8495
- <h2>Status</h2>
8496
- <p>Hosted-tier status: check <a href="https://thumbgate-production.up.railway.app/health">/health</a> for current health. Railway-hosted; rebuilds take 2-5 minutes.</p>
8497
- <h2>Refunds</h2>
8498
- <p>Pro / Team subscriptions: 7-day full refund window from first charge. One-off purchases: refund on request if we cannot deliver. Email the address above.</p>
8499
- <h2>Security</h2>
8500
- <p>Disclose vulnerabilities by email; please do not post to public GitHub issues. We acknowledge within 48 hours.</p>
8501
- <p><a href="https://github.com/IgorGanapolsky/ThumbGate">GitHub</a> · <a href="/privacy">Privacy</a> · <a href="/terms">Terms</a></p>
8502
- </body></html>`, {}, {
8503
- headOnly: isHeadRequest,
8504
- });
8505
- return;
8506
- }
8507
-
8508
- // Public privacy policy — required for GPT Store and marketplace listings
8509
- if (isGetLikeRequest && pathname === '/privacy') {
8510
- sendHtml(res, 200, `<!DOCTYPE html><html><head><title>Privacy Policy — ThumbGate</title></head><body>
8511
- <h1>Privacy Policy</h1>
8512
- <p><strong>ThumbGate</strong> (npm: thumbgate)</p>
8513
- <p>Last updated: 2026-03-11</p>
8514
- <h2>Data Collection</h2>
8515
- <p>The self-hosted version stores workflow data locally on your machine. Local feedback, memory entries, proof artifacts, and context packs stay in your project files unless you explicitly point the system at a hosted endpoint.</p>
8516
- <p>The hosted tier (thumbgate-production.up.railway.app) stores feedback signals, memory entries, and related workflow metadata associated with your API key.</p>
8517
- <p>Optional CLI telemetry is best-effort and covers install or usage metadata needed to understand adoption and failures. You can disable it with <code>THUMBGATE_NO_TELEMETRY=1</code>.</p>
8518
- <h2>Data Stored</h2><ul>
8519
- <li>Feedback signals (thumbs up/down) with context you provide</li>
8520
- <li>Promoted memory entries</li>
8521
- <li>Prevention rules generated from your feedback</li>
8522
- </ul>
8523
- <h2>Data Sharing</h2>
8524
- <p>We do not sell customer data. Hosted data is used to operate the service and is not shared with third parties except for infrastructure providers needed to run the product.</p>
8525
- <h2>Data Retention</h2>
8526
- <p>Local data is retained until you delete the files. Hosted data is retained while your account or API key remains active, or until you request deletion, subject to operational or legal retention requirements.</p>
8527
- <h2>Data Deletion</h2>
8528
- <p>Contact igor.ganapolsky@gmail.com to request deletion of hosted data.</p>
8529
- <h2>Contact</h2><p>igor.ganapolsky@gmail.com</p>
8530
- <p><a href="https://github.com/IgorGanapolsky/ThumbGate">GitHub</a></p>
8531
- </body></html>`, {}, {
8532
- headOnly: isHeadRequest,
8533
- });
8924
+ // Public Remote MCP Connector Documentation.
8925
+ if (isGetLikeRequest && (pathname === '/docs/connectors' || pathname === '/docs/connectors/' || pathname === '/docs/connectors.html')) {
8926
+ try {
8927
+ const connectorsHtml = fs.readFileSync(path.join(PUBLIC_DIR, 'docs', 'connectors.html'), 'utf-8');
8928
+ sendHtml(res, 200, connectorsHtml, {}, { headOnly: isHeadRequest });
8929
+ } catch {
8930
+ sendJson(res, 404, { error: 'Connectors documentation not found' });
8931
+ }
8534
8932
  return;
8535
8933
  }
8536
8934
 
@@ -9688,6 +10086,13 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
9688
10086
  guardrails: body.guardrails,
9689
10087
  tags: extractTags(body.tags),
9690
10088
  skill: body.skill,
10089
+ memorySource: body.memorySource && typeof body.memorySource === 'object'
10090
+ ? {
10091
+ type: body.memorySource.type,
10092
+ identifier: body.memorySource.identifier,
10093
+ trust: body.memorySource.trust,
10094
+ }
10095
+ : undefined,
9691
10096
  reviewOrigin: 'human',
9692
10097
  });
9693
10098
  const actionIntegration = inferActionIntegration(body, req.headers);
@@ -10026,12 +10431,23 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
10026
10431
  } catch (err) {
10027
10432
  throw createHttpError(400, err.message || 'Invalid namespaces');
10028
10433
  }
10029
- const pack = constructContextPack({
10030
- query: body.query || '',
10031
- maxItems: Number(body.maxItems || 8),
10032
- maxChars: Number(body.maxChars || 6000),
10033
- namespaces,
10034
- });
10434
+ let pack;
10435
+ try {
10436
+ pack = constructContextPack({
10437
+ query: body.query || '',
10438
+ maxItems: Number(body.maxItems || 8),
10439
+ maxChars: Number(body.maxChars || 6000),
10440
+ namespaces,
10441
+ strategy: body.strategy || null,
10442
+ contextEnvelope: body.contextEnvelope || null,
10443
+ });
10444
+ } catch (err) {
10445
+ if (err.code === 'INVALID_CONTEXT_ENVELOPE'
10446
+ || err.code === 'CONTEXT_BUDGET_EXCEEDED') {
10447
+ throw createHttpError(400, err.message);
10448
+ }
10449
+ throw err;
10450
+ }
10035
10451
  sendJson(res, 200, pack);
10036
10452
  return;
10037
10453
  }
@@ -10744,11 +11160,151 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
10744
11160
  });
10745
11161
  return;
10746
11162
  }
10747
- const entry = satisfyCondition(body.gateId, body.evidence);
11163
+ // Attribute the override to the API surface. Without an explicit
11164
+ // source the record defaults to 'cli' and the audit summary cannot
11165
+ // tell an HTTP unlock from a local one.
11166
+ const entry = satisfyCondition(body.gateId, body.evidence, null, {
11167
+ source: 'api',
11168
+ actor: body.actor || 'api-client',
11169
+ });
10748
11170
  sendJson(res, 200, { satisfied: true, gateId: body.gateId, ...entry });
10749
11171
  return;
10750
11172
  }
10751
11173
 
11174
+ // POST /v1/hermes/initialize — Bind a connectionId before turn start/end
11175
+ if (req.method === 'POST' && pathname === '/v1/hermes/initialize') {
11176
+ const body = await parseJsonBody(req);
11177
+ const { protocol } = getHostedHermes(requestDataIdentity);
11178
+ const result = protocol.initialize({
11179
+ connectionId: body.connectionId,
11180
+ });
11181
+ if (!result.ok) {
11182
+ sendProblem(res, {
11183
+ type: PROBLEM_TYPES.BAD_REQUEST,
11184
+ title: 'Hermes Initialize Rejected',
11185
+ status: 400,
11186
+ detail: result.reason,
11187
+ ...result,
11188
+ });
11189
+ return;
11190
+ }
11191
+ sendJson(res, 200, result);
11192
+ return;
11193
+ }
11194
+
11195
+ // POST /v1/hermes/turn/start — Start turn under Hermes Platform Protocol
11196
+ // Fail closed until POST /v1/hermes/initialize registers the connectionId.
11197
+ if (req.method === 'POST' && pathname === '/v1/hermes/turn/start') {
11198
+ const body = await parseJsonBody(req);
11199
+ const { protocol } = getHostedHermes(requestDataIdentity);
11200
+ const result = protocol.startTurn(body);
11201
+ if (!result.ok) {
11202
+ sendProblem(res, {
11203
+ type: PROBLEM_TYPES.BAD_REQUEST,
11204
+ title: 'Hermes Turn Rejected',
11205
+ status: 400,
11206
+ detail: result.reason,
11207
+ ...result,
11208
+ });
11209
+ return;
11210
+ }
11211
+ sendJson(res, 200, result);
11212
+ return;
11213
+ }
11214
+
11215
+ // POST /v1/hermes/turn/end — End active turn under Hermes Platform Protocol
11216
+ if (req.method === 'POST' && pathname === '/v1/hermes/turn/end') {
11217
+ const body = await parseJsonBody(req);
11218
+ const { protocol } = getHostedHermes(requestDataIdentity);
11219
+ const result = protocol.endTurn(body);
11220
+ if (!result.ok) {
11221
+ sendProblem(res, {
11222
+ type: PROBLEM_TYPES.BAD_REQUEST,
11223
+ title: 'Hermes End Turn Rejected',
11224
+ status: 400,
11225
+ detail: result.reason,
11226
+ ...result,
11227
+ });
11228
+ return;
11229
+ }
11230
+ sendJson(res, 200, result);
11231
+ return;
11232
+ }
11233
+
11234
+ // POST /v1/hermes/action/approve — Record human/operator approval for consequential action
11235
+ if (req.method === 'POST' && pathname === '/v1/hermes/action/approve') {
11236
+ const body = await parseJsonBody(req);
11237
+ const boundApproverId = resolveHermesApproverId(
11238
+ req,
11239
+ humanReviewerConfig,
11240
+ requestDataIdentity,
11241
+ );
11242
+ if (!boundApproverId) {
11243
+ sendProblem(res, {
11244
+ type: PROBLEM_TYPES.UNAUTHORIZED,
11245
+ title: 'Hermes Approval Rejected',
11246
+ status: 401,
11247
+ detail: 'Authenticated reviewer identity is required for Hermes approvals.',
11248
+ });
11249
+ return;
11250
+ }
11251
+ const { protocol } = getHostedHermes(requestDataIdentity);
11252
+ const result = protocol.approve({
11253
+ ...body,
11254
+ approverId: boundApproverId,
11255
+ });
11256
+ if (!result.ok) {
11257
+ sendProblem(res, {
11258
+ type: PROBLEM_TYPES.BAD_REQUEST,
11259
+ title: 'Hermes Approval Rejected',
11260
+ status: 400,
11261
+ detail: result.reason,
11262
+ ...result,
11263
+ });
11264
+ return;
11265
+ }
11266
+ sendJson(res, 200, result);
11267
+ return;
11268
+ }
11269
+
11270
+ // GET /v1/hermes/sync/read — Reactive read path with cursor + offset
11271
+ if (req.method === 'GET' && pathname === '/v1/hermes/sync/read') {
11272
+ const rawOffset = parsed.searchParams.get('offset');
11273
+ const offset = rawOffset !== null ? Number(rawOffset) : undefined;
11274
+ const cursor = parsed.searchParams.get('cursor') || undefined;
11275
+ const threadId = parsed.searchParams.get('threadId') || undefined;
11276
+ const recordId = parsed.searchParams.get('recordId') || undefined;
11277
+ const mode = parsed.searchParams.get('mode') || undefined;
11278
+ // Prefer the authenticated principal over a client-supplied userId so
11279
+ // hosted tenants cannot probe another identity's sync shapes.
11280
+ const authUserId = requestDataIdentity?.accessContext?.principalId
11281
+ || parsed.searchParams.get('userId')
11282
+ || undefined;
11283
+
11284
+ const { syncPlane } = getHostedHermes(requestDataIdentity);
11285
+ const result = syncPlane.readStatus({
11286
+ offset,
11287
+ cursor,
11288
+ auth: authUserId ? { userId: authUserId } : null,
11289
+ threadId,
11290
+ recordId,
11291
+ mode,
11292
+ });
11293
+
11294
+ if (!result.ok) {
11295
+ sendProblem(res, {
11296
+ type: PROBLEM_TYPES.BAD_REQUEST,
11297
+ title: 'Hermes Sync Read Rejected',
11298
+ status: 400,
11299
+ detail: result.reason,
11300
+ ...result,
11301
+ });
11302
+ return;
11303
+ }
11304
+ sendJson(res, 200, result);
11305
+ return;
11306
+ }
11307
+
10752
11308
  // GET /api/conversions — Conversion stats derived from the Stripe event log
10753
11309
  if (req.method === 'GET' && pathname === '/api/conversions') {
10754
11310
  try {
@@ -10786,7 +11342,20 @@ a{color:#8b9}</style></head><body><form class="card" method="post" action="/oaut
10786
11342
  function startServer({ port, host } = {}) {
10787
11343
  const listenPort = Number(port ?? process.env.PORT ?? 8787);
10788
11344
  const listenHost = String(host ?? process.env.HOST ?? '0.0.0.0').trim() || '0.0.0.0';
10789
- fs.mkdirSync(getFeedbackPaths().FEEDBACK_DIR, { recursive: true });
11345
+ const feedbackPaths = getFeedbackPaths();
11346
+ fs.mkdirSync(feedbackPaths.FEEDBACK_DIR, { recursive: true });
11347
+ // Hosted volumes often start empty (Docker does not ship .claude/memory).
11348
+ // Seed a durable "thumbgate" corpus so authenticated deploy proof can verify retrieval.
11349
+ if (shouldEnsureProductionSearchCorpus(process.env)) {
11350
+ try {
11351
+ const seedResult = ensureProductionSearchCorpus({ feedbackDir: feedbackPaths.FEEDBACK_DIR });
11352
+ if (seedResult.wrote.memory || seedResult.wrote.feedback || seedResult.wrote.rules) {
11353
+ console.log('[startup] ensured production search corpus seed', JSON.stringify(seedResult.wrote));
11354
+ }
11355
+ } catch (err) {
11356
+ console.warn('[startup] ensure production search corpus failed:', err?.message || err);
11357
+ }
11358
+ }
10790
11359
  const server = createApiServer();
10791
11360
  registerGracefulShutdown(server);
10792
11361
  return new Promise((resolve) => {
@@ -10869,6 +11438,8 @@ module.exports = {
10869
11438
  sanitizeHtmlUnsafeJsonValue,
10870
11439
  buildHostedTenantIdentity,
10871
11440
  partitionFeedbackPaths,
11441
+ buildLiveDashboardCacheKey,
11442
+ loadCachedLiveDashboardData,
10872
11443
  },
10873
11444
  };
10874
11445