thumbgate 1.14.1 → 1.16.0

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 (150) hide show
  1. package/.claude-plugin/marketplace.json +6 -6
  2. package/.claude-plugin/plugin.json +3 -3
  3. package/.well-known/llms.txt +5 -5
  4. package/.well-known/mcp/server-card.json +1 -1
  5. package/README.md +60 -35
  6. package/adapters/chatgpt/openapi.yaml +118 -2
  7. package/adapters/claude/.mcp.json +2 -2
  8. package/adapters/mcp/server-stdio.js +217 -84
  9. package/adapters/opencode/opencode.json +1 -1
  10. package/bench/prompt-eval-suite.json +5 -1
  11. package/bin/cli.js +211 -8
  12. package/config/enforcement.json +59 -7
  13. package/config/evals/agent-safety-eval.json +338 -22
  14. package/config/gates/default.json +33 -0
  15. package/config/gates/routine.json +43 -0
  16. package/config/github-about.json +3 -3
  17. package/config/mcp-allowlists.json +4 -0
  18. package/config/merge-quality-checks.json +2 -1
  19. package/config/model-candidates.json +131 -0
  20. package/openapi/openapi.yaml +118 -2
  21. package/package.json +70 -51
  22. package/public/blog.html +7 -7
  23. package/public/codex-plugin.html +13 -7
  24. package/public/compare.html +29 -23
  25. package/public/dashboard.html +105 -12
  26. package/public/guide.html +28 -28
  27. package/public/index.html +233 -97
  28. package/public/learn.html +87 -20
  29. package/public/lessons.html +26 -2
  30. package/public/numbers.html +271 -0
  31. package/public/pro.html +89 -19
  32. package/scripts/agent-audit-trace.js +55 -0
  33. package/scripts/agent-memory-lifecycle.js +96 -0
  34. package/scripts/agent-readiness-plan.js +118 -0
  35. package/scripts/agentic-data-pipeline.js +21 -1
  36. package/scripts/agents-sdk-sandbox-plan.js +57 -0
  37. package/scripts/ai-org-governance.js +98 -0
  38. package/scripts/ai-search-distribution.js +43 -0
  39. package/scripts/artifact-agent-plan.js +81 -0
  40. package/scripts/billing.js +27 -8
  41. package/scripts/cli-feedback.js +2 -1
  42. package/scripts/cli-schema.js +60 -5
  43. package/scripts/code-mode-mcp-plan.js +71 -0
  44. package/scripts/commercial-offer.js +1 -1
  45. package/scripts/context-engine.js +1 -2
  46. package/scripts/context-manager.js +4 -1
  47. package/scripts/contextfs.js +214 -32
  48. package/scripts/dashboard-render-spec.js +1 -1
  49. package/scripts/dashboard.js +275 -9
  50. package/scripts/decision-journal.js +13 -3
  51. package/scripts/document-workflow-governance.js +62 -0
  52. package/scripts/enterprise-agent-rollout.js +34 -0
  53. package/scripts/experience-replay-governance.js +69 -0
  54. package/scripts/export-hf-dataset.js +1 -1
  55. package/scripts/feedback-loop.js +141 -9
  56. package/scripts/feedback-to-rules.js +17 -23
  57. package/scripts/gates-engine.js +4 -6
  58. package/scripts/growth-campaigns.js +49 -0
  59. package/scripts/harness-selector.js +145 -1
  60. package/scripts/hybrid-supervisor-agent.js +64 -0
  61. package/scripts/inference-cache-policy.js +72 -0
  62. package/scripts/inference-economics.js +53 -0
  63. package/scripts/internal-agent-bootstrap.js +12 -2
  64. package/scripts/knowledge-layer-plan.js +108 -0
  65. package/scripts/lesson-canonical.js +181 -0
  66. package/scripts/lesson-db.js +71 -10
  67. package/scripts/lesson-inference.js +183 -44
  68. package/scripts/lesson-search.js +4 -1
  69. package/scripts/lesson-synthesis.js +23 -2
  70. package/scripts/llm-client.js +157 -26
  71. package/scripts/mailer/resend-mailer.js +112 -1
  72. package/scripts/mcp-transport-strategy.js +66 -0
  73. package/scripts/memory-store-governance.js +60 -0
  74. package/scripts/meta-agent-loop.js +7 -13
  75. package/scripts/model-access-eligibility.js +38 -0
  76. package/scripts/model-migration-readiness.js +55 -0
  77. package/scripts/native-messaging-audit.js +514 -0
  78. package/scripts/operational-integrity.js +96 -3
  79. package/scripts/otel-declarative-config.js +56 -0
  80. package/scripts/perplexity-client.js +1 -1
  81. package/scripts/post-training-governance.js +34 -0
  82. package/scripts/pr-manager.js +47 -7
  83. package/scripts/private-core-boundary.js +72 -0
  84. package/scripts/production-agent-readiness.js +40 -0
  85. package/scripts/profile-router.js +16 -1
  86. package/scripts/prompt-eval.js +564 -32
  87. package/scripts/prompt-programs.js +93 -0
  88. package/scripts/provider-action-normalizer.js +585 -0
  89. package/scripts/rule-validator.js +285 -0
  90. package/scripts/scaling-law-claims.js +60 -0
  91. package/scripts/security-scanner.js +1 -1
  92. package/scripts/self-distill-agent.js +7 -32
  93. package/scripts/seo-gsd.js +400 -43
  94. package/scripts/skill-rag-router.js +53 -0
  95. package/scripts/spec-gate.js +1 -1
  96. package/scripts/student-consistent-training.js +73 -0
  97. package/scripts/synthetic-data-provenance.js +98 -0
  98. package/scripts/task-context-result.js +81 -0
  99. package/scripts/telemetry-analytics.js +149 -0
  100. package/scripts/thompson-sampling.js +2 -2
  101. package/scripts/token-savings.js +7 -6
  102. package/scripts/token-tco.js +46 -0
  103. package/scripts/tool-registry.js +75 -3
  104. package/scripts/verification-loop.js +10 -1
  105. package/scripts/verifier-scoring.js +71 -0
  106. package/scripts/workflow-sentinel.js +284 -28
  107. package/scripts/workspace-agent-routines.js +118 -0
  108. package/skills/thumbgate/SKILL.md +1 -1
  109. package/src/api/server.js +434 -120
  110. package/.claude-plugin/README.md +0 -170
  111. package/adapters/README.md +0 -12
  112. package/scripts/analytics-report.js +0 -328
  113. package/scripts/autonomous-workflow.js +0 -377
  114. package/scripts/billing-setup.js +0 -109
  115. package/scripts/creator-campaigns.js +0 -239
  116. package/scripts/cross-encoder-reranker.js +0 -235
  117. package/scripts/daemon-manager.js +0 -108
  118. package/scripts/decision-trace.js +0 -354
  119. package/scripts/delegation-runtime.js +0 -896
  120. package/scripts/dispatch-brief.js +0 -159
  121. package/scripts/distribution-surfaces.js +0 -110
  122. package/scripts/feedback-history-distiller.js +0 -382
  123. package/scripts/funnel-analytics.js +0 -35
  124. package/scripts/history-distiller.js +0 -200
  125. package/scripts/hosted-job-launcher.js +0 -256
  126. package/scripts/intent-router.js +0 -392
  127. package/scripts/lesson-reranker.js +0 -263
  128. package/scripts/lesson-retrieval.js +0 -148
  129. package/scripts/managed-lesson-agent.js +0 -183
  130. package/scripts/operational-dashboard.js +0 -103
  131. package/scripts/operational-summary.js +0 -129
  132. package/scripts/operator-artifacts.js +0 -608
  133. package/scripts/optimize-context.js +0 -17
  134. package/scripts/org-dashboard.js +0 -206
  135. package/scripts/partner-orchestration.js +0 -146
  136. package/scripts/predictive-insights.js +0 -356
  137. package/scripts/pulse.js +0 -80
  138. package/scripts/reflector-agent.js +0 -221
  139. package/scripts/sales-pipeline.js +0 -681
  140. package/scripts/session-episode-store.js +0 -329
  141. package/scripts/session-health-sensor.js +0 -242
  142. package/scripts/session-report.js +0 -120
  143. package/scripts/swarm-coordinator.js +0 -81
  144. package/scripts/tool-kpi-tracker.js +0 -12
  145. package/scripts/webhook-delivery.js +0 -62
  146. package/scripts/workflow-sprint-intake.js +0 -475
  147. package/skills/agent-memory/SKILL.md +0 -97
  148. package/skills/solve-architecture-autonomy/SKILL.md +0 -17
  149. package/skills/solve-architecture-autonomy/tool.js +0 -33
  150. package/skills/thumbgate-feedback/SKILL.md +0 -49
@@ -0,0 +1,514 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('node:fs');
5
+ const os = require('node:os');
6
+ const path = require('node:path');
7
+
8
+ const ROOT = path.join(__dirname, '..');
9
+
10
+ const BROWSER_TARGETS = Object.freeze({
11
+ darwin: [
12
+ browserTarget(
13
+ 'chrome',
14
+ 'Google Chrome',
15
+ ['Library', 'Application Support', 'Google', 'Chrome', 'NativeMessagingHosts'],
16
+ ['/Applications/Google Chrome.app', '~/Applications/Google Chrome.app']
17
+ ),
18
+ browserTarget(
19
+ 'edge',
20
+ 'Microsoft Edge',
21
+ ['Library', 'Application Support', 'Microsoft Edge', 'NativeMessagingHosts'],
22
+ ['/Applications/Microsoft Edge.app', '~/Applications/Microsoft Edge.app']
23
+ ),
24
+ browserTarget(
25
+ 'brave',
26
+ 'Brave',
27
+ ['Library', 'Application Support', 'BraveSoftware', 'Brave-Browser', 'NativeMessagingHosts'],
28
+ ['/Applications/Brave Browser.app', '~/Applications/Brave Browser.app']
29
+ ),
30
+ browserTarget(
31
+ 'arc',
32
+ 'Arc',
33
+ ['Library', 'Application Support', 'Arc', 'User Data', 'NativeMessagingHosts'],
34
+ ['/Applications/Arc.app', '~/Applications/Arc.app']
35
+ ),
36
+ browserTarget(
37
+ 'chromium',
38
+ 'Chromium',
39
+ ['Library', 'Application Support', 'Chromium', 'NativeMessagingHosts'],
40
+ ['/Applications/Chromium.app', '~/Applications/Chromium.app']
41
+ ),
42
+ browserTarget(
43
+ 'vivaldi',
44
+ 'Vivaldi',
45
+ ['Library', 'Application Support', 'Vivaldi', 'NativeMessagingHosts'],
46
+ ['/Applications/Vivaldi.app', '~/Applications/Vivaldi.app']
47
+ ),
48
+ browserTarget(
49
+ 'opera',
50
+ 'Opera',
51
+ ['Library', 'Application Support', 'com.operasoftware.Opera', 'NativeMessagingHosts'],
52
+ ['/Applications/Opera.app', '~/Applications/Opera.app']
53
+ ),
54
+ ],
55
+ linux: [
56
+ browserTarget(
57
+ 'chrome',
58
+ 'Google Chrome',
59
+ ['.config', 'google-chrome', 'NativeMessagingHosts'],
60
+ ['/usr/bin/google-chrome', '/opt/google/chrome/chrome']
61
+ ),
62
+ browserTarget(
63
+ 'edge',
64
+ 'Microsoft Edge',
65
+ ['.config', 'microsoft-edge', 'NativeMessagingHosts'],
66
+ ['/usr/bin/microsoft-edge', '/opt/microsoft/msedge/msedge']
67
+ ),
68
+ browserTarget(
69
+ 'brave',
70
+ 'Brave',
71
+ ['.config', 'BraveSoftware', 'Brave-Browser', 'NativeMessagingHosts'],
72
+ ['/usr/bin/brave-browser', '/opt/brave.com/brave/brave-browser']
73
+ ),
74
+ browserTarget(
75
+ 'chromium',
76
+ 'Chromium',
77
+ ['.config', 'chromium', 'NativeMessagingHosts'],
78
+ ['/usr/bin/chromium', '/usr/bin/chromium-browser']
79
+ ),
80
+ browserTarget(
81
+ 'vivaldi',
82
+ 'Vivaldi',
83
+ ['.config', 'vivaldi', 'NativeMessagingHosts'],
84
+ ['/usr/bin/vivaldi', '/opt/vivaldi/vivaldi']
85
+ ),
86
+ browserTarget(
87
+ 'opera',
88
+ 'Opera',
89
+ ['.config', 'opera', 'NativeMessagingHosts'],
90
+ ['/usr/bin/opera', '/usr/lib/x86_64-linux-gnu/opera/opera']
91
+ ),
92
+ ],
93
+ win32: [],
94
+ });
95
+
96
+ const AI_VENDOR_PATTERNS = Object.freeze([
97
+ { vendor: 'Anthropic', pattern: /\banthropic\b|\bclaude\b/i },
98
+ { vendor: 'OpenAI', pattern: /\bopenai\b|\bcodex\b|\bchatgpt\b/i },
99
+ { vendor: 'Google', pattern: /\bgoogle\b|\bgemini\b/i },
100
+ { vendor: 'Cursor', pattern: /\bcursor\b/i },
101
+ { vendor: 'Perplexity', pattern: /\bperplexity\b/i },
102
+ { vendor: 'Browserbase', pattern: /\bbrowserbase\b|\bstagehand\b/i },
103
+ ]);
104
+
105
+ function browserTarget(key, displayName, manifestDirParts, installHints) {
106
+ return Object.freeze({ key, displayName, manifestDirParts, installHints });
107
+ }
108
+
109
+ function normalizePlatform(platform) {
110
+ const normalized = String(platform || process.platform).toLowerCase();
111
+ if (normalized === 'mac' || normalized === 'macos' || normalized === 'darwin') return 'darwin';
112
+ if (normalized === 'linux') return 'linux';
113
+ if (normalized === 'windows' || normalized === 'win32') return 'win32';
114
+ return normalized;
115
+ }
116
+
117
+ function resolveInstallHint(hint, homeDir) {
118
+ return hint.startsWith('~/')
119
+ ? path.join(homeDir, hint.slice(2))
120
+ : hint;
121
+ }
122
+
123
+ function getBrowserTargets(platform) {
124
+ return BROWSER_TARGETS[normalizePlatform(platform)] || [];
125
+ }
126
+
127
+ function listJsonFiles(dirPath) {
128
+ try {
129
+ return fs.readdirSync(dirPath, { withFileTypes: true })
130
+ .filter((entry) => entry.isFile() && entry.name.endsWith('.json'))
131
+ .map((entry) => path.join(dirPath, entry.name));
132
+ } catch {
133
+ return [];
134
+ }
135
+ }
136
+
137
+ function guessVendor(manifestPath, manifest) {
138
+ const haystack = [
139
+ manifestPath,
140
+ manifest?.name,
141
+ manifest?.description,
142
+ manifest?.path,
143
+ ...(Array.isArray(manifest?.allowed_origins) ? manifest.allowed_origins : []),
144
+ ]
145
+ .filter(Boolean)
146
+ .join(' ');
147
+
148
+ for (const candidate of AI_VENDOR_PATTERNS) {
149
+ if (candidate.pattern.test(haystack)) {
150
+ return candidate.vendor;
151
+ }
152
+ }
153
+
154
+ return 'Unknown';
155
+ }
156
+
157
+ function isAiVendor(vendor) {
158
+ return vendor !== 'Unknown';
159
+ }
160
+
161
+ function extractExtensionId(origin) {
162
+ const match = /^chrome-extension:\/\/([^/]+)/i.exec(String(origin || ''));
163
+ return match?.[1] || null;
164
+ }
165
+
166
+ function readManifest(filePath) {
167
+ const raw = fs.readFileSync(filePath, 'utf8');
168
+ try {
169
+ const parsed = JSON.parse(raw);
170
+ return { raw, parsed, parseError: null };
171
+ } catch (error) {
172
+ return { raw, parsed: null, parseError: error.message };
173
+ }
174
+ }
175
+
176
+ function guessBrowserInstalled(target, { platform, homeDir, explicitHomeDir }) {
177
+ const normalizedPlatform = normalizePlatform(platform);
178
+ if (normalizedPlatform !== 'darwin' && normalizedPlatform !== 'linux') {
179
+ return null;
180
+ }
181
+
182
+ const installHints = Array.isArray(target.installHints) ? target.installHints : [];
183
+ if (installHints.length === 0) return null;
184
+ return installHints.some((hint) => {
185
+ if (explicitHomeDir && !hint.startsWith('~/')) {
186
+ return false;
187
+ }
188
+ return fs.existsSync(resolveInstallHint(hint, homeDir));
189
+ });
190
+ }
191
+
192
+ function describeFinding(code, severity, message) {
193
+ return { code, severity, message };
194
+ }
195
+
196
+ function analyzeManifestEntry(entry) {
197
+ const findings = [];
198
+
199
+ if (entry.parseError) {
200
+ findings.push(describeFinding(
201
+ 'invalid_manifest_json',
202
+ 'high',
203
+ 'Manifest JSON is invalid, so the host registration cannot be reviewed safely.'
204
+ ));
205
+ return findings;
206
+ }
207
+
208
+ if (!entry.hostName) {
209
+ findings.push(describeFinding(
210
+ 'missing_host_name',
211
+ 'medium',
212
+ 'Manifest is missing the required host name.'
213
+ ));
214
+ }
215
+
216
+ if (!entry.hostPath) {
217
+ findings.push(describeFinding(
218
+ 'missing_host_path',
219
+ 'high',
220
+ 'Manifest does not declare a host binary path.'
221
+ ));
222
+ } else if (!entry.hostPathExists) {
223
+ findings.push(describeFinding(
224
+ 'missing_host_binary',
225
+ 'high',
226
+ 'Manifest points at a host binary that is not present on disk.'
227
+ ));
228
+ }
229
+
230
+ if (entry.allowedOriginsCount > 0) {
231
+ findings.push(describeFinding(
232
+ 'preauthorized_extension_bridge',
233
+ 'medium',
234
+ `Manifest pre-authorizes ${entry.allowedOriginsCount} browser extension origin${entry.allowedOriginsCount === 1 ? '' : 's'}.`
235
+ ));
236
+ }
237
+
238
+ if (entry.aiBridge) {
239
+ findings.push(describeFinding(
240
+ 'ai_browser_bridge',
241
+ 'medium',
242
+ `${entry.vendor} browser bridge detected through native messaging.`
243
+ ));
244
+ }
245
+
246
+ if (entry.browserInstalledGuess === false) {
247
+ findings.push(describeFinding(
248
+ 'browser_not_detected',
249
+ 'medium',
250
+ `${entry.browser} is not detected in the usual install locations for this machine.`
251
+ ));
252
+ }
253
+
254
+ if (entry.aiBridge && entry.allowedOriginsCount > 0 && entry.browserInstalledGuess === false) {
255
+ findings.push(describeFinding(
256
+ 'dormant_ai_browser_bridge',
257
+ 'high',
258
+ 'An AI browser bridge is registered for a browser that is not detected locally, which expands future attack surface without an obvious active integration.'
259
+ ));
260
+ }
261
+
262
+ return findings;
263
+ }
264
+
265
+ function shouldIncludeEntry(entry, options = {}) {
266
+ if (options.aiOnly !== true) return true;
267
+ return entry.aiBridge;
268
+ }
269
+
270
+ function getAllowedOrigins(manifest) {
271
+ return Array.isArray(manifest?.allowed_origins)
272
+ ? manifest.allowed_origins.filter((origin) => typeof origin === 'string' && origin.trim())
273
+ : [];
274
+ }
275
+
276
+ function buildManifestEntry(target, manifestPath, auditOptions) {
277
+ const { parsed, parseError } = readManifest(manifestPath);
278
+ const allowedOrigins = getAllowedOrigins(parsed);
279
+ const hostPath = typeof parsed?.path === 'string' ? parsed.path : null;
280
+ const vendor = guessVendor(manifestPath, parsed);
281
+ const entry = {
282
+ browser: target.displayName,
283
+ browserKey: target.key,
284
+ manifestPath,
285
+ manifestDir: path.join(auditOptions.homeDir, ...target.manifestDirParts),
286
+ hostName: typeof parsed?.name === 'string' ? parsed.name : null,
287
+ hostPath,
288
+ hostPathExists: hostPath ? fs.existsSync(hostPath) : false,
289
+ allowedOrigins,
290
+ allowedOriginsCount: allowedOrigins.length,
291
+ extensionIds: allowedOrigins.map(extractExtensionId).filter(Boolean),
292
+ vendor,
293
+ aiBridge: isAiVendor(vendor),
294
+ browserInstalledGuess: guessBrowserInstalled(target, auditOptions),
295
+ parseError,
296
+ };
297
+ return {
298
+ ...entry,
299
+ findings: analyzeManifestEntry(entry),
300
+ };
301
+ }
302
+
303
+ function collectTargetEntries(target, auditOptions, options) {
304
+ const manifestDir = path.join(auditOptions.homeDir, ...target.manifestDirParts);
305
+ return listJsonFiles(manifestDir)
306
+ .map((manifestPath) => buildManifestEntry(target, manifestPath, auditOptions))
307
+ .filter((entry) => shouldIncludeEntry(entry, options));
308
+ }
309
+
310
+ function buildAuditOptions(options = {}) {
311
+ return {
312
+ platform: normalizePlatform(options.platform),
313
+ homeDir: path.resolve(options.homeDir || os.homedir()),
314
+ explicitHomeDir: typeof options.homeDir === 'string' && options.homeDir.trim().length > 0,
315
+ };
316
+ }
317
+
318
+ function buildWindowsAudit(auditOptions) {
319
+ return {
320
+ platform: auditOptions.platform,
321
+ homeDir: auditOptions.homeDir,
322
+ entries: [],
323
+ notes: ['Windows native messaging is registry-based; this file audit focuses on macOS and Linux host manifests.'],
324
+ };
325
+ }
326
+
327
+ function collectNativeMessagingEntries(options = {}) {
328
+ const auditOptions = buildAuditOptions(options);
329
+ if (auditOptions.platform === 'win32') {
330
+ return buildWindowsAudit(auditOptions);
331
+ }
332
+
333
+ const entries = getBrowserTargets(auditOptions.platform)
334
+ .flatMap((target) => collectTargetEntries(target, auditOptions, options));
335
+ return {
336
+ platform: auditOptions.platform,
337
+ homeDir: auditOptions.homeDir,
338
+ entries,
339
+ notes: [],
340
+ };
341
+ }
342
+
343
+ function summarizeFindings(entries) {
344
+ return entries.flatMap((entry) => entry.findings.map((finding) => ({
345
+ browser: entry.browser,
346
+ manifestPath: entry.manifestPath,
347
+ hostName: entry.hostName,
348
+ vendor: entry.vendor,
349
+ ...finding,
350
+ })));
351
+ }
352
+
353
+ function buildRecommendations(findings, options = {}) {
354
+ const recommendations = [
355
+ 'Review every native messaging host that grants browser automation or extension bridge access before allowing high-risk tasks.',
356
+ 'Prefer ask-before-acting modes for browser-use agents until connector scope, extension permissions, and revocation steps are explicit.',
357
+ 'Use ThumbGate to gate new connector installs and require explicit approval before cross-app integrations become part of the workflow.',
358
+ ];
359
+
360
+ if (findings.some((finding) => finding.code === 'dormant_ai_browser_bridge')) {
361
+ recommendations.unshift('Remove or disable AI browser bridge manifests for browsers you did not intentionally integrate, then re-enable only after explicit approval.');
362
+ }
363
+
364
+ if (findings.some((finding) => finding.code === 'missing_host_binary')) {
365
+ recommendations.push('Clean up broken host registrations so browsers do not keep stale native messaging entries that point at missing binaries.');
366
+ }
367
+
368
+ if (options.aiOnly === true) {
369
+ recommendations.push('Re-run the full audit without --ai-only when you need a complete inventory of non-AI browser bridge registrations.');
370
+ }
371
+
372
+ return recommendations;
373
+ }
374
+
375
+ function buildNativeMessagingAudit(options = {}) {
376
+ const collected = collectNativeMessagingEntries(options);
377
+ const findings = summarizeFindings(collected.entries);
378
+ const highSeverityCount = findings.filter((finding) => finding.severity === 'high').length;
379
+ const mediumSeverityCount = findings.filter((finding) => finding.severity === 'medium').length;
380
+ const browsersCovered = [...new Set(collected.entries.map((entry) => entry.browser))];
381
+ const aiBridgeCount = collected.entries.filter((entry) => entry.aiBridge).length;
382
+
383
+ let status = 'clear';
384
+ if (highSeverityCount > 0) {
385
+ status = 'review';
386
+ } else if (mediumSeverityCount > 0) {
387
+ status = 'watch';
388
+ }
389
+
390
+ return {
391
+ name: 'thumbgate-native-messaging-audit',
392
+ generatedAt: new Date().toISOString(),
393
+ platform: collected.platform,
394
+ homeDir: collected.homeDir,
395
+ status,
396
+ summary: {
397
+ manifestCount: collected.entries.length,
398
+ browsersCovered: browsersCovered.length,
399
+ aiBridgeCount,
400
+ highSeverityCount,
401
+ mediumSeverityCount,
402
+ },
403
+ notes: collected.notes,
404
+ manifests: collected.entries,
405
+ findings,
406
+ recommendations: buildRecommendations(findings, options),
407
+ };
408
+ }
409
+
410
+ function appendBlock(lines, heading, entries) {
411
+ if (entries.length === 0) return;
412
+ lines.push('', heading, ...entries);
413
+ }
414
+
415
+ function formatFindingLine(finding) {
416
+ return ` - [${finding.severity}] ${finding.browser}: ${finding.message}`;
417
+ }
418
+
419
+ function formatManifestLines(entry) {
420
+ const lines = [
421
+ ` - ${entry.browser} -> ${entry.hostName || path.basename(entry.manifestPath)}`,
422
+ ` manifest: ${entry.manifestPath}`,
423
+ ];
424
+ if (entry.hostPath) {
425
+ lines.push(` host: ${entry.hostPath}${entry.hostPathExists ? '' : ' (missing)'}`);
426
+ }
427
+ if (entry.allowedOriginsCount > 0) {
428
+ lines.push(` allowed origins: ${entry.allowedOriginsCount}`);
429
+ }
430
+ return lines;
431
+ }
432
+
433
+ function formatNativeMessagingAudit(report) {
434
+ const lines = [
435
+ 'ThumbGate Native Messaging Audit',
436
+ `Status : ${report.status}`,
437
+ `Hosts : ${report.summary.manifestCount} manifest${report.summary.manifestCount === 1 ? '' : 's'} across ${report.summary.browsersCovered} browser${report.summary.browsersCovered === 1 ? '' : 's'}`,
438
+ `AI : ${report.summary.aiBridgeCount} AI browser bridge${report.summary.aiBridgeCount === 1 ? '' : 's'}`,
439
+ ];
440
+ appendBlock(lines, 'Findings:', report.findings.map(formatFindingLine));
441
+ appendBlock(lines, 'Registered manifests:', report.manifests.flatMap(formatManifestLines));
442
+ appendBlock(lines, 'Recommendations:', report.recommendations.map((recommendation) => ` - ${recommendation}`));
443
+ if (report.notes.length > 0) {
444
+ lines.splice(4, 0, '', ...report.notes.map((note) => `Note : ${note}`));
445
+ }
446
+ lines.push('');
447
+ return `${lines.join('\n')}`;
448
+ }
449
+
450
+ function parseArgs(argv) {
451
+ const args = {};
452
+ for (let index = 0; index < argv.length; index++) {
453
+ const token = argv[index];
454
+ if (!token.startsWith('--')) continue;
455
+ const [rawKey, inlineValue] = token.slice(2).split('=');
456
+ const key = rawKey;
457
+ if (inlineValue !== undefined) {
458
+ args[key] = inlineValue;
459
+ continue;
460
+ }
461
+ const next = argv[index + 1];
462
+ if (next && !next.startsWith('--')) {
463
+ args[key] = next;
464
+ index += 1;
465
+ continue;
466
+ }
467
+ args[key] = true;
468
+ }
469
+ return args;
470
+ }
471
+
472
+ function parseBooleanFlag(value) {
473
+ return value === true || value === 'true';
474
+ }
475
+
476
+ function isMainModule() {
477
+ return Boolean(process.argv[1] && path.resolve(process.argv[1]) === __filename);
478
+ }
479
+
480
+ function main(argv = process.argv.slice(2)) {
481
+ const args = parseArgs(argv);
482
+ const report = buildNativeMessagingAudit({
483
+ homeDir: args['home-dir'],
484
+ platform: args.platform,
485
+ aiOnly: parseBooleanFlag(args['ai-only']),
486
+ });
487
+
488
+ if (args.json) {
489
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
490
+ return;
491
+ }
492
+
493
+ process.stdout.write(formatNativeMessagingAudit(report));
494
+ }
495
+
496
+ if (isMainModule()) {
497
+ try {
498
+ main();
499
+ } catch (error) {
500
+ console.error(error?.message || error);
501
+ process.exit(1);
502
+ }
503
+ }
504
+
505
+ module.exports = {
506
+ AI_VENDOR_PATTERNS,
507
+ BROWSER_TARGETS,
508
+ buildNativeMessagingAudit,
509
+ collectNativeMessagingEntries,
510
+ formatNativeMessagingAudit,
511
+ getBrowserTargets,
512
+ guessVendor,
513
+ normalizePlatform,
514
+ };
@@ -603,10 +603,18 @@ function findOpenPrForBranch({ branchName, runner = runGh, env = process.env } =
603
603
 
604
604
  function classifyCommand(command) {
605
605
  const text = String(command || '').trim();
606
+ const workflowRunMatch = text.match(/\bgh\s+workflow\s+run\s+([^\s]+)/i);
607
+ const refMatch = text.match(/(?:--ref|-r)\s+([^\s]+)/i);
608
+ const fieldArgs = [...text.matchAll(/(?:--field|-f)\s+([A-Za-z0-9_.-]+)=([^\s]+)/gi)]
609
+ .map((match) => ({ name: match[1], value: match[2] }));
606
610
  return {
607
611
  text,
608
612
  isPrCreate: /\bgh\s+pr\s+create\b/i.test(text),
609
613
  isPrMerge: /\bgh\s+pr\s+merge\b/i.test(text),
614
+ isWorkflowRun: /\bgh\s+workflow\s+run\b/i.test(text),
615
+ workflowName: workflowRunMatch ? workflowRunMatch[1] : null,
616
+ workflowRef: refMatch ? refMatch[1] : null,
617
+ workflowFields: fieldArgs,
610
618
  isPublish: /\b(?:npm|yarn|pnpm)\s+publish\b/i.test(text),
611
619
  isReleaseCreate: /\bgh\s+release\s+create\b/i.test(text),
612
620
  isTagCreate: /\bgit\s+tag\b/i.test(text),
@@ -648,23 +656,108 @@ function evaluateOperationalIntegrity(options = {}) {
648
656
  const commandInfo = classifyCommand(options.command || '');
649
657
  const blockers = [];
650
658
 
651
- const requiresGovernance = commandInfo.isPrCreate || commandInfo.isPrMerge || commandInfo.isPublish || commandInfo.isReleaseCreate || commandInfo.isTagCreate;
659
+ const requiresGovernance = commandInfo.isPrCreate
660
+ || commandInfo.isPrMerge
661
+ || commandInfo.isWorkflowRun
662
+ || commandInfo.isPublish
663
+ || commandInfo.isReleaseCreate
664
+ || commandInfo.isTagCreate;
652
665
  const isPublishLike = commandInfo.isPublish || commandInfo.isReleaseCreate || commandInfo.isTagCreate;
653
666
 
654
667
  if (requiresGovernance && !branchGovernance) {
655
668
  blockers.push(buildBlocker(
656
669
  'missing_branch_governance',
657
- 'PR, merge, release, and publish actions require explicit branch governance.'
670
+ 'PR, workflow dispatch, merge, release, and publish actions require explicit branch governance.'
658
671
  ));
659
672
  }
660
673
 
661
674
  if (branchGovernance && branchGovernance.localOnly === true && requiresGovernance) {
662
675
  blockers.push(buildBlocker(
663
676
  'local_only_branch',
664
- 'This task is marked local-only. PR, merge, release, and publish actions are blocked.'
677
+ 'This task is marked local-only. PR, workflow dispatch, merge, release, and publish actions are blocked.'
665
678
  ));
666
679
  }
667
680
 
681
+ if (commandInfo.isWorkflowRun) {
682
+ const workflowEvidence = branchGovernance && branchGovernance.workflowDispatch
683
+ && typeof branchGovernance.workflowDispatch === 'object'
684
+ ? branchGovernance.workflowDispatch
685
+ : null;
686
+ const requestedEnvironment = workflowEvidence && workflowEvidence.environment
687
+ ? String(workflowEvidence.environment).trim()
688
+ : '';
689
+ const expectedWorkflow = workflowEvidence && workflowEvidence.workflow
690
+ ? String(workflowEvidence.workflow).trim()
691
+ : '';
692
+ const expectedRef = workflowEvidence && workflowEvidence.ref
693
+ ? String(workflowEvidence.ref).trim()
694
+ : '';
695
+ const expectedSha = workflowEvidence && workflowEvidence.sha
696
+ ? String(workflowEvidence.sha).trim()
697
+ : '';
698
+ const expectedJob = workflowEvidence && workflowEvidence.job
699
+ ? String(workflowEvidence.job).trim()
700
+ : '';
701
+
702
+ if (!workflowEvidence) {
703
+ blockers.push(buildBlocker(
704
+ 'missing_workflow_dispatch_evidence',
705
+ 'GitHub Actions workflow dispatch requires explicit workflowDispatch evidence: environment, workflow, ref, sha, and job.'
706
+ ));
707
+ }
708
+ if (workflowEvidence && !requestedEnvironment) {
709
+ blockers.push(buildBlocker(
710
+ 'missing_workflow_environment',
711
+ 'Workflow dispatch requires the requested environment, such as dev, staging, beta, or release.'
712
+ ));
713
+ }
714
+ if (workflowEvidence && !expectedWorkflow) {
715
+ blockers.push(buildBlocker(
716
+ 'missing_workflow_name',
717
+ 'Workflow dispatch requires the expected workflow file name before execution.'
718
+ ));
719
+ }
720
+ if (workflowEvidence && expectedWorkflow && commandInfo.workflowName !== expectedWorkflow) {
721
+ blockers.push(buildBlocker(
722
+ 'workflow_name_mismatch',
723
+ `Requested ${requestedEnvironment || 'workflow'} dispatch expects ${expectedWorkflow}, but command runs ${commandInfo.workflowName || 'unknown workflow'}.`,
724
+ { expectedWorkflow, actualWorkflow: commandInfo.workflowName }
725
+ ));
726
+ }
727
+ if (workflowEvidence && !expectedRef) {
728
+ blockers.push(buildBlocker(
729
+ 'missing_workflow_ref',
730
+ 'Workflow dispatch requires an explicit branch/ref before execution.'
731
+ ));
732
+ }
733
+ if (workflowEvidence && expectedRef && commandInfo.workflowRef !== expectedRef) {
734
+ blockers.push(buildBlocker(
735
+ 'workflow_ref_mismatch',
736
+ `Workflow dispatch expects ref ${expectedRef}, but command uses ${commandInfo.workflowRef || 'no --ref value'}.`,
737
+ { expectedRef, actualRef: commandInfo.workflowRef }
738
+ ));
739
+ }
740
+ if (workflowEvidence && !expectedSha) {
741
+ blockers.push(buildBlocker(
742
+ 'missing_workflow_sha',
743
+ 'Workflow dispatch requires the HEAD SHA that will be verified after dispatch.'
744
+ ));
745
+ }
746
+ if (workflowEvidence && expectedSha && headSha && expectedSha !== headSha) {
747
+ blockers.push(buildBlocker(
748
+ 'workflow_sha_mismatch',
749
+ `Workflow dispatch expects SHA ${expectedSha}, but repository HEAD is ${headSha}.`,
750
+ { expectedSha, headSha }
751
+ ));
752
+ }
753
+ if (workflowEvidence && !expectedJob) {
754
+ blockers.push(buildBlocker(
755
+ 'missing_workflow_job',
756
+ 'Workflow dispatch requires the expected job name to verify before reporting the run URL.'
757
+ ));
758
+ }
759
+ }
760
+
668
761
  if (commandInfo.isPrMerge && /--admin\b/i.test(commandInfo.text)) {
669
762
  blockers.push(buildBlocker(
670
763
  'admin_merge_bypass_forbidden',
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ function buildOtelDeclarativeConfig(input = {}) {
5
+ const serviceName = input.serviceName || 'thumbgate-agent-harness';
6
+ const environment = input.environment || 'production';
7
+ return {
8
+ file: 'otel.yaml',
9
+ envVar: 'OTEL_CONFIG_FILE',
10
+ config: {
11
+ resource: {
12
+ attributes: {
13
+ 'service.name': serviceName,
14
+ 'deployment.environment': environment,
15
+ },
16
+ },
17
+ traces: {
18
+ sampler: input.sampler || 'parentbased_traceidratio',
19
+ ratio: Number.isFinite(Number(input.ratio)) ? Number(input.ratio) : 0.25,
20
+ dropAttributes: ['authorization', 'cookie', 'x-api-key'],
21
+ },
22
+ metrics: {
23
+ exportIntervalMs: Number.isFinite(Number(input.exportIntervalMs)) ? Number(input.exportIntervalMs) : 60000,
24
+ },
25
+ logs: {
26
+ redactAttributes: ['prompt', 'toolInput', 'secret', 'token'],
27
+ },
28
+ },
29
+ policy: {
30
+ versionControlled: true,
31
+ reviewedBeforeProduction: true,
32
+ dynamicReloadAllowed: input.dynamicReloadAllowed === true,
33
+ },
34
+ };
35
+ }
36
+
37
+ function evaluateOtelConfig(config = {}) {
38
+ const issues = [];
39
+ const payload = config.config || config;
40
+ if (!payload.resource?.attributes?.['service.name']) issues.push('missing_service_name');
41
+ if (!payload.traces) issues.push('missing_trace_pipeline');
42
+ if (!payload.metrics) issues.push('missing_metric_pipeline');
43
+ if (!payload.logs) issues.push('missing_log_pipeline');
44
+ if (!Array.isArray(payload.traces?.dropAttributes) || !payload.traces.dropAttributes.includes('authorization')) {
45
+ issues.push('missing_sensitive_trace_attribute_drop');
46
+ }
47
+ return {
48
+ decision: issues.length === 0 ? 'allow' : 'warn',
49
+ issues,
50
+ };
51
+ }
52
+
53
+ module.exports = {
54
+ buildOtelDeclarativeConfig,
55
+ evaluateOtelConfig,
56
+ };