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.
- package/.agents/skills/cyberstrike-compare-not-clone/SKILL.md +36 -0
- package/.agents/skills/gitlab-sandbox-allowlist-not-trust/SKILL.md +77 -0
- package/.agents/skills/jit-harness-compare-not-clone/SKILL.md +34 -0
- package/.agents/skills/openui-catalog-compose-honesty/SKILL.md +64 -0
- package/.agents/skills/zvec-grep-compare-not-clone/SKILL.md +34 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.well-known/llms.txt +1 -0
- package/.well-known/mcp/server-card.json +1 -1
- package/CONTRIBUTING.md +95 -0
- package/README.md +195 -632
- package/THIRD_PARTY_NOTICES.md +89 -0
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/forge/forge.yaml +3 -3
- package/adapters/future-agi/.mcp.json +8 -0
- package/adapters/future-agi/FUTURE_AGI.md +23 -0
- package/adapters/future-agi/config.toml +3 -0
- package/adapters/future-agi/future-agi-bridge.js +9 -0
- package/adapters/future-agi/opencode.json +8 -0
- package/adapters/herdr/herdr-plugin.toml +18 -0
- package/adapters/mcp/server-stdio.js +238 -25
- package/adapters/opencode/opencode.json +1 -1
- package/adapters/workos/WORKOS.md +52 -0
- package/bin/cli.js +336 -3
- package/bin/futureagi-bridge +9 -0
- package/config/gate-templates.json +581 -4
- package/config/gates/actor-critic-audit.json +34 -0
- package/config/gates/default.json +9 -3
- package/config/gates/five-walls-governance.json +34 -0
- package/config/gates/future-agi-guardrails.json +34 -0
- package/config/gates/radware-threat-defense-2026.json +61 -0
- package/config/gates/simatree-data-governance.json +33 -0
- package/config/mcp-allowlists.json +4 -0
- package/config/merge-quality-checks.json +6 -0
- package/config/model-candidates.json +312 -29
- package/config/model-tiers.json +18 -0
- package/config/post-deploy-marketing-pages.json +10 -0
- package/config/progressive/01-wire-only.json +11 -0
- package/config/progressive/02-dashboard-empty-ok.json +10 -0
- package/config/progressive/03-one-lesson.json +10 -0
- package/config/progressive/04-warn-fires.json +11 -0
- package/config/progressive/05-strict-optional.json +11 -0
- package/config/progressive/README.md +15 -0
- package/config/schemas/broker-execution-receipt.schema.json +139 -0
- package/config/schemas/provider-execution-attestation-v1.schema.json +58 -0
- package/conformance/provider-attestation/vectors.json +320 -0
- package/docs/specs/provider-execution-attestation-v1.md +69 -0
- package/openapi/openapi.yaml +15 -0
- package/package.json +390 -148
- package/public/about.html +2 -2
- package/public/ai-malpractice-prevention.html +7 -7
- package/public/blog/a-10-dollar-vps-is-not-a-computer.html +143 -0
- package/public/blog/a-receipt-is-not-world-state.html +388 -0
- package/public/blog/git-at-agent-scale.html +374 -0
- package/public/blog/no-llm-in-the-gate.html +133 -0
- package/public/blog.html +80 -0
- package/public/case-studies.html +16 -1
- package/public/compare.html +28 -0
- package/public/diagnostic.html +216 -7
- package/public/docs/connectors.html +39 -0
- package/public/federal.html +2 -2
- package/public/founders.html +639 -0
- package/public/index.html +87 -9
- package/public/install.html +8 -8
- package/public/learn.html +39 -0
- package/public/numbers.html +2 -2
- package/public/peter.html +310 -0
- package/public/platform-partners.html +119 -0
- package/public/pricing.html +24 -3
- package/public/privacy.html +117 -0
- package/public/pro.html +17 -0
- package/public/support.html +62 -0
- package/public/terms.html +130 -0
- package/public/third-party-notices.html +95 -0
- package/public/yt.html +351 -0
- package/scripts/action-receipts.js +133 -3
- package/scripts/adaptive-governance-arena.js +349 -0
- package/scripts/admin-override.js +205 -0
- package/scripts/agent-action-inventory.js +869 -0
- package/scripts/agent-audit-trace.js +42 -2
- package/scripts/agent-egress-policy.js +1117 -0
- package/scripts/agent-memory-lifecycle.js +141 -2
- package/scripts/agent-operations-planner.js +441 -1
- package/scripts/agent-readiness.js +68 -0
- package/scripts/agent-security-central.js +647 -0
- package/scripts/allowlist-bridge-honesty.js +417 -0
- package/scripts/async-job-runner.js +102 -11
- package/scripts/audit-trail.js +212 -0
- package/scripts/billing.js +1 -1
- package/scripts/broker-execution-receipts.js +719 -0
- package/scripts/budget-aware-gates-proof.js +423 -0
- package/scripts/claude-feedback-sync.js +29 -3
- package/scripts/claw-harness-production.js +237 -0
- package/scripts/cli-schema.js +163 -1
- package/scripts/codex-runbook-flywheel.js +318 -0
- package/scripts/context-footprint.js +186 -0
- package/scripts/contextfs.js +143 -61
- package/scripts/dashboard.js +251 -32
- package/scripts/deepseek-v4-runtime-guardrails.js +72 -6
- package/scripts/docker-sandbox-planner.js +18 -0
- package/scripts/double-blind-eval-protocol.js +252 -0
- package/scripts/edotenv-rl-gateway.js +259 -0
- package/scripts/ensure-production-search-corpus.js +162 -0
- package/scripts/eval-holdout.js +311 -0
- package/scripts/feedback-aggregate.js +21 -2
- package/scripts/feedback-loop.js +87 -5
- package/scripts/feedback-quality.js +9 -0
- package/scripts/file-ledger-lock.js +4 -1
- package/scripts/financial-control-plane.js +41 -1
- package/scripts/find-dormant-requires.js +118 -0
- package/scripts/fs-utils.js +84 -8
- package/scripts/gates-engine.js +810 -63
- package/scripts/generate-case-study-outreach.js +24 -15
- package/scripts/git-at-scale.js +628 -0
- package/scripts/governance-conflict-audit.js +1650 -0
- package/scripts/governance-difficulty-curriculum.js +328 -0
- package/scripts/graphrag-retrieval.js +275 -0
- package/scripts/gurobi-optimizer.js +324 -0
- package/scripts/gurobi_optimizer.py +485 -0
- package/scripts/harness-selector.js +82 -1
- package/scripts/hidden-entry-points.js +284 -0
- package/scripts/human-escalation.js +199 -1
- package/scripts/hybrid-feedback-context.js +152 -19
- package/scripts/intent-governed-execution.js +602 -0
- package/scripts/intervention-policy.js +123 -20
- package/scripts/jit-harness-compose.js +628 -0
- package/scripts/jsonl-watcher.js +10 -0
- package/scripts/lesson-embedding-index.js +95 -12
- package/scripts/lesson-retrieval.js +105 -19
- package/scripts/local-model-profile.js +19 -2
- package/scripts/mailer/resend-mailer.js +1 -1
- package/scripts/matryoshka-embedding.js +235 -0
- package/scripts/mcp-oauth.js +42 -4
- package/scripts/mcp-session-handles.js +1016 -0
- package/scripts/mcp-wiring-doctor.js +314 -0
- package/scripts/memory-firewall.js +115 -2
- package/scripts/memory-scope-readiness.js +299 -0
- package/scripts/memory-vs-rag-route.js +161 -0
- package/scripts/model-tier-router.js +148 -21
- package/scripts/nvidia-specdecode-al-doctor.js +536 -0
- package/scripts/openui-catalog-compose-honesty.js +593 -0
- package/scripts/operational-integrity.js +19 -1
- package/scripts/override-audit.js +213 -0
- package/scripts/package-manager-honesty-doctor.js +458 -0
- package/scripts/pr-manager.js +63 -1
- package/scripts/prove-herdr-adapter.js +52 -0
- package/scripts/prove-memory-pyramid-and-symbolic-canvas.js +95 -0
- package/scripts/prove-workos.js +73 -0
- package/scripts/provider-attestation-conformance.js +192 -0
- package/scripts/provider-receipt-contract.js +136 -0
- package/scripts/qwen38-max-cost-optimizer.js +401 -0
- package/scripts/radware-threat-defense.js +280 -0
- package/scripts/rag-embedding-identity.js +221 -0
- package/scripts/rag-precision-guardrails.js +112 -2
- package/scripts/remote-feedback-capture.js +159 -0
- package/scripts/research-agent-harness.js +256 -0
- package/scripts/rsi-safety-hillclimb.js +200 -0
- package/scripts/rule-sprawl.js +188 -0
- package/scripts/schedule-manager.js +147 -0
- package/scripts/self-heal.js +8 -0
- package/scripts/session-lease.js +415 -0
- package/scripts/simatree-data-governance.js +347 -0
- package/scripts/slo-alert-engine.js +172 -7
- package/scripts/solver-parity.js +539 -0
- package/scripts/stealth-memory-injection-gate.js +333 -0
- package/scripts/switchyard-router.js +366 -0
- package/scripts/telemetry-analytics.js +84 -27
- package/scripts/temporal-decay-weighting.js +138 -0
- package/scripts/test-all.js +165 -0
- package/scripts/token-savings.js +42 -0
- package/scripts/tool-kpi-tracker.js +108 -5
- package/scripts/tool-registry.js +193 -5
- package/scripts/universal-claim-evaluator.js +14 -2
- package/scripts/vector-store.js +279 -9
- package/scripts/workflow-notebook.js +391 -0
- package/scripts/workflow-sentinel.js +111 -12
- package/scripts/workos-production-guard.js +260 -0
- package/scripts/workspace-search-route.js +515 -0
- package/server.json +2 -2
- package/src/agent-identity-boundary.js +76 -0
- package/src/agent-retrieval-cache.js +155 -0
- package/src/alert-noise-ledger.js +502 -0
- package/src/api/server.js +724 -153
- package/src/git-fast-cache.js +220 -0
- package/src/git-wal-sync.js +156 -0
- package/src/hash-anchored-edit.js +82 -0
- package/src/hermes-platform-protocol.js +475 -0
- package/src/hermes-sync-plane.js +241 -0
- package/src/index.js +30 -1
- package/src/iso42001-compliance-guard.js +97 -0
- package/src/latency-budget.js +244 -0
- package/src/mcp-writeguard.js +316 -0
- package/src/miminions-adapter.js +106 -0
- package/src/pipeline-compass.js +104 -0
- package/src/ppl-alert-pipeline.js +284 -0
- package/src/rendezvous-router.js +90 -0
- 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
|
+
}
|