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
@@ -0,0 +1,515 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Workspace search route doctor — zg (zvec-grep) FORMAT steal.
6
+ *
7
+ * Maps Qwen/zvec-ai's four local-first routes onto EXISTING ThumbGate rails:
8
+ * --rg → ripgrep / Grep (exact / regex)
9
+ * --fts → filesystem-search / BM25F lesson retrieval (lexical)
10
+ * --vector → LanceDB / semantic lesson retrieval (optional dense)
11
+ * --hybrid → pragmatic-hybrid-search RRF (default)
12
+ * --graph → Graphify AST graph (architecture / causality)
13
+ *
14
+ * Does NOT install @zvec/zvec-grep, rebuild their index, or ship a new SKU.
15
+ * Remote embeddings stay fail-closed unless THUMBGATE_ALLOW_REMOTE_EMBED=1.
16
+ *
17
+ * Source inspiration (not affiliated):
18
+ * https://github.com/zvec-ai/zvec-grep
19
+ * https://www.marktechpost.com/2026/09/02/qwen-developers-open-sources-zg-zvec-grep-a-local-first-search-layer-unifying-ripgrep-bm25-and-vector-search/
20
+ */
21
+
22
+ const fs = require('node:fs');
23
+ const path = require('node:path');
24
+ const { spawnSync } = require('node:child_process');
25
+
26
+ const SOURCE_URL = 'https://github.com/zvec-ai/zvec-grep';
27
+ const ROUTES = Object.freeze(['hybrid', 'fts', 'vector', 'rg', 'graph']);
28
+
29
+ const ROUTE_RAILS = Object.freeze({
30
+ rg: {
31
+ label: 'exact / regex',
32
+ rails: ['rg', 'Grep/Glob'],
33
+ when: 'Known symbol, path, error string, or regex.',
34
+ },
35
+ fts: {
36
+ label: 'BM25 / lexical',
37
+ rails: ['filesystem-search', 'lesson-retrieval BM25F', 'thumbgate search'],
38
+ when: 'Known vocabulary; want ranked lexical hits without embeddings.',
39
+ },
40
+ vector: {
41
+ label: 'semantic / dense',
42
+ rails: ['LanceDB lesson vectors', 'lesson-semantic-retrieval'],
43
+ when: 'Paraphrase / intent with little lexical overlap; local embedder available.',
44
+ },
45
+ hybrid: {
46
+ label: 'RRF hybrid (default)',
47
+ rails: ['pragmatic-hybrid-search', 'lesson-retrieval reciprocalRankFusion'],
48
+ when: 'Default agent query: fuse lexical + optional dense with matchedBy provenance.',
49
+ },
50
+ graph: {
51
+ label: 'AST / causality',
52
+ rails: ['graphify query/path/explain'],
53
+ when: 'Architecture / what-connects-X-to-Y; graphify-out/graph.json present.',
54
+ },
55
+ });
56
+
57
+ function normalizeBoolean(value) {
58
+ if (value === true || value === 1) return true;
59
+ if (value === false || value === 0 || value == null) return false;
60
+ const s = String(value).trim().toLowerCase();
61
+ return s === '1' || s === 'true' || s === 'yes' || s === 'on';
62
+ }
63
+
64
+ function normalizeRoute(raw) {
65
+ if (raw == null || raw === '') return null;
66
+ const s = String(raw).trim().toLowerCase().replace(/^--+/, '');
67
+ if (s === 'bm25' || s === 'lexical') return 'fts';
68
+ if (s === 'semantic' || s === 'dense' || s === 'embed') return 'vector';
69
+ if (s === 'regex' || s === 'ripgrep' || s === 'grep') return 'rg';
70
+ if (s === 'ast' || s === 'graphify') return 'graph';
71
+ if (ROUTES.includes(s)) return s;
72
+ return null;
73
+ }
74
+
75
+ function classifyQuery(queryText) {
76
+ const q = String(queryText || '').trim();
77
+ const reasons = [];
78
+ if (!q) {
79
+ return { route: 'hybrid', confidence: 0.4, reasons: ['empty query → hybrid default'] };
80
+ }
81
+
82
+ // Exact / regex signals
83
+ if (/[.*+?^${}()|[\]\\]/.test(q) && /\b(rg|regex|grep)\b/i.test(q) === false) {
84
+ // bare regex-ish tokens often mean exact
85
+ if (/^[\w./:@-]+$/.test(q) === false && /[.()*?[\]|]/.test(q)) {
86
+ reasons.push('regex metacharacters → prefer rg');
87
+ return { route: 'rg', confidence: 0.85, reasons };
88
+ }
89
+ }
90
+ if (
91
+ /^(function|class|const|let|var|export|import)\s+\w+/i.test(q)
92
+ || /^[A-Za-z_][\w.]*\(/i.test(q)
93
+ || /\.[jt]sx?:?\d+/.test(q)
94
+ || /^[\w-]+\.(js|ts|mjs|cjs|json|md|yml|yaml)$/i.test(q)
95
+ ) {
96
+ reasons.push('symbol/path shaped → rg');
97
+ return { route: 'rg', confidence: 0.9, reasons };
98
+ }
99
+ if (/\b(how does|what connects|call graph|depends on|imports|architecture)\b/i.test(q)) {
100
+ reasons.push('architecture / causality phrasing → graph');
101
+ return { route: 'graph', confidence: 0.8, reasons };
102
+ }
103
+ if (/\b(similar to|conceptually|semantically|paraphrase|meaning of)\b/i.test(q)) {
104
+ reasons.push('semantic phrasing → vector');
105
+ return { route: 'vector', confidence: 0.75, reasons };
106
+ }
107
+ if (/\b(exact|literal|string|error message|stack trace)\b/i.test(q)) {
108
+ reasons.push('exact-match cue → rg');
109
+ return { route: 'rg', confidence: 0.8, reasons };
110
+ }
111
+ if (/\b(BM25|full[- ]?text|fts|keyword)\b/i.test(q)) {
112
+ reasons.push('lexical cue → fts');
113
+ return { route: 'fts', confidence: 0.8, reasons };
114
+ }
115
+
116
+ // Natural language without symbols → hybrid
117
+ const words = q.split(/\s+/).filter(Boolean);
118
+ if (words.length >= 4) {
119
+ reasons.push('multi-word natural language → hybrid RRF');
120
+ return { route: 'hybrid', confidence: 0.7, reasons };
121
+ }
122
+ reasons.push('default → hybrid RRF');
123
+ return { route: 'hybrid', confidence: 0.55, reasons };
124
+ }
125
+
126
+ function detectGraphify(rootDir) {
127
+ const graph = path.join(rootDir, 'graphify-out', 'graph.json');
128
+ const bin = path.join(rootDir, '.graphify-venv', 'bin', 'graphify');
129
+ return {
130
+ graphExists: fs.existsSync(graph),
131
+ binExists: fs.existsSync(bin),
132
+ graphPath: graph,
133
+ binPath: bin,
134
+ };
135
+ }
136
+
137
+ function detectRemoteEmbedGrant(env = process.env) {
138
+ return normalizeBoolean(env.THUMBGATE_ALLOW_REMOTE_EMBED)
139
+ || normalizeBoolean(env.THUMBGATE_ALLOW_REMOTE_EMBEDDING);
140
+ }
141
+
142
+ function buildFindings({ route, graphify, allowRemoteEmbed, forceRemote }) {
143
+ const findings = [];
144
+ if (route === 'graph' && !graphify.graphExists) {
145
+ findings.push({
146
+ id: 'graphify_graph_missing',
147
+ severity: 'warn',
148
+ message: 'graphify-out/graph.json missing. Run `npm run graphify:setup` before --graph, or fall back to hybrid.',
149
+ });
150
+ }
151
+ if (route === 'vector' && forceRemote && !allowRemoteEmbed) {
152
+ findings.push({
153
+ id: 'remote_embed_ungranted',
154
+ severity: 'fail',
155
+ message: 'Remote embedding requested without grant. Set THUMBGATE_ALLOW_REMOTE_EMBED=1 (explicit) or use local LanceDB / hybrid without remote.',
156
+ });
157
+ }
158
+ if (forceRemote && !allowRemoteEmbed) {
159
+ findings.push({
160
+ id: 'remote_embed_policy',
161
+ severity: 'fail',
162
+ message: 'zg-style local-first policy: remote embeddings require an explicit grant (THUMBGATE_ALLOW_REMOTE_EMBED=1).',
163
+ });
164
+ }
165
+ return findings;
166
+ }
167
+
168
+ function resolveRipgrepBin() {
169
+ // Sonar S4036: do not spawn bare "rg" from PATH. Prefer fixed absolute bins.
170
+ const candidates = [
171
+ process.env.THUMBGATE_RG_BIN,
172
+ '/opt/homebrew/bin/rg',
173
+ '/usr/local/bin/rg',
174
+ '/usr/bin/rg',
175
+ ].filter(Boolean);
176
+ for (const candidate of candidates) {
177
+ try {
178
+ if (fs.existsSync(candidate)) return candidate;
179
+ } catch {
180
+ // ignore
181
+ }
182
+ }
183
+ return null;
184
+ }
185
+
186
+ function runRipgrep(rootDir, query, limit) {
187
+ const rgBin = resolveRipgrepBin();
188
+ if (!rgBin) {
189
+ return { ok: false, error: 'rg not found at fixed absolute paths (/opt/homebrew/bin/rg, /usr/local/bin/rg, /usr/bin/rg)', hits: [] };
190
+ }
191
+ const rg = spawnSync(
192
+ rgBin,
193
+ ['-n', '--max-count', String(Math.max(1, limit)), '--', query, '.'],
194
+ { cwd: rootDir, encoding: 'utf8', timeout: 15000 }
195
+ );
196
+ if (rg.error && rg.error.code === 'ENOENT') {
197
+ return { ok: false, error: `rg binary missing at ${rgBin}`, hits: [] };
198
+ }
199
+ const lines = String(rg.stdout || '')
200
+ .split(/\r?\n/)
201
+ .filter(Boolean)
202
+ .slice(0, limit)
203
+ .map((line) => {
204
+ const m = /^([^:]+):(\d+):(.*)$/.exec(line);
205
+ if (!m) return { path: null, line: null, text: line, matchedBy: ['rg'] };
206
+ return {
207
+ path: m[1],
208
+ line: Number(m[2]),
209
+ text: m[3].slice(0, 240),
210
+ matchedBy: ['rg'],
211
+ };
212
+ });
213
+ return { ok: rg.status === 0 || rg.status === 1, hits: lines, exitCode: rg.status };
214
+ }
215
+
216
+ function runFilesystemFts(rootDir, query, limit) {
217
+ try {
218
+ const { searchFeedbackLog } = require('./filesystem-search');
219
+ // Signature: searchFeedbackLog(queryText, limit = 5, options = {})
220
+ const rows = searchFeedbackLog(
221
+ query,
222
+ limit,
223
+ { feedbackDir: path.join(rootDir, '.claude', 'memory', 'feedback') }
224
+ );
225
+ return {
226
+ ok: true,
227
+ hits: (rows || []).slice(0, limit).map((r) => ({
228
+ id: r.id || null,
229
+ score: r._score || r.score || null,
230
+ text: String(r.context || r.title || '').slice(0, 240),
231
+ matchedBy: ['fts'],
232
+ matchedTokens: r._matchedTokens || [],
233
+ })),
234
+ };
235
+ } catch (err) {
236
+ return { ok: false, error: err.message, hits: [] };
237
+ }
238
+ }
239
+
240
+ async function runVector(rootDir, query, limit, options = {}) {
241
+ if (options.forceRemote && !options.allowRemoteEmbed) {
242
+ return { ok: false, error: 'remote embed refused (no grant)', hits: [] };
243
+ }
244
+ try {
245
+ const vectorStore = require('./vector-store');
246
+ if (typeof vectorStore.searchSimilar !== 'function') {
247
+ return { ok: false, error: 'vector-store.searchSimilar unavailable', hits: [] };
248
+ }
249
+ const rows = await vectorStore.searchSimilar(query, limit);
250
+ return {
251
+ ok: true,
252
+ hits: (rows || []).slice(0, limit).map((r) => ({
253
+ id: r.id || null,
254
+ score: r._distance != null ? r._distance : (r.score || null),
255
+ text: String(r.context || r.title || r.text || '').slice(0, 240),
256
+ matchedBy: ['vector'],
257
+ })),
258
+ };
259
+ } catch (err) {
260
+ return { ok: false, error: err.message, hits: [] };
261
+ }
262
+ }
263
+
264
+ function runHybrid(rootDir, query, limit) {
265
+ try {
266
+ const { pragmaticHybridSearch } = require('./pragmatic-hybrid-search');
267
+ // Minimal empty corpus still exercises route wiring; callers pass lessons in production.
268
+ const { results, meta } = pragmaticHybridSearch({
269
+ query,
270
+ lexical: [],
271
+ dense: [],
272
+ limit,
273
+ });
274
+ return {
275
+ ok: true,
276
+ hits: (results || []).slice(0, limit).map((r) => ({
277
+ id: r.id || null,
278
+ score: r.score || null,
279
+ matchedBy: ['hybrid', ...(r.hybridFeatures ? Object.keys(r.hybridFeatures) : [])],
280
+ text: String(r.title || r.context || '').slice(0, 240),
281
+ })),
282
+ meta: meta || null,
283
+ note: 'Empty lexical/dense lists in doctor dry-run; production lesson-retrieval supplies ranked ids.',
284
+ };
285
+ } catch (err) {
286
+ return { ok: false, error: err.message, hits: [] };
287
+ }
288
+ }
289
+
290
+ function normalizeOptions(options = {}) {
291
+ const rootDir = path.resolve(String(options.root || options.cwd || process.cwd()));
292
+ const explicit = normalizeRoute(options.route || options.r || null)
293
+ || (normalizeBoolean(options.rg) && 'rg')
294
+ || (normalizeBoolean(options.fts) && 'fts')
295
+ || (normalizeBoolean(options.vector) && 'vector')
296
+ || (normalizeBoolean(options.hybrid) && 'hybrid')
297
+ || (normalizeBoolean(options.graph) && 'graph')
298
+ || null;
299
+ return {
300
+ rootDir,
301
+ query: options.query || options.q || options._?.[0] || '',
302
+ route: explicit,
303
+ limit: Math.min(50, Math.max(1, Number(options.limit || 10) || 10)),
304
+ execute: normalizeBoolean(options.execute) || normalizeBoolean(options.run),
305
+ forceRemote: normalizeBoolean(options['force-remote']) || normalizeBoolean(options.remote),
306
+ allowRemoteEmbed: detectRemoteEmbedGrant(options.env || process.env),
307
+ json: normalizeBoolean(options.json),
308
+ strict: normalizeBoolean(options.strict),
309
+ mapOnly: normalizeBoolean(options.map) || normalizeBoolean(options['map-only']),
310
+ };
311
+ }
312
+
313
+ async function buildWorkspaceSearchRouteReport(rawOptions = {}) {
314
+ const options = normalizeOptions(rawOptions);
315
+ const classified = classifyQuery(options.query);
316
+ const route = options.route || classified.route;
317
+ const graphify = detectGraphify(options.rootDir);
318
+ const findings = buildFindings({
319
+ route,
320
+ graphify,
321
+ allowRemoteEmbed: options.allowRemoteEmbed,
322
+ forceRemote: options.forceRemote,
323
+ });
324
+
325
+ const failCount = findings.filter((f) => f.severity === 'fail').length;
326
+ const warnCount = findings.filter((f) => f.severity === 'warn').length;
327
+ let status = 'ready';
328
+ if (failCount > 0) status = 'fail';
329
+ else if (warnCount > 0) status = 'actionable';
330
+
331
+ const rail = ROUTE_RAILS[route] || ROUTE_RAILS.hybrid;
332
+ const report = {
333
+ name: 'thumbgate-workspace-search-route',
334
+ status,
335
+ source: SOURCE_URL,
336
+ disclaimer:
337
+ 'FORMAT steal from Qwen/zvec-ai zg (zvec-grep): one interface for rg + BM25 + vector (+ graph). Not affiliated; does not install @zvec/zvec-grep or rebuild their index.',
338
+ rootDir: options.rootDir,
339
+ query: options.query,
340
+ route,
341
+ routeLabel: rail.label,
342
+ confidence: options.route ? 1 : classified.confidence,
343
+ reasons: options.route
344
+ ? [`explicit --${route}`]
345
+ : classified.reasons,
346
+ rails: rail.rails,
347
+ when: rail.when,
348
+ routes: ROUTES.map((id) => ({ id, ...ROUTE_RAILS[id] })),
349
+ graphify,
350
+ remoteEmbedGranted: options.allowRemoteEmbed,
351
+ findings,
352
+ metrics: {
353
+ routeCount: ROUTES.length,
354
+ classifiedRoute: classified.route,
355
+ explicitRoute: Boolean(options.route),
356
+ },
357
+ nextCommands: [
358
+ route === 'rg' ? `rg -n -- '${options.query || '<pattern>'}' .` : null,
359
+ route === 'fts' ? 'npx thumbgate search --source=all --json "<query>"' : null,
360
+ route === 'hybrid' ? 'node -e "require(\'./scripts/pragmatic-hybrid-search\')" # via lesson-retrieval' : null,
361
+ route === 'graph' ? '.graphify-venv/bin/graphify query "<architecture question>"' : null,
362
+ route === 'vector' ? 'npx thumbgate lessons --local --json # semantic path when LanceDB present' : null,
363
+ 'npx thumbgate workspace-search-route --query="..." --json',
364
+ ].filter(Boolean),
365
+ };
366
+
367
+ if (options.mapOnly || !options.execute) {
368
+ return report;
369
+ }
370
+
371
+ // Execute against local rails (best-effort; never calls remote embed without grant).
372
+ let execution = { route, ok: false };
373
+ if (route === 'rg') {
374
+ execution = { route, ...runRipgrep(options.rootDir, options.query, options.limit) };
375
+ } else if (route === 'fts') {
376
+ execution = { route, ...runFilesystemFts(options.rootDir, options.query, options.limit) };
377
+ } else if (route === 'hybrid') {
378
+ execution = { route, ...runHybrid(options.rootDir, options.query, options.limit) };
379
+ } else if (route === 'vector') {
380
+ execution = {
381
+ route,
382
+ ...(await runVector(options.rootDir, options.query, options.limit, options)),
383
+ };
384
+ } else if (route === 'graph') {
385
+ if (!graphify.binExists || !graphify.graphExists) {
386
+ execution = { route, ok: false, error: 'graphify not ready', hits: [] };
387
+ } else {
388
+ const out = spawnSync(graphify.binPath, ['query', options.query || 'ThumbGate gates'], {
389
+ cwd: options.rootDir,
390
+ encoding: 'utf8',
391
+ timeout: 20000,
392
+ });
393
+ execution = {
394
+ route,
395
+ ok: out.status === 0,
396
+ hits: String(out.stdout || '')
397
+ .split(/\r?\n/)
398
+ .filter(Boolean)
399
+ .slice(0, options.limit)
400
+ .map((text) => ({ text: text.slice(0, 240), matchedBy: ['graph'] })),
401
+ stderr: out.stderr ? String(out.stderr).slice(0, 400) : null,
402
+ };
403
+ }
404
+ }
405
+ report.execution = execution;
406
+ return report;
407
+ }
408
+
409
+ function formatWorkspaceSearchRouteReport(report) {
410
+ const lines = [
411
+ 'ThumbGate Workspace Search Route (zg FORMAT steal)',
412
+ `Status : ${report.status}`,
413
+ `Query : ${report.query || '(none)'}`,
414
+ `Route : ${report.route} (${report.routeLabel}) confidence=${report.confidence}`,
415
+ `Rails : ${(report.rails || []).join(' · ')}`,
416
+ `Reasons : ${(report.reasons || []).join('; ')}`,
417
+ `Remote embed grant: ${report.remoteEmbedGranted ? 'yes' : 'no'}`,
418
+ `Graphify : graph=${report.graphify?.graphExists ? 'yes' : 'no'} bin=${report.graphify?.binExists ? 'yes' : 'no'}`,
419
+ ];
420
+ if (report.findings?.length) {
421
+ lines.push('Findings:');
422
+ for (const f of report.findings) {
423
+ lines.push(` [${f.severity}] ${f.id}: ${f.message}`);
424
+ }
425
+ }
426
+ if (report.execution) {
427
+ lines.push(`Execution: ok=${report.execution.ok} hits=${(report.execution.hits || []).length}`);
428
+ }
429
+ lines.push(`Source : ${report.source}`);
430
+ lines.push(report.disclaimer);
431
+ return `${lines.join('\n')}\n`;
432
+ }
433
+
434
+ function printHelp() {
435
+ process.stdout.write(`Usage: node scripts/workspace-search-route.js [options]
436
+
437
+ zg (zvec-grep) FORMAT steal — route a query to existing ThumbGate rails.
438
+ Does not install @zvec/zvec-grep.
439
+
440
+ Options:
441
+ --query=<text> Query to classify / optionally execute
442
+ --route=hybrid|fts|vector|rg|graph
443
+ --rg | --fts | --vector | --hybrid | --graph
444
+ --execute Run the chosen local rail (best-effort)
445
+ --limit=<n> Max hits when executing (default 10)
446
+ --force-remote Request remote embeddings (requires THUMBGATE_ALLOW_REMOTE_EMBED=1)
447
+ --map-only Print route map only
448
+ --json
449
+ --strict Exit 1 unless status=ready
450
+ --root=<dir>
451
+ `);
452
+ }
453
+
454
+ async function main(argv = process.argv.slice(2)) {
455
+ if (argv.includes('--help') || argv.includes('-h')) {
456
+ printHelp();
457
+ return 0;
458
+ }
459
+ const options = {};
460
+ for (const arg of argv) {
461
+ if (arg === '--json') options.json = true;
462
+ else if (arg === '--strict') options.strict = true;
463
+ else if (arg === '--execute' || arg === '--run') options.execute = true;
464
+ else if (arg === '--map' || arg === '--map-only') options.map = true;
465
+ else if (arg === '--force-remote' || arg === '--remote') options['force-remote'] = true;
466
+ else if (arg === '--rg') options.rg = true;
467
+ else if (arg === '--fts') options.fts = true;
468
+ else if (arg === '--vector') options.vector = true;
469
+ else if (arg === '--hybrid') options.hybrid = true;
470
+ else if (arg === '--graph') options.graph = true;
471
+ else if (arg.startsWith('--query=')) options.query = arg.slice('--query='.length);
472
+ else if (arg.startsWith('--route=')) options.route = arg.slice('--route='.length);
473
+ else if (arg.startsWith('--limit=')) options.limit = arg.slice('--limit='.length);
474
+ else if (arg.startsWith('--root=')) options.root = arg.slice('--root='.length);
475
+ else if (!arg.startsWith('-') && !options.query) options.query = arg;
476
+ }
477
+
478
+ const report = await buildWorkspaceSearchRouteReport(options);
479
+ if (options.json) {
480
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
481
+ } else {
482
+ process.stdout.write(formatWorkspaceSearchRouteReport(report));
483
+ }
484
+ if (options.strict && report.status !== 'ready') return 1;
485
+ if (report.status === 'fail') return 1;
486
+ return 0;
487
+ }
488
+
489
+ if (require.main === module || (
490
+ process.argv[1]
491
+ && path.resolve(process.argv[1]) === path.resolve(__filename)
492
+ )) {
493
+ main().then((code) => {
494
+ process.exitCode = code;
495
+ }).catch((err) => {
496
+ console.error(err);
497
+ process.exitCode = 1;
498
+ });
499
+ }
500
+
501
+ module.exports = {
502
+ ROUTES,
503
+ ROUTE_RAILS,
504
+ classifyQuery,
505
+ normalizeRoute,
506
+ detectGraphify,
507
+ detectRemoteEmbedGrant,
508
+ resolveRipgrepBin,
509
+ runFilesystemFts,
510
+ runVector,
511
+ buildWorkspaceSearchRouteReport,
512
+ formatWorkspaceSearchRouteReport,
513
+ main,
514
+ SOURCE_URL,
515
+ };
package/server.json CHANGED
@@ -8,13 +8,13 @@
8
8
  "source": "github",
9
9
  "url": "https://github.com/IgorGanapolsky/ThumbGate"
10
10
  },
11
- "version": "1.35.0",
11
+ "version": "1.37.1",
12
12
  "packages": [
13
13
  {
14
14
  "registryType": "npm",
15
15
  "registryBaseUrl": "https://registry.npmjs.org",
16
16
  "identifier": "thumbgate",
17
- "version": "1.35.0",
17
+ "version": "1.37.1",
18
18
  "runtimeHint": "npx",
19
19
  "runtimeArguments": [
20
20
  {
@@ -0,0 +1,76 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Agent Identity & Least-Privilege Role Boundary Enforcer.
5
+ *
6
+ * Implements fine-grained agent identity token verification and capability scoping
7
+ * to prevent privilege escalation across sub-agents in autonomous multi-agent fleets.
8
+ */
9
+
10
+ const ROLE_CAPABILITY_MATRIX = Object.freeze({
11
+ researcher: ['fs:read', 'web:search', 'mcp:read', 'telemetry:read'],
12
+ developer: ['fs:read', 'fs:write', 'git:commit', 'git:push_branch', 'tests:run'],
13
+ qa_auditor: ['fs:read', 'tests:run', 'e2e:execute', 'telemetry:read'],
14
+ release_engineer: ['fs:read', 'fs:write', 'git:*', 'ci:deploy', 'npm:publish'],
15
+ admin: ['*'],
16
+ });
17
+
18
+ class AgentIdentityBoundary {
19
+ constructor() {
20
+ this.agentRegistry = new Map();
21
+ }
22
+
23
+ registerAgent({ agentId, role = 'researcher', extraCapabilities = [] } = {}) {
24
+ if (!agentId || typeof agentId !== 'string') {
25
+ throw new Error('agentId is required and must be a string');
26
+ }
27
+
28
+ const baseCapabilities = ROLE_CAPABILITY_MATRIX[role] || ROLE_CAPABILITY_MATRIX.researcher;
29
+ const capabilities = new Set([...baseCapabilities, ...extraCapabilities]);
30
+
31
+ const record = {
32
+ agentId,
33
+ role,
34
+ capabilities: Array.from(capabilities),
35
+ registeredAt: Date.now(),
36
+ };
37
+
38
+ this.agentRegistry.set(agentId, record);
39
+ return record;
40
+ }
41
+
42
+ isAuthorized(agentId, requiredCapability) {
43
+ if (!agentId || !requiredCapability) return false;
44
+ const record = this.agentRegistry.get(agentId);
45
+ if (!record) return false;
46
+
47
+ if (record.capabilities.includes('*')) return true;
48
+ if (record.capabilities.includes(requiredCapability)) return true;
49
+
50
+ // Check wildcard namespace e.g. git:* matches git:commit
51
+ const [ns] = requiredCapability.split(':');
52
+ if (record.capabilities.includes(`${ns}:*`)) return true;
53
+
54
+ return false;
55
+ }
56
+
57
+ verifyAction(agentId, toolName, requiredCapability) {
58
+ const authorized = this.isAuthorized(agentId, requiredCapability);
59
+ const agent = this.agentRegistry.get(agentId) || { agentId, role: 'unregistered' };
60
+
61
+ return {
62
+ allowed: authorized,
63
+ agentId,
64
+ role: agent.role,
65
+ toolName,
66
+ requiredCapability,
67
+ decision: authorized ? 'ALLOW' : 'DENY_PRIVILEGE_VIOLATION',
68
+ timestamp: Date.now(),
69
+ };
70
+ }
71
+ }
72
+
73
+ module.exports = {
74
+ AgentIdentityBoundary,
75
+ ROLE_CAPABILITY_MATRIX,
76
+ };