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,647 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Agent Security Central — free, local, centralized security posture for AI coding agents.
6
+ *
7
+ * Transferable GTM from Oracle Database Security Central (InfoWorld 2026-08-14):
8
+ * free time-boxed security posture product → experience-first wedge.
9
+ * ThumbGate maps that onto agent control planes (not RDBMS):
10
+ * 1. Central posture score (gates + hooks + MCP wiring)
11
+ * 2. Configuration drift (settings hooks vs expected PreToolUse contract)
12
+ * 3. Privileged-tool / access risk (missing high-blast-radius blocks)
13
+ * 4. Sensitive-data + audit evidence (secret denials, audit trail presence)
14
+ * 5. Policy variance (manual vs auto-promoted rules, empty block set)
15
+ *
16
+ * Local report is free forever (OSS). Hosted Pro remains optional self-serve.
17
+ * No paid pilot / enterprise SOW language in this surface.
18
+ */
19
+
20
+ const fs = require('fs');
21
+ const path = require('path');
22
+
23
+ const PKG_ROOT = path.resolve(__dirname, '..');
24
+ const PKG_VERSION = (() => {
25
+ try {
26
+ return require(path.join(PKG_ROOT, 'package.json')).version;
27
+ } catch {
28
+ return '0.0.0';
29
+ }
30
+ })();
31
+
32
+ /** Expected PreToolUse command fragments that prove ThumbGate is on the wire. */
33
+ const EXPECTED_HOOK_MARKERS = [
34
+ 'gate-check',
35
+ 'thumbgate',
36
+ 'PreToolUse',
37
+ ];
38
+
39
+ /**
40
+ * Privileged / high-blast categories every agent security central should cover.
41
+ * Matched case-insensitively against gate id, name, pattern, tags, category.
42
+ */
43
+ const PRIVILEGED_COVERAGE = [
44
+ {
45
+ id: 'secret-exfil',
46
+ label: 'Secret / credential exfiltration',
47
+ match: /secret|exfil|credential|api.?key|token.?leak|password/i,
48
+ },
49
+ {
50
+ id: 'destructive-shell',
51
+ label: 'Destructive shell / force-push',
52
+ match: /force.?push|rm\s*-rf|destructive|git.?clean|hard.?reset|sudo/i,
53
+ },
54
+ {
55
+ id: 'branch-protection',
56
+ label: 'Branch protection / admin merge bypass',
57
+ match: /branch.?protect|admin.?merge|--admin|never-bypass|pr.?approve/i,
58
+ },
59
+ {
60
+ id: 'spend-commerce',
61
+ label: 'Spend / commerce / outbound money',
62
+ match: /spend|commerce|stripe|payment|checkout|outbound.?email/i,
63
+ },
64
+ {
65
+ id: 'production-deploy',
66
+ label: 'Production deploy / live ship',
67
+ match: /deploy|production|railway|prod.?ship|live.?claim/i,
68
+ },
69
+ ];
70
+
71
+ function readJsonIfExists(filePath) {
72
+ try {
73
+ if (!fs.existsSync(filePath)) return null;
74
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
75
+ } catch {
76
+ return { __parseError: true, path: filePath };
77
+ }
78
+ }
79
+
80
+ function readTextIfExists(filePath) {
81
+ try {
82
+ if (!fs.existsSync(filePath)) return '';
83
+ return fs.readFileSync(filePath, 'utf8');
84
+ } catch {
85
+ return '';
86
+ }
87
+ }
88
+
89
+ /** How many trailing audit records the posture scan inspects. */
90
+ const AUDIT_TAIL_LINES = 500;
91
+ /** Read window for the tail scan: enough for AUDIT_TAIL_LINES typical records. */
92
+ const AUDIT_TAIL_BYTES = 1024 * 1024;
93
+
94
+ /**
95
+ * Read the last `maxLines` non-empty lines of a JSONL file without
96
+ * materializing the whole file. Audit logs grow without bound (a 101 MB /
97
+ * 200k-line log previously allocated 200k split entries plus a 200k filtered
98
+ * array just to inspect the final 500 records), so the scan reads at most
99
+ * AUDIT_TAIL_BYTES from the end of the file.
100
+ *
101
+ * @returns {{lines: string[], totalLines: number, truncated: boolean}}
102
+ * `totalLines` is exact when the file fits in the window, otherwise it is a
103
+ * lower bound and `truncated` is true.
104
+ */
105
+ function readTailLines(filePath, maxLines) {
106
+ let fd;
107
+ try {
108
+ const { size } = fs.statSync(filePath);
109
+ if (size === 0) return { lines: [], totalLines: 0, truncated: false };
110
+ const readBytes = Math.min(size, AUDIT_TAIL_BYTES);
111
+ const buf = Buffer.alloc(readBytes);
112
+ fd = fs.openSync(filePath, 'r');
113
+ fs.readSync(fd, buf, 0, readBytes, size - readBytes);
114
+ const truncated = readBytes < size;
115
+ let chunk = buf.toString('utf8');
116
+ // A partial leading record would be misparsed; drop it when truncated.
117
+ if (truncated) {
118
+ const nl = chunk.indexOf('\n');
119
+ chunk = nl === -1 ? '' : chunk.slice(nl + 1);
120
+ }
121
+ const all = chunk.split('\n').filter(Boolean);
122
+ return {
123
+ lines: all.slice(-maxLines),
124
+ totalLines: all.length,
125
+ truncated,
126
+ };
127
+ } catch {
128
+ return { lines: [], totalLines: 0, truncated: false };
129
+ } finally {
130
+ if (fd !== undefined) {
131
+ try {
132
+ fs.closeSync(fd);
133
+ } catch {
134
+ /* already closed */
135
+ }
136
+ }
137
+ }
138
+ }
139
+
140
+ function loadGatesFromFile(filePath) {
141
+ const data = readJsonIfExists(filePath);
142
+ if (!data || data.__parseError) return [];
143
+ if (Array.isArray(data.gates)) return data.gates;
144
+ if (Array.isArray(data)) return data;
145
+ return [];
146
+ }
147
+
148
+ function gateBlob(gate) {
149
+ return [
150
+ gate.id,
151
+ gate.name,
152
+ gate.pattern,
153
+ gate.category,
154
+ gate.description,
155
+ Array.isArray(gate.tags) ? gate.tags.join(' ') : '',
156
+ ]
157
+ .filter(Boolean)
158
+ .join(' ');
159
+ }
160
+
161
+ function collectHooksFromSettings(settings) {
162
+ if (!settings || typeof settings !== 'object' || settings.__parseError) return [];
163
+ const hooks = settings.hooks || {};
164
+ const pre = hooks.PreToolUse || hooks.preToolUse || [];
165
+ const list = Array.isArray(pre) ? pre : [];
166
+ const commands = [];
167
+ for (const entry of list) {
168
+ if (!entry || typeof entry !== 'object') continue;
169
+ const hooksArr = Array.isArray(entry.hooks) ? entry.hooks : [entry];
170
+ for (const h of hooksArr) {
171
+ if (!h || typeof h !== 'object') continue;
172
+ if (typeof h.command === 'string') commands.push(h.command);
173
+ if (typeof h.script === 'string') commands.push(h.script);
174
+ }
175
+ if (typeof entry.command === 'string') commands.push(entry.command);
176
+ }
177
+ // Scan only the selected PreToolUse surface for command strings (Claude Code
178
+ // formats vary). Serializing the whole `hooks` object would let a PostToolUse
179
+ // `thumbgate gate-check` entry masquerade as PreToolUse wiring, hiding real
180
+ // hook drift while tools still execute before the gate runs.
181
+ const raw = JSON.stringify(pre);
182
+ return { commands, raw };
183
+ }
184
+
185
+ function assessHookDrift(projectRoot, homeDir) {
186
+ const candidates = [
187
+ path.join(projectRoot, '.claude', 'settings.json'),
188
+ path.join(projectRoot, '.claude', 'settings.local.json'),
189
+ homeDir ? path.join(homeDir, '.claude', 'settings.json') : null,
190
+ homeDir ? path.join(homeDir, '.claude', 'settings.local.json') : null,
191
+ ].filter(Boolean);
192
+
193
+ const surfaces = [];
194
+ let anyThumbgateHook = false;
195
+ let anyGateCheck = false;
196
+
197
+ for (const filePath of candidates) {
198
+ const settings = readJsonIfExists(filePath);
199
+ if (!settings) {
200
+ surfaces.push({ path: filePath, present: false, markers: [] });
201
+ continue;
202
+ }
203
+ if (settings.__parseError) {
204
+ surfaces.push({ path: filePath, present: true, parseError: true, markers: [] });
205
+ continue;
206
+ }
207
+ const { commands, raw } = collectHooksFromSettings(settings);
208
+ const blob = `${commands.join('\n')}\n${raw}`.toLowerCase();
209
+ const markers = EXPECTED_HOOK_MARKERS.filter((m) => blob.includes(m.toLowerCase()));
210
+ if (markers.includes('thumbgate') || markers.includes('gate-check')) {
211
+ anyThumbgateHook = true;
212
+ }
213
+ if (markers.includes('gate-check')) anyGateCheck = true;
214
+ surfaces.push({
215
+ path: filePath,
216
+ present: true,
217
+ commandCount: commands.length,
218
+ markers,
219
+ });
220
+ }
221
+
222
+ const findings = [];
223
+ if (!anyThumbgateHook) {
224
+ findings.push({
225
+ severity: 'high',
226
+ id: 'hook-drift-missing-thumbgate',
227
+ message:
228
+ 'No PreToolUse hook surface references thumbgate/gate-check — agent actions may run ungated (configuration drift from expected install).',
229
+ });
230
+ } else if (!anyGateCheck) {
231
+ findings.push({
232
+ severity: 'medium',
233
+ id: 'hook-drift-weak-marker',
234
+ message:
235
+ 'ThumbGate mentioned in settings but gate-check marker missing — verify PreToolUse actually invokes the gate runtime.',
236
+ });
237
+ }
238
+
239
+ return {
240
+ anyThumbgateHook,
241
+ anyGateCheck,
242
+ surfaces,
243
+ findings,
244
+ drifted: !anyThumbgateHook,
245
+ };
246
+ }
247
+
248
+ function assessPrivilegedCoverage(gates) {
249
+ const covered = [];
250
+ const missing = [];
251
+ for (const need of PRIVILEGED_COVERAGE) {
252
+ const matching = gates.filter((g) => need.match.test(gateBlob(g)));
253
+ // Only a block-action gate counts as coverage. A warn/log/approve gate is
254
+ // advisory: the privileged operation still executes, so certifying it as
255
+ // coverage would report enforcement that does not exist.
256
+ const hit = matching.find((g) => g.action === 'block');
257
+ if (hit) {
258
+ covered.push({
259
+ id: need.id,
260
+ label: need.label,
261
+ gateId: hit.id || hit.name || null,
262
+ action: hit.action,
263
+ });
264
+ } else {
265
+ missing.push({
266
+ id: need.id,
267
+ label: need.label,
268
+ advisoryOnly: matching.length > 0,
269
+ advisoryGates: matching.map((g) => g.id || g.name || null).filter(Boolean),
270
+ });
271
+ }
272
+ }
273
+ const findings = missing.map((m) => ({
274
+ severity: 'medium',
275
+ id: `privileged-gap-${m.id}`,
276
+ message: m.advisoryOnly
277
+ ? `No block-action gate covers privileged risk: ${m.label} — only advisory (warn/log/approve) gates matched: ${m.advisoryGates.join(', ') || 'unnamed'}. The operation is not prevented.`
278
+ : `No active gate covers privileged risk: ${m.label}`,
279
+ }));
280
+ return { covered, missing, findings };
281
+ }
282
+
283
+ function assessPolicyVariance(manualGates, autoGates) {
284
+ const blockManual = manualGates.filter((g) => g.action === 'block').length;
285
+ const blockAuto = autoGates.filter((g) => g.action === 'block').length;
286
+ const warnManual = manualGates.filter((g) => g.action === 'warn').length;
287
+ const warnAuto = autoGates.filter((g) => g.action === 'warn').length;
288
+ const findings = [];
289
+ if (blockManual + blockAuto === 0) {
290
+ findings.push({
291
+ severity: 'critical',
292
+ id: 'policy-no-blocks',
293
+ message: 'Zero block-action gates configured — posture is allow-by-default (policy variance risk).',
294
+ });
295
+ }
296
+ if (manualGates.length === 0 && autoGates.length === 0) {
297
+ findings.push({
298
+ severity: 'critical',
299
+ id: 'policy-empty',
300
+ message: 'No gates loaded from default or auto-promoted stores.',
301
+ });
302
+ }
303
+ return {
304
+ manualCount: manualGates.length,
305
+ autoCount: autoGates.length,
306
+ blockCount: blockManual + blockAuto,
307
+ warnCount: warnManual + warnAuto,
308
+ findings,
309
+ };
310
+ }
311
+
312
+ function assessSensitiveAudit(projectRoot, homeDir, env = {}) {
313
+ const candidates = [
314
+ env.THUMBGATE_GATE_STATS,
315
+ homeDir ? path.join(homeDir, '.thumbgate', 'gate-stats.json') : null,
316
+ path.join(projectRoot, '.thumbgate', 'gate-stats.json'),
317
+ env.THUMBGATE_AUDIT_LOG,
318
+ homeDir ? path.join(homeDir, '.thumbgate', 'audit-trail.jsonl') : null,
319
+ path.join(projectRoot, '.thumbgate', 'audit-trail.jsonl'),
320
+ path.join(projectRoot, '.claude', 'memory', 'feedback', 'feedback-log.jsonl'),
321
+ ].filter(Boolean);
322
+
323
+ const present = [];
324
+ let secretDenialSignals = 0;
325
+ let auditEvents = 0;
326
+
327
+ for (const filePath of candidates) {
328
+ if (!fs.existsSync(filePath)) continue;
329
+ // A path is only counted as evidence once usable content is established.
330
+ // An empty JSONL file or malformed JSON is not evidence, and counting it
331
+ // would suppress the `audit-missing` remediation with zero usable events.
332
+ if (filePath.endsWith('.jsonl')) {
333
+ const { lines, totalLines } = readTailLines(filePath, AUDIT_TAIL_LINES);
334
+ if (totalLines === 0) continue;
335
+ present.push(filePath);
336
+ auditEvents += totalLines;
337
+ for (const line of lines) {
338
+ if (/secret|exfil|credential|deny|blocked|hard.?block/i.test(line)) {
339
+ secretDenialSignals += 1;
340
+ }
341
+ }
342
+ } else if (filePath.endsWith('.json')) {
343
+ const text = readTextIfExists(filePath);
344
+ if (!text) continue;
345
+ let data;
346
+ try {
347
+ data = JSON.parse(text);
348
+ } catch {
349
+ continue; // malformed JSON is not usable evidence
350
+ }
351
+ if (!data || typeof data !== 'object') continue;
352
+ present.push(filePath);
353
+ auditEvents += Number(data.totalBlocked || data.blocked || 0);
354
+ if (/secret|exfil/i.test(text)) secretDenialSignals += 1;
355
+ }
356
+ }
357
+
358
+ const findings = [];
359
+ if (present.length === 0) {
360
+ findings.push({
361
+ severity: 'medium',
362
+ id: 'audit-missing',
363
+ message:
364
+ 'No local audit/gate-stats surface found — cannot produce sensitive-access evidence pack yet. Run agents with ThumbGate hooks to generate evidence.',
365
+ });
366
+ }
367
+
368
+ return {
369
+ surfaces: present,
370
+ auditEvents,
371
+ secretDenialSignals,
372
+ findings,
373
+ hasEvidence: present.length > 0,
374
+ };
375
+ }
376
+
377
+ function assessMcpThumbgate(projectRoot) {
378
+ const mcpPath = path.join(projectRoot, '.mcp.json');
379
+ const data = readJsonIfExists(mcpPath);
380
+ if (!data) {
381
+ return {
382
+ present: false,
383
+ thumbgate: false,
384
+ findings: [
385
+ {
386
+ severity: 'high',
387
+ id: 'mcp-missing',
388
+ message: 'No project .mcp.json — MCP discovery cannot equal callables; unattended agents may skip recall/capture.',
389
+ },
390
+ ],
391
+ };
392
+ }
393
+ if (data.__parseError) {
394
+ return {
395
+ present: true,
396
+ parseError: true,
397
+ thumbgate: false,
398
+ findings: [
399
+ {
400
+ severity: 'high',
401
+ id: 'mcp-parse-error',
402
+ message: '.mcp.json is not valid JSON.',
403
+ },
404
+ ],
405
+ };
406
+ }
407
+ const servers = data.mcpServers || data.servers || {};
408
+ const blob = JSON.stringify(servers).toLowerCase();
409
+ const thumbgate = Boolean(servers.thumbgate) || blob.includes('thumbgate') || blob.includes('server-stdio.js');
410
+ const findings = [];
411
+ if (!thumbgate) {
412
+ findings.push({
413
+ severity: 'high',
414
+ id: 'mcp-no-thumbgate',
415
+ message: 'Project .mcp.json has no thumbgate server entry.',
416
+ });
417
+ }
418
+ return { present: true, thumbgate, findings };
419
+ }
420
+
421
+ function scorePosture(findings) {
422
+ let score = 100;
423
+ for (const f of findings) {
424
+ if (f.severity === 'critical') score -= 25;
425
+ else if (f.severity === 'high') score -= 15;
426
+ else if (f.severity === 'medium') score -= 8;
427
+ else score -= 3;
428
+ }
429
+ if (score < 0) score = 0;
430
+ let band = 'healthy';
431
+ if (score < 40) band = 'critical';
432
+ else if (score < 60) band = 'at_risk';
433
+ else if (score < 80) band = 'needs_attention';
434
+ return { score, band };
435
+ }
436
+
437
+ /**
438
+ * Build the free Agent Security Central report for a project root.
439
+ * @param {object} [options]
440
+ * @param {string} [options.projectRoot]
441
+ * @param {string} [options.homeDir]
442
+ * @param {object} [options.env]
443
+ * @param {string} [options.manualGatesPath]
444
+ * @param {string} [options.autoGatesPath]
445
+ */
446
+ function buildSecurityCentralReport(options = {}) {
447
+ const projectRoot = path.resolve(options.projectRoot || process.cwd());
448
+ const homeDir = options.homeDir || process.env.HOME || null;
449
+ const env = options.env || process.env;
450
+
451
+ const manualGatesPath =
452
+ options.manualGatesPath || path.join(PKG_ROOT, 'config', 'gates', 'default.json');
453
+ // Pick the first store that actually EXISTS, not the first non-empty string.
454
+ // A plain `||` chain always selects the HOME filename when homeDir is set,
455
+ // so an absent ~/.thumbgate/auto-promoted-gates.json would silently hide the
456
+ // project's real auto-promoted gates from every policy count.
457
+ const projectAutoGatesPath = path.join(projectRoot, '.thumbgate', 'auto-promoted-gates.json');
458
+ const homeAutoGatesPath = homeDir
459
+ ? path.join(homeDir, '.thumbgate', 'auto-promoted-gates.json')
460
+ : null;
461
+ const autoGatesPath =
462
+ options.autoGatesPath ||
463
+ env.THUMBGATE_AUTO_GATES ||
464
+ [homeAutoGatesPath, projectAutoGatesPath].find((p) => p && fs.existsSync(p)) ||
465
+ projectAutoGatesPath;
466
+
467
+ const manualGates = loadGatesFromFile(manualGatesPath);
468
+ const autoGates = loadGatesFromFile(autoGatesPath);
469
+ const allGates = [...manualGates, ...autoGates];
470
+
471
+ const hooks = assessHookDrift(projectRoot, homeDir);
472
+ const privileged = assessPrivilegedCoverage(allGates);
473
+ const policy = assessPolicyVariance(manualGates, autoGates);
474
+ const audit = assessSensitiveAudit(projectRoot, homeDir, env);
475
+ const mcp = assessMcpThumbgate(projectRoot);
476
+
477
+ const findings = [
478
+ ...hooks.findings,
479
+ ...privileged.findings,
480
+ ...policy.findings,
481
+ ...audit.findings,
482
+ ...mcp.findings,
483
+ ];
484
+
485
+ const posture = scorePosture(findings);
486
+
487
+ return {
488
+ product: 'ThumbGate Agent Security Central',
489
+ free: true,
490
+ freemiumNote:
491
+ 'Local posture report is free forever (open source). Optional self-serve Pro is separate and not required for this report.',
492
+ version: PKG_VERSION,
493
+ generatedAt: new Date().toISOString(),
494
+ projectRoot,
495
+ posture,
496
+ dimensions: {
497
+ configurationDrift: {
498
+ drifted: hooks.drifted,
499
+ anyThumbgateHook: hooks.anyThumbgateHook,
500
+ anyGateCheck: hooks.anyGateCheck,
501
+ surfaces: hooks.surfaces,
502
+ },
503
+ privilegedAccess: {
504
+ coveredCount: privileged.covered.length,
505
+ missingCount: privileged.missing.length,
506
+ covered: privileged.covered,
507
+ missing: privileged.missing,
508
+ },
509
+ policyVariance: {
510
+ manualCount: policy.manualCount,
511
+ autoCount: policy.autoCount,
512
+ blockCount: policy.blockCount,
513
+ warnCount: policy.warnCount,
514
+ },
515
+ sensitiveDataAudit: {
516
+ hasEvidence: audit.hasEvidence,
517
+ surfaces: audit.surfaces,
518
+ auditEvents: audit.auditEvents,
519
+ secretDenialSignals: audit.secretDenialSignals,
520
+ },
521
+ mcpWiring: {
522
+ present: mcp.present,
523
+ thumbgate: mcp.thumbgate,
524
+ parseError: Boolean(mcp.parseError),
525
+ },
526
+ },
527
+ findings,
528
+ remediation: buildRemediation(findings),
529
+ commercialCta: {
530
+ // Soft self-serve only — no $499 pilot language (ECI-safe)
531
+ free: 'npx thumbgate init && npx thumbgate security-central',
532
+ softPro: 'https://thumbgate.ai/?utm_source=security_central&utm_medium=cli&utm_campaign=oracle_free_posture&cta_id=security_central_free',
533
+ },
534
+ };
535
+ }
536
+
537
+ function buildRemediation(findings) {
538
+ const steps = [];
539
+ const ids = new Set(findings.map((f) => f.id));
540
+ if (ids.has('hook-drift-missing-thumbgate') || ids.has('hook-drift-weak-marker')) {
541
+ steps.push('Run `npx thumbgate init` (or re-wire PreToolUse hooks) so gate-check runs before every tool call.');
542
+ }
543
+ if (ids.has('mcp-missing') || ids.has('mcp-no-thumbgate')) {
544
+ steps.push('Add a `thumbgate` entry to project `.mcp.json` so discovery matches callable tools.');
545
+ }
546
+ if (ids.has('policy-no-blocks') || ids.has('policy-empty')) {
547
+ steps.push('Load default gates (`config/gates/default.json`) and promote feedback to block rules.');
548
+ }
549
+ if ([...ids].some((id) => id.startsWith('privileged-gap-'))) {
550
+ steps.push('Enable privileged-coverage gates (secret exfil, force-push, spend, deploy) for high-blast tools.');
551
+ }
552
+ if (ids.has('audit-missing')) {
553
+ steps.push('Keep hooks on for a few sessions so gate-stats / audit surfaces accumulate denial evidence.');
554
+ }
555
+ if (steps.length === 0) {
556
+ steps.push('Posture looks solid. Re-run security-central after dependency or hook changes to catch drift.');
557
+ }
558
+ return steps;
559
+ }
560
+
561
+ function formatSecurityCentralReport(report) {
562
+ const lines = [];
563
+ lines.push(`ThumbGate Agent Security Central v${report.version} (FREE local report)`);
564
+ lines.push(`Project: ${report.projectRoot}`);
565
+ lines.push(`Posture: ${report.posture.band} · score ${report.posture.score}/100`);
566
+ lines.push('');
567
+ lines.push('Dimensions (Oracle Security Central → agent control plane):');
568
+ lines.push(
569
+ ` · config drift: ${report.dimensions.configurationDrift.drifted ? 'DRIFTED' : 'ok'} (hooks=${report.dimensions.configurationDrift.anyThumbgateHook ? 'yes' : 'no'})`,
570
+ );
571
+ lines.push(
572
+ ` · privileged coverage: ${report.dimensions.privilegedAccess.coveredCount}/${report.dimensions.privilegedAccess.coveredCount + report.dimensions.privilegedAccess.missingCount}`,
573
+ );
574
+ lines.push(
575
+ ` · policy: ${report.dimensions.policyVariance.blockCount} block / ${report.dimensions.policyVariance.warnCount} warn · manual ${report.dimensions.policyVariance.manualCount} · auto ${report.dimensions.policyVariance.autoCount}`,
576
+ );
577
+ lines.push(
578
+ ` · audit evidence: ${report.dimensions.sensitiveDataAudit.hasEvidence ? 'present' : 'missing'} (events≈${report.dimensions.sensitiveDataAudit.auditEvents})`,
579
+ );
580
+ lines.push(
581
+ ` · MCP thumbgate: ${report.dimensions.mcpWiring.thumbgate ? 'wired' : 'missing'}`,
582
+ );
583
+ if (report.findings.length) {
584
+ lines.push('');
585
+ lines.push(`Findings (${report.findings.length}):`);
586
+ for (const f of report.findings) {
587
+ lines.push(` [${f.severity}] ${f.id}: ${f.message}`);
588
+ }
589
+ }
590
+ lines.push('');
591
+ lines.push('Remediation:');
592
+ for (const step of report.remediation) {
593
+ lines.push(` · ${step}`);
594
+ }
595
+ lines.push('');
596
+ lines.push(`Free forever: ${report.commercialCta.free}`);
597
+ lines.push(`Soft product link: ${report.commercialCta.softPro}`);
598
+ return lines.join('\n');
599
+ }
600
+
601
+ function parseArgs(argv) {
602
+ const out = { json: false, projectRoot: null };
603
+ for (let i = 0; i < argv.length; i += 1) {
604
+ const a = argv[i];
605
+ if (a === '--json') out.json = true;
606
+ else if (a === '--project' || a === '--cwd') {
607
+ out.projectRoot = argv[i + 1];
608
+ i += 1;
609
+ }
610
+ }
611
+ return out;
612
+ }
613
+
614
+ function main(argv = process.argv.slice(2)) {
615
+ const args = parseArgs(argv);
616
+ const report = buildSecurityCentralReport({
617
+ projectRoot: args.projectRoot || process.cwd(),
618
+ });
619
+ if (args.json) {
620
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
621
+ } else {
622
+ process.stdout.write(`${formatSecurityCentralReport(report)}\n`);
623
+ }
624
+ // Exit non-zero only when posture is critically broken — free tool should not scream on warnings alone
625
+ if (report.posture.band === 'critical') process.exitCode = 2;
626
+ else if (report.posture.band === 'at_risk') process.exitCode = 1;
627
+ else process.exitCode = 0;
628
+ }
629
+
630
+ if (path.resolve(process.argv[1] || '') === path.resolve(__filename)) {
631
+ main();
632
+ }
633
+
634
+ module.exports = {
635
+ buildSecurityCentralReport,
636
+ formatSecurityCentralReport,
637
+ assessHookDrift,
638
+ assessPrivilegedCoverage,
639
+ assessPolicyVariance,
640
+ assessSensitiveAudit,
641
+ assessMcpThumbgate,
642
+ scorePosture,
643
+ PRIVILEGED_COVERAGE,
644
+ EXPECTED_HOOK_MARKERS,
645
+ main,
646
+ parseArgs,
647
+ };