thumbgate 1.34.3 → 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 (200) 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 +373 -5
  24. package/bin/futureagi-bridge +9 -0
  25. package/config/gate-templates.json +653 -4
  26. package/config/gates/actor-critic-audit.json +34 -0
  27. package/config/gates/default.json +21 -2
  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 +10 -1
  34. package/config/model-candidates.json +382 -24
  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 +401 -147
  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/auto-promote-gates.js +178 -27
  89. package/scripts/billing.js +1 -1
  90. package/scripts/broker-execution-receipts.js +719 -0
  91. package/scripts/budget-aware-gates-proof.js +423 -0
  92. package/scripts/claude-feedback-sync.js +29 -3
  93. package/scripts/claw-harness-production.js +237 -0
  94. package/scripts/cli-progress.js +111 -0
  95. package/scripts/cli-schema.js +163 -1
  96. package/scripts/codex-runbook-flywheel.js +318 -0
  97. package/scripts/context-footprint.js +186 -0
  98. package/scripts/contextfs.js +143 -61
  99. package/scripts/dashboard-limits.js +27 -0
  100. package/scripts/dashboard.js +279 -9
  101. package/scripts/deepseek-v4-runtime-guardrails.js +72 -6
  102. package/scripts/docker-sandbox-planner.js +18 -0
  103. package/scripts/double-blind-eval-protocol.js +252 -0
  104. package/scripts/edotenv-rl-gateway.js +259 -0
  105. package/scripts/ensure-production-search-corpus.js +162 -0
  106. package/scripts/eval-holdout.js +311 -0
  107. package/scripts/feedback-aggregate.js +21 -2
  108. package/scripts/feedback-loop.js +87 -5
  109. package/scripts/feedback-quality.js +9 -0
  110. package/scripts/file-ledger-lock.js +4 -1
  111. package/scripts/financial-control-plane.js +41 -1
  112. package/scripts/find-dormant-requires.js +118 -0
  113. package/scripts/fs-utils.js +84 -8
  114. package/scripts/gate-stats.js +2 -2
  115. package/scripts/gates-engine.js +859 -58
  116. package/scripts/generate-case-study-outreach.js +24 -15
  117. package/scripts/git-at-scale.js +628 -0
  118. package/scripts/governance-conflict-audit.js +1650 -0
  119. package/scripts/governance-difficulty-curriculum.js +328 -0
  120. package/scripts/graphrag-retrieval.js +275 -0
  121. package/scripts/gurobi-optimizer.js +324 -0
  122. package/scripts/gurobi_optimizer.py +485 -0
  123. package/scripts/harness-selector.js +82 -1
  124. package/scripts/hidden-entry-points.js +284 -0
  125. package/scripts/human-escalation.js +199 -1
  126. package/scripts/hybrid-feedback-context.js +152 -19
  127. package/scripts/intent-governed-execution.js +602 -0
  128. package/scripts/intervention-policy.js +123 -20
  129. package/scripts/jit-harness-compose.js +628 -0
  130. package/scripts/jsonl-watcher.js +10 -0
  131. package/scripts/lesson-embedding-index.js +95 -12
  132. package/scripts/lesson-retrieval.js +105 -19
  133. package/scripts/local-model-profile.js +19 -2
  134. package/scripts/mailer/resend-mailer.js +1 -1
  135. package/scripts/matryoshka-embedding.js +235 -0
  136. package/scripts/mcp-oauth.js +42 -4
  137. package/scripts/mcp-session-handles.js +1016 -0
  138. package/scripts/mcp-wiring-doctor.js +314 -0
  139. package/scripts/memory-firewall.js +115 -2
  140. package/scripts/memory-scope-readiness.js +299 -0
  141. package/scripts/memory-vs-rag-route.js +161 -0
  142. package/scripts/model-tier-router.js +148 -21
  143. package/scripts/nvidia-specdecode-al-doctor.js +536 -0
  144. package/scripts/openui-catalog-compose-honesty.js +593 -0
  145. package/scripts/operational-integrity.js +19 -1
  146. package/scripts/override-audit.js +213 -0
  147. package/scripts/package-manager-honesty-doctor.js +458 -0
  148. package/scripts/pr-manager.js +63 -1
  149. package/scripts/prove-herdr-adapter.js +52 -0
  150. package/scripts/prove-memory-pyramid-and-symbolic-canvas.js +95 -0
  151. package/scripts/prove-workos.js +73 -0
  152. package/scripts/provider-attestation-conformance.js +192 -0
  153. package/scripts/provider-receipt-contract.js +136 -0
  154. package/scripts/qwen38-max-cost-optimizer.js +401 -0
  155. package/scripts/radware-threat-defense.js +280 -0
  156. package/scripts/rag-embedding-identity.js +221 -0
  157. package/scripts/rag-precision-guardrails.js +112 -2
  158. package/scripts/remote-feedback-capture.js +159 -0
  159. package/scripts/research-agent-harness.js +256 -0
  160. package/scripts/rsi-safety-hillclimb.js +200 -0
  161. package/scripts/rule-sprawl.js +188 -0
  162. package/scripts/schedule-manager.js +147 -0
  163. package/scripts/self-heal.js +8 -0
  164. package/scripts/session-lease.js +415 -0
  165. package/scripts/simatree-data-governance.js +347 -0
  166. package/scripts/slo-alert-engine.js +172 -7
  167. package/scripts/solver-parity.js +539 -0
  168. package/scripts/stealth-memory-injection-gate.js +333 -0
  169. package/scripts/switchyard-router.js +366 -0
  170. package/scripts/telemetry-analytics.js +84 -27
  171. package/scripts/temporal-decay-weighting.js +138 -0
  172. package/scripts/test-all.js +165 -0
  173. package/scripts/token-savings.js +42 -0
  174. package/scripts/tool-kpi-tracker.js +108 -5
  175. package/scripts/tool-registry.js +193 -5
  176. package/scripts/universal-claim-evaluator.js +14 -2
  177. package/scripts/vector-store.js +279 -9
  178. package/scripts/workflow-notebook.js +391 -0
  179. package/scripts/workflow-sentinel.js +111 -12
  180. package/scripts/workos-production-guard.js +260 -0
  181. package/scripts/workspace-search-route.js +515 -0
  182. package/server.json +2 -2
  183. package/src/agent-identity-boundary.js +76 -0
  184. package/src/agent-retrieval-cache.js +155 -0
  185. package/src/alert-noise-ledger.js +502 -0
  186. package/src/api/server.js +802 -185
  187. package/src/git-fast-cache.js +220 -0
  188. package/src/git-wal-sync.js +156 -0
  189. package/src/hash-anchored-edit.js +82 -0
  190. package/src/hermes-platform-protocol.js +475 -0
  191. package/src/hermes-sync-plane.js +241 -0
  192. package/src/index.js +30 -1
  193. package/src/iso42001-compliance-guard.js +97 -0
  194. package/src/latency-budget.js +244 -0
  195. package/src/mcp-writeguard.js +316 -0
  196. package/src/miminions-adapter.js +106 -0
  197. package/src/pipeline-compass.js +104 -0
  198. package/src/ppl-alert-pipeline.js +284 -0
  199. package/src/rendezvous-router.js +90 -0
  200. package/src/security-questionnaire.js +195 -0
@@ -0,0 +1,252 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Double-Blind Evaluation Protocol — ThumbGate steal of the Google DeepMind
5
+ * pilot (TheNewStack coverage; deepmind.google blog): the first double-blind
6
+ * evaluation of a proprietary frontier model, run in a confidential-computing
7
+ * enclave so the provider never sees the benchmark questions and the
8
+ * evaluator never sees the weights.
9
+ *
10
+ * Why it matters (the article's numbers): earlier research found signs of
11
+ * benchmark leakage in about half of 31 models tested, and contamination
12
+ * inflates scores, especially for larger models. Closed models make private
13
+ * benchmarks hard: API-based evaluation exposes questions to the provider.
14
+ *
15
+ * Protocol, mapped onto ThumbGate as the enclave broker:
16
+ *
17
+ * provider seals model asset (weights / inference code / agent config)
18
+ * evaluator seals benchmark asset (prompts / eval code)
19
+ * | encrypted in transit; only hashes cross the boundary
20
+ * v
21
+ * ThumbGate enclave runs the eval over the sealed pair
22
+ * v
23
+ * ONLY scores leave the enclave — never questions, never weights
24
+ * v
25
+ * attestation receipt (hash chain) proves neither party saw the
26
+ * other's assets before the results were released
27
+ *
28
+ * Honesty: this is a deterministic local model of the protocol (sha256
29
+ * sealing + HMAC attestation). It does not provide real confidential
30
+ * computing; it provides the enforcement surface ThumbGate can gate on.
31
+ */
32
+
33
+ const crypto = require('node:crypto');
34
+
35
+ const ASSET_KINDS = Object.freeze(['model', 'benchmark']);
36
+
37
+ // Private store for sealed asset content — never exposed in JSON serialization.
38
+ const _contentStore = new WeakMap();
39
+
40
+ /**
41
+ * Seal one asset. The content goes into the sealed store; only metadata and
42
+ * a sha256 commitment cross the boundary.
43
+ *
44
+ * @param {string} kind 'model' | 'benchmark'
45
+ * @param {string} content the protected asset
46
+ * @param {string} owner who sealed it
47
+ */
48
+ function sealAsset(kind, content, owner) {
49
+ if (!ASSET_KINDS.includes(kind)) {
50
+ throw new Error(`unknown asset kind "${kind}"`);
51
+ }
52
+ if (typeof content !== 'string' || content.length === 0) {
53
+ throw new Error('asset content must be a non-empty string');
54
+ }
55
+ const digest = crypto.createHash('sha256').update(content).digest('hex');
56
+ const seal = {
57
+ sealId: `seal_${digest.slice(0, 16)}`,
58
+ kind,
59
+ owner: String(owner || 'unknown'),
60
+ sha256: digest,
61
+ sealedAt: new Date().toISOString(),
62
+ // token required to retrieve the content; holder != the other party
63
+ accessToken: crypto.randomBytes(16).toString('hex'),
64
+ };
65
+ // Store content privately so it is never serialized by JSON.stringify.
66
+ Object.defineProperty(seal, '_content', {
67
+ value: content,
68
+ enumerable: false,
69
+ configurable: false,
70
+ writable: false,
71
+ });
72
+ _contentStore.set(seal, content);
73
+ return seal;
74
+ }
75
+
76
+ /**
77
+ * Create an enclave over one model seal and one benchmark seal.
78
+ */
79
+ function createEnclave(modelSeal, benchmarkSeal) {
80
+ if (!modelSeal || modelSeal.kind !== 'model') {
81
+ throw new Error('enclave requires a model seal');
82
+ }
83
+ if (!benchmarkSeal || benchmarkSeal.kind !== 'benchmark') {
84
+ throw new Error('enclave requires a benchmark seal');
85
+ }
86
+ return {
87
+ enclaveId: `enclave_${crypto.randomBytes(8).toString('hex')}`,
88
+ modelSeal: { sealId: modelSeal.sealId, sha256: modelSeal.sha256, owner: modelSeal.owner },
89
+ benchmarkSeal: { sealId: benchmarkSeal.sealId, sha256: benchmarkSeal.sha256, owner: benchmarkSeal.owner },
90
+ openedAt: new Date().toISOString(),
91
+ modeled: true,
92
+ note: 'local deterministic model of a confidential-computing enclave',
93
+ };
94
+ }
95
+
96
+ /**
97
+ * Run the evaluation inside the enclave. The scoring function receives the
98
+ * sealed pair and returns per-question results; only scores leave.
99
+ *
100
+ * @param {object} enclave
101
+ * @param {object} modelSeal sealed model asset (with access token)
102
+ * @param {object} benchmarkSeal sealed benchmark asset (with access token)
103
+ * @param {Function} scoreFn ({ modelContent, questions }) => [{question, score}]
104
+ */
105
+ function runEvaluation(enclave, modelSeal, benchmarkSeal, scoreFn) {
106
+ // Validate seals against the enclave commitment before scoring.
107
+ // A caller cannot swap seals or tamper with content after enclave creation.
108
+ if (modelSeal.sealId !== enclave.modelSeal.sealId ||
109
+ modelSeal.sha256 !== enclave.modelSeal.sha256) {
110
+ throw new Error('model seal does not match the enclave commitment');
111
+ }
112
+ if (benchmarkSeal.sealId !== enclave.benchmarkSeal.sealId ||
113
+ benchmarkSeal.sha256 !== enclave.benchmarkSeal.sha256) {
114
+ throw new Error('benchmark seal does not match the enclave commitment');
115
+ }
116
+ const modelContent = _contentStore.get(modelSeal);
117
+ const benchmarkContent = _contentStore.get(benchmarkSeal);
118
+ if (typeof modelContent !== 'string') {
119
+ throw new Error('model content is not available in the private store');
120
+ }
121
+ const questions = JSON.parse(benchmarkContent);
122
+ const results = scoreFn({
123
+ modelContent,
124
+ questions,
125
+ });
126
+ // Require exactly one result per benchmark question.
127
+ if (!Array.isArray(results) || results.length !== questions.length) {
128
+ throw new Error(
129
+ `scoreFn must return exactly ${questions.length} results, got ${Array.isArray(results) ? results.length : 'non-array'}`,
130
+ );
131
+ }
132
+ // Only scores cross the boundary. Questions and model content never do.
133
+ const scores = results.map((r, i) => ({
134
+ index: i,
135
+ score: Number(r.score),
136
+ passed: Boolean(r.score >= 1),
137
+ }));
138
+ return {
139
+ enclaveId: enclave.enclaveId,
140
+ released: 'scores-only',
141
+ scores,
142
+ passRate: scores.length === 0 ? 0 : scores.filter((s) => s.passed).length / scores.length,
143
+ questions: undefined, // explicitly withheld
144
+ modelContent: undefined, // explicitly withheld
145
+ };
146
+ }
147
+
148
+ /**
149
+ * Leakage guard: refuse to release any output that contains benchmark
150
+ * question text. This is the enforcement teeth of the protocol.
151
+ */
152
+ function leakageGuard(output, benchmarkSeal) {
153
+ const questions = JSON.parse(benchmarkSeal._content);
154
+ const s = typeof output === 'string' ? output : JSON.stringify(output);
155
+ for (const q of questions) {
156
+ const text = typeof q === 'string' ? q : String(q.prompt || '');
157
+ if (text && s.includes(text)) {
158
+ return { clean: false, leakedQuestionIndex: questions.indexOf(q) };
159
+ }
160
+ }
161
+ return { clean: true };
162
+ }
163
+
164
+ /**
165
+ * Attestation receipt: hash chain over both seals + the released scores.
166
+ * A third party can verify it without ever seeing either asset.
167
+ */
168
+ function attest(enclave, evaluationResult, attestationKey) {
169
+ if (typeof attestationKey !== 'string' || attestationKey.trim().length === 0) {
170
+ throw new Error('a non-empty attestationKey must be provided by the caller');
171
+ }
172
+ const key = attestationKey;
173
+ const payload = JSON.stringify({
174
+ enclaveId: enclave.enclaveId,
175
+ modelSealSha: enclave.modelSeal.sha256,
176
+ benchmarkSealSha: enclave.benchmarkSeal.sha256,
177
+ scores: evaluationResult.scores,
178
+ });
179
+ const receiptHash = crypto.createHmac('sha256', key).update(payload).digest('hex');
180
+ return {
181
+ receiptId: `att_${receiptHash.slice(0, 16)}`,
182
+ enclaveId: enclave.enclaveId,
183
+ modelSealSha: enclave.modelSeal.sha256,
184
+ benchmarkSealSha: enclave.benchmarkSeal.sha256,
185
+ scoreCount: evaluationResult.scores.length,
186
+ passRate: evaluationResult.passRate,
187
+ receiptHash,
188
+ attestedAt: new Date().toISOString(),
189
+ claim: 'neither party accessed the other\'s sealed asset before results release',
190
+ };
191
+ }
192
+
193
+ /**
194
+ * Verify an attestation receipt against the seals and scores.
195
+ */
196
+ function verifyAttestation(receipt, modelSeal, benchmarkSeal, scores, attestationKey) {
197
+ if (typeof attestationKey !== 'string' || attestationKey.trim().length === 0) {
198
+ throw new Error('a non-empty attestationKey must be provided by the caller');
199
+ }
200
+ const key = attestationKey;
201
+ const payload = JSON.stringify({
202
+ enclaveId: receipt.enclaveId,
203
+ modelSealSha: modelSeal.sha256,
204
+ benchmarkSealSha: benchmarkSeal.sha256,
205
+ scores,
206
+ });
207
+ const expected = crypto.createHmac('sha256', key).update(payload).digest('hex');
208
+ return {
209
+ valid: expected === receipt.receiptHash,
210
+ sealIntegrity: {
211
+ model: modelSeal.sha256 === receipt.modelSealSha,
212
+ benchmark: benchmarkSeal.sha256 === receipt.benchmarkSealSha,
213
+ },
214
+ };
215
+ }
216
+
217
+ function isCliEntrypoint() {
218
+ return require.main === module;
219
+ }
220
+
221
+ function main() {
222
+ const modelSeal = sealAsset('model', 'agent-config-v3 with valuation skill', 'provider');
223
+ const benchmarkSeal = sealAsset(
224
+ 'benchmark',
225
+ JSON.stringify(['Value MSFT', 'What is my risk exposure?']),
226
+ 'mlcommons',
227
+ );
228
+ const enclave = createEnclave(modelSeal, benchmarkSeal);
229
+ const result = runEvaluation(enclave, modelSeal, benchmarkSeal, ({ questions }) =>
230
+ questions.map((q) => ({ question: q, score: /value/i.test(q) ? 1 : 0.5 })),
231
+ );
232
+ const guard = leakageGuard(JSON.stringify(result.scores), benchmarkSeal);
233
+ const receipt = attest(enclave, result);
234
+ const verification = verifyAttestation(receipt, modelSeal, benchmarkSeal, result.scores);
235
+ process.stdout.write(JSON.stringify({
236
+ honesty: 'deterministic local model of the DeepMind double-blind protocol',
237
+ enclave, result, guard, receipt, verification,
238
+ }, null, 2) + '\n');
239
+ }
240
+
241
+ if (isCliEntrypoint()) main();
242
+
243
+ module.exports = {
244
+ ASSET_KINDS,
245
+ sealAsset,
246
+ createEnclave,
247
+ runEvaluation,
248
+ leakageGuard,
249
+ attest,
250
+ verifyAttestation,
251
+ isCliEntrypoint,
252
+ };
@@ -0,0 +1,259 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * EdotEnv-style RL environment governance gateway (ThumbGate)
5
+ *
6
+ * Competitive transfer from EdotEnv (edotenv.com, Quant Neolab RSI / market RL envs):
7
+ * multi-step agents that research → experiment → verify under a moving difficulty
8
+ * frontier. ThumbGate does NOT train models or ship market simulators.
9
+ *
10
+ * High-ROI controls implemented here:
11
+ * 1. Pre-action interdiction for destructive / secret-egress / unbounded finance tools
12
+ * 2. Reward modifiers (negative) for safety violations (for RL post-train traces)
13
+ * 3. DPO-style preference pairs from blocked vs preferred safe actions
14
+ * 4. Honest non-affiliation: complementary runtime governance for eval agents
15
+ *
16
+ * Positioning: EdotEnv trains/evals agents; ThumbGate gates tool actions they take.
17
+ */
18
+
19
+ const crypto = require('crypto');
20
+ const {
21
+ containsSecret,
22
+ redactSecrets,
23
+ redactSecretsDeep,
24
+ } = require('./secret-redaction');
25
+
26
+ const GOVERNANCE_SOURCE = 'ThumbGate-EdotEnv-Gate';
27
+
28
+ /** Destructive / filesystem / shell wipe patterns */
29
+ const DESTRUCTIVE_PATTERNS = [
30
+ /\brm\s+(-[a-zA-Z]*f[a-zA-Z]*\s+)?\/\b/i,
31
+ /\brm\s+-rf\b/i,
32
+ /\bDROP\s+TABLE\b/i,
33
+ /\bTRUNCATE\s+TABLE\b/i,
34
+ /\bgit\s+reset\s+--hard\b/i,
35
+ /\beval\s*\(/i,
36
+ /\bexec\s*\(/i,
37
+ ];
38
+
39
+ /** Secret / credential egress patterns (underscore-safe; no false \b mid-token) */
40
+ const SECRET_EGRESS_PATTERNS = [
41
+ /STRIPE_SECRET/i,
42
+ /AWS_SECRET/i,
43
+ /OPENAI_API_KEY/i,
44
+ /GH_TOKEN|GITHUB_TOKEN|PRIVATE_KEY/i,
45
+ /curl\b[\s\S]{0,200}(SECRET|TOKEN|API_KEY|PASSWORD)/i,
46
+ /\bcat\s+\/etc\/passwd\b/i,
47
+ /\bbase64\b/i,
48
+ ];
49
+
50
+ /** Unbounded / high-leverage finance actions (demo market stress surface) */
51
+ const FINANCE_RISK_PATTERNS = [
52
+ /place_order/i,
53
+ /withdraw_all/i,
54
+ /leverage["'\s:=]+(\d{2,})/i,
55
+ /amount["'\s:=]+(\d{6,})/i,
56
+ ];
57
+
58
+ /** Cross-tenant / forged session handle signals */
59
+ const TENANT_FORGERY_PATTERNS = [
60
+ /mcp_sess_fake/i,
61
+ /override_tenant_scope/i,
62
+ /mcpSessionHandle["'\s:=]+mcp_h_forged/i,
63
+ ];
64
+
65
+ function flattenParams(params = {}) {
66
+ if (params == null) return '';
67
+ if (typeof params === 'string') return params;
68
+ try {
69
+ return JSON.stringify(params);
70
+ } catch {
71
+ return String(params);
72
+ }
73
+ }
74
+
75
+ function actionText(stepPayload = {}) {
76
+ const params = stepPayload.params || {};
77
+ const parts = [
78
+ stepPayload.toolName || '',
79
+ params.command,
80
+ params.query,
81
+ params.action,
82
+ flattenParams(params),
83
+ ];
84
+ return parts.filter(Boolean).join(' ');
85
+ }
86
+
87
+ function computeActionDigest(toolName, params) {
88
+ const payload = JSON.stringify({
89
+ toolName: String(toolName || ''),
90
+ params: params && typeof params === 'object' ? params : { value: params },
91
+ });
92
+ return crypto.createHash('sha256').update(payload).digest('hex').slice(0, 24);
93
+ }
94
+
95
+ function matchFirst(patterns, text) {
96
+ for (const re of patterns) {
97
+ if (re.test(text)) return re.source;
98
+ }
99
+ return null;
100
+ }
101
+
102
+ /**
103
+ * Strip bare http(s) URLs before canonical secret scan.
104
+ * secret-redaction's assignment pattern treats `https://host/path` as key:value
105
+ * (key=`https`, value=`//host/...`), which false-positives public WebFetch URLs.
106
+ * Literal credential tokens outside the URL still match.
107
+ */
108
+ function textForLiteralSecretScan(text) {
109
+ return String(text || '').replace(/https?:\/\/[^\s"'<>]+/gi, ' ');
110
+ }
111
+
112
+ function hasLiteralSecret(text) {
113
+ return containsSecret(textForLiteralSecretScan(text));
114
+ }
115
+
116
+ /**
117
+ * Evaluate a single agent step for an EdotEnv-style RL / research environment.
118
+ * @param {object} stepPayload
119
+ * @returns {object}
120
+ */
121
+ function evaluateEdotEnvStep(stepPayload = {}) {
122
+ const started = Date.now();
123
+ const agentId = String(stepPayload.agentId || 'agent_anon');
124
+ const environmentId = String(stepPayload.environmentId || 'edotenv_market_v1');
125
+ const toolName = String(stepPayload.toolName || 'exec');
126
+ const params = stepPayload.params && typeof stepPayload.params === 'object'
127
+ ? stepPayload.params
128
+ : {};
129
+ const text = actionText(stepPayload);
130
+ const actionDigest = computeActionDigest(toolName, params);
131
+
132
+ const hits = [];
133
+ const destructive = matchFirst(DESTRUCTIVE_PATTERNS, text);
134
+ if (destructive) hits.push({ class: 'destructive', pattern: destructive, reward: -1.0 });
135
+ const secret = matchFirst(SECRET_EGRESS_PATTERNS, text);
136
+ if (secret || hasLiteralSecret(text)) {
137
+ hits.push({
138
+ class: 'secret_egress',
139
+ pattern: secret || 'canonical_secret_detector',
140
+ reward: -0.9,
141
+ });
142
+ }
143
+ const finance = matchFirst(FINANCE_RISK_PATTERNS, text);
144
+ if (finance) hits.push({ class: 'finance_risk', pattern: finance, reward: -0.85 });
145
+ const forgery = matchFirst(TENANT_FORGERY_PATTERNS, text);
146
+ if (forgery) hits.push({ class: 'tenant_forgery', pattern: forgery, reward: -1.0 });
147
+
148
+ const latencyMs = Date.now() - started;
149
+
150
+ if (hits.length > 0) {
151
+ const rewardModifier = Math.min(...hits.map((h) => h.reward));
152
+ const preferredSafe = preferredSafeAlternative(hits[0].class, toolName);
153
+ return {
154
+ status: 'BLOCKED',
155
+ allowed: false,
156
+ rewardModifier,
157
+ reason: `Blocked by ThumbGate RL firewall (${hits.map((h) => h.class).join(', ')})`,
158
+ hits,
159
+ actionDigest,
160
+ agentId,
161
+ environmentId,
162
+ toolName,
163
+ latencyMs,
164
+ governanceSource: GOVERNANCE_SOURCE,
165
+ dpoPair: {
166
+ rejected: {
167
+ toolName,
168
+ params: redactSecretsDeep(params),
169
+ text: redactSecrets(text.slice(0, 500)),
170
+ },
171
+ preferred: preferredSafe,
172
+ label: hits[0].class,
173
+ },
174
+ disclaimer: 'No affiliation with EdotEnv / Quant Neolab. Complementary governance layer only.',
175
+ };
176
+ }
177
+
178
+ return {
179
+ status: 'ALLOWED',
180
+ allowed: true,
181
+ rewardModifier: 0,
182
+ reason: 'Cleared RL safety boundaries',
183
+ hits: [],
184
+ actionDigest,
185
+ agentId,
186
+ environmentId,
187
+ toolName,
188
+ latencyMs,
189
+ governanceSource: GOVERNANCE_SOURCE,
190
+ disclaimer: 'No affiliation with EdotEnv / Quant Neolab. Complementary governance layer only.',
191
+ };
192
+ }
193
+
194
+ function preferredSafeAlternative(hitClass, toolName) {
195
+ switch (hitClass) {
196
+ case 'destructive':
197
+ return {
198
+ toolName: 'Bash',
199
+ params: { command: 'ls -la' },
200
+ note: 'Prefer non-destructive inspection',
201
+ };
202
+ case 'secret_egress':
203
+ return {
204
+ toolName: toolName || 'WebFetch',
205
+ params: { url: 'https://api.github.com/zen' },
206
+ note: 'Prefer public non-secret API without credential injection',
207
+ };
208
+ case 'finance_risk':
209
+ return {
210
+ toolName: 'query_orderbook',
211
+ params: { symbol: 'AAPL', depth: 5 },
212
+ note: 'Prefer read-only market data under risk limits',
213
+ };
214
+ case 'tenant_forgery':
215
+ return {
216
+ toolName: 'tools/call',
217
+ params: { name: 'search_lessons', arguments: {} },
218
+ note: 'Prefer unscoped read tools without forged handles',
219
+ };
220
+ default:
221
+ return { toolName: 'noop', params: {}, note: 'No-op safe alternative' };
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Batch-evaluate a trajectory of steps (multi-turn RL episode).
227
+ * Returns cumulative reward modifier and first block index.
228
+ */
229
+ function evaluateEdotEnvTrajectory(steps = []) {
230
+ const results = [];
231
+ let cumulativeReward = 0;
232
+ let firstBlockIndex = null;
233
+ for (let i = 0; i < steps.length; i += 1) {
234
+ const r = evaluateEdotEnvStep(steps[i]);
235
+ results.push(r);
236
+ cumulativeReward += Number(r.rewardModifier) || 0;
237
+ if (!r.allowed && firstBlockIndex == null) firstBlockIndex = i;
238
+ }
239
+ return {
240
+ steps: results.length,
241
+ cumulativeReward,
242
+ firstBlockIndex,
243
+ blocked: firstBlockIndex != null,
244
+ results,
245
+ governanceSource: GOVERNANCE_SOURCE,
246
+ };
247
+ }
248
+
249
+ module.exports = {
250
+ GOVERNANCE_SOURCE,
251
+ DESTRUCTIVE_PATTERNS,
252
+ SECRET_EGRESS_PATTERNS,
253
+ FINANCE_RISK_PATTERNS,
254
+ TENANT_FORGERY_PATTERNS,
255
+ evaluateEdotEnvStep,
256
+ evaluateEdotEnvTrajectory,
257
+ computeActionDigest,
258
+ preferredSafeAlternative,
259
+ };
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Ensure hosted production volumes always have a minimal searchable corpus
6
+ * for the deploy proof query ("thumbgate").
7
+ *
8
+ * Railway containers ship without .claude/memory/feedback. Ephemeral /data
9
+ * volumes can leave /v1/search and /v1/lessons/search returning zero hits even
10
+ * when /health is green — which fails GHA "Verify authenticated production
11
+ * behavior" while the release is otherwise live.
12
+ *
13
+ * Idempotent: only appends missing seed rows; never rewrites customer data.
14
+ */
15
+
16
+ const fs = require('node:fs');
17
+ const path = require('node:path');
18
+
19
+ const SEED_ID = 'seed_deploy_proof_thumbgate_v1';
20
+ const SEED_FEEDBACK_ID = 'fb_seed_deploy_proof_thumbgate_v1';
21
+ const SEED_RULE_HEADING = '## ThumbGate deploy-proof prevention seed';
22
+
23
+
24
+ function shouldEnsureProductionSearchCorpus(env = process.env) {
25
+ if (String(env.THUMBGATE_SKIP_SEARCH_CORPUS_SEED || '').trim() === '1') return false;
26
+ if (String(env.THUMBGATE_ENSURE_SEARCH_CORPUS || '').trim() === '1') return true;
27
+ // Hosted deploy surfaces only — never seed local/temp/E2E feedback dirs.
28
+ return Boolean(
29
+ env.RAILWAY_ENVIRONMENT
30
+ || env.RAILWAY_VOLUME_MOUNT_PATH
31
+ || env.K_SERVICE
32
+ || env.CLOUD_RUN_JOB
33
+ );
34
+ }
35
+
36
+ function ensureDir(dirPath) {
37
+ fs.mkdirSync(dirPath, { recursive: true });
38
+ }
39
+
40
+ function readText(filePath) {
41
+ try {
42
+ return fs.readFileSync(filePath, 'utf8');
43
+ } catch {
44
+ return '';
45
+ }
46
+ }
47
+
48
+ function appendLine(filePath, line) {
49
+ ensureDir(path.dirname(filePath));
50
+ fs.appendFileSync(filePath, `${line}\n`, 'utf8');
51
+ }
52
+
53
+ function fileContains(filePath, needle) {
54
+ return readText(filePath).includes(needle);
55
+ }
56
+
57
+ function buildSeedMemory(nowIso) {
58
+ return {
59
+ id: SEED_ID,
60
+ timestamp: nowIso,
61
+ category: 'learning',
62
+ title: 'ThumbGate infrastructure firewall deploy-proof seed',
63
+ content: [
64
+ 'ThumbGate is the infrastructure firewall for AI coding agents.',
65
+ 'Capture feedback, promote lessons, generate prevention rules, and block known-bad tool calls.',
66
+ 'This seed keeps production /v1/search and /v1/lessons/search non-empty for the thumbgate proof query.',
67
+ ].join(' '),
68
+ whatWorked: 'Keep a durable searchable ThumbGate seed so deploy authenticated proof can verify retrieval.',
69
+ whatToChange: 'Do not delete seed_deploy_proof_thumbgate_v1 from the production feedback volume.',
70
+ tags: ['thumbgate', 'deploy-proof', 'seed', 'retrieval'],
71
+ importance: 0.9,
72
+ sourceFeedbackId: SEED_FEEDBACK_ID,
73
+ };
74
+ }
75
+
76
+ function buildSeedFeedback(nowIso) {
77
+ return {
78
+ id: SEED_FEEDBACK_ID,
79
+ timestamp: nowIso,
80
+ signal: 'up',
81
+ feedback: 'up',
82
+ title: 'ThumbGate deploy proof seed feedback',
83
+ context: 'ThumbGate production must keep a searchable thumbgate corpus for authenticated deploy proof.',
84
+ whatWorked: 'Seed prevention rules and memory so hybrid-parent-child search returns at least one hit.',
85
+ tags: ['thumbgate', 'deploy-proof', 'seed'],
86
+ };
87
+ }
88
+
89
+ function buildSeedRulesBlock() {
90
+ return [
91
+ SEED_RULE_HEADING,
92
+ '',
93
+ '- ALWAYS keep a minimal ThumbGate-searchable corpus on the production feedback volume.',
94
+ '- NEVER claim production retrieval is healthy when /v1/search?q=thumbgate returns zero results.',
95
+ '- Deploy proof depends on hybrid-parent-child search + lesson evidence for the query "thumbgate".',
96
+ '',
97
+ ].join('\n');
98
+ }
99
+
100
+ /**
101
+ * @param {object} options
102
+ * @param {string} options.feedbackDir
103
+ * @param {string} [options.nowIso]
104
+ * @returns {{feedbackDir:string,wrote:{memory:boolean,feedback:boolean,rules:boolean},paths:object}}
105
+ */
106
+ function ensureProductionSearchCorpus(options = {}) {
107
+ const feedbackDir = path.resolve(String(options.feedbackDir || '').trim());
108
+ if (!feedbackDir) {
109
+ throw new Error('feedbackDir is required');
110
+ }
111
+
112
+ const nowIso = options.nowIso || new Date().toISOString();
113
+ ensureDir(feedbackDir);
114
+
115
+ const paths = {
116
+ memoryLog: path.join(feedbackDir, 'memory-log.jsonl'),
117
+ feedbackLog: path.join(feedbackDir, 'feedback-log.jsonl'),
118
+ preventionRules: path.join(feedbackDir, 'prevention-rules.md'),
119
+ };
120
+
121
+ const wrote = { memory: false, feedback: false, rules: false };
122
+
123
+ if (!fileContains(paths.memoryLog, SEED_ID)) {
124
+ appendLine(paths.memoryLog, JSON.stringify(buildSeedMemory(nowIso)));
125
+ wrote.memory = true;
126
+ }
127
+
128
+ if (!fileContains(paths.feedbackLog, SEED_FEEDBACK_ID)) {
129
+ appendLine(paths.feedbackLog, JSON.stringify(buildSeedFeedback(nowIso)));
130
+ wrote.feedback = true;
131
+ }
132
+
133
+ if (!fileContains(paths.preventionRules, SEED_RULE_HEADING)) {
134
+ const existing = readText(paths.preventionRules);
135
+ const next = existing
136
+ ? `${existing.replace(/\s*$/, '')}\n\n${buildSeedRulesBlock()}`
137
+ : `# Prevention Rules\n\n${buildSeedRulesBlock()}`;
138
+ ensureDir(path.dirname(paths.preventionRules));
139
+ fs.writeFileSync(paths.preventionRules, next, 'utf8');
140
+ wrote.rules = true;
141
+ }
142
+
143
+ return { feedbackDir, wrote, paths, seedId: SEED_ID };
144
+ }
145
+
146
+ module.exports = {
147
+ SEED_ID,
148
+ SEED_FEEDBACK_ID,
149
+ SEED_RULE_HEADING,
150
+ shouldEnsureProductionSearchCorpus,
151
+ ensureProductionSearchCorpus,
152
+ };
153
+
154
+ if (require.main === module) {
155
+ const feedbackDir = process.argv[2] || process.env.THUMBGATE_FEEDBACK_DIR;
156
+ if (!feedbackDir) {
157
+ console.error('Usage: node scripts/ensure-production-search-corpus.js <feedbackDir>');
158
+ process.exit(2);
159
+ }
160
+ const result = ensureProductionSearchCorpus({ feedbackDir });
161
+ console.log(JSON.stringify(result, null, 2));
162
+ }