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.
- package/.claude-plugin/marketplace.json +6 -6
- package/.claude-plugin/plugin.json +3 -3
- package/.well-known/llms.txt +5 -5
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +60 -35
- package/adapters/chatgpt/openapi.yaml +118 -2
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/mcp/server-stdio.js +217 -84
- package/adapters/opencode/opencode.json +1 -1
- package/bench/prompt-eval-suite.json +5 -1
- package/bin/cli.js +211 -8
- package/config/enforcement.json +59 -7
- package/config/evals/agent-safety-eval.json +338 -22
- package/config/gates/default.json +33 -0
- package/config/gates/routine.json +43 -0
- package/config/github-about.json +3 -3
- package/config/mcp-allowlists.json +4 -0
- package/config/merge-quality-checks.json +2 -1
- package/config/model-candidates.json +131 -0
- package/openapi/openapi.yaml +118 -2
- package/package.json +70 -51
- package/public/blog.html +7 -7
- package/public/codex-plugin.html +13 -7
- package/public/compare.html +29 -23
- package/public/dashboard.html +105 -12
- package/public/guide.html +28 -28
- package/public/index.html +233 -97
- package/public/learn.html +87 -20
- package/public/lessons.html +26 -2
- package/public/numbers.html +271 -0
- package/public/pro.html +89 -19
- package/scripts/agent-audit-trace.js +55 -0
- package/scripts/agent-memory-lifecycle.js +96 -0
- package/scripts/agent-readiness-plan.js +118 -0
- package/scripts/agentic-data-pipeline.js +21 -1
- package/scripts/agents-sdk-sandbox-plan.js +57 -0
- package/scripts/ai-org-governance.js +98 -0
- package/scripts/ai-search-distribution.js +43 -0
- package/scripts/artifact-agent-plan.js +81 -0
- package/scripts/billing.js +27 -8
- package/scripts/cli-feedback.js +2 -1
- package/scripts/cli-schema.js +60 -5
- package/scripts/code-mode-mcp-plan.js +71 -0
- package/scripts/commercial-offer.js +1 -1
- package/scripts/context-engine.js +1 -2
- package/scripts/context-manager.js +4 -1
- package/scripts/contextfs.js +214 -32
- package/scripts/dashboard-render-spec.js +1 -1
- package/scripts/dashboard.js +275 -9
- package/scripts/decision-journal.js +13 -3
- package/scripts/document-workflow-governance.js +62 -0
- package/scripts/enterprise-agent-rollout.js +34 -0
- package/scripts/experience-replay-governance.js +69 -0
- package/scripts/export-hf-dataset.js +1 -1
- package/scripts/feedback-loop.js +141 -9
- package/scripts/feedback-to-rules.js +17 -23
- package/scripts/gates-engine.js +4 -6
- package/scripts/growth-campaigns.js +49 -0
- package/scripts/harness-selector.js +145 -1
- package/scripts/hybrid-supervisor-agent.js +64 -0
- package/scripts/inference-cache-policy.js +72 -0
- package/scripts/inference-economics.js +53 -0
- package/scripts/internal-agent-bootstrap.js +12 -2
- package/scripts/knowledge-layer-plan.js +108 -0
- package/scripts/lesson-canonical.js +181 -0
- package/scripts/lesson-db.js +71 -10
- package/scripts/lesson-inference.js +183 -44
- package/scripts/lesson-search.js +4 -1
- package/scripts/lesson-synthesis.js +23 -2
- package/scripts/llm-client.js +157 -26
- package/scripts/mailer/resend-mailer.js +112 -1
- package/scripts/mcp-transport-strategy.js +66 -0
- package/scripts/memory-store-governance.js +60 -0
- package/scripts/meta-agent-loop.js +7 -13
- package/scripts/model-access-eligibility.js +38 -0
- package/scripts/model-migration-readiness.js +55 -0
- package/scripts/native-messaging-audit.js +514 -0
- package/scripts/operational-integrity.js +96 -3
- package/scripts/otel-declarative-config.js +56 -0
- package/scripts/perplexity-client.js +1 -1
- package/scripts/post-training-governance.js +34 -0
- package/scripts/pr-manager.js +47 -7
- package/scripts/private-core-boundary.js +72 -0
- package/scripts/production-agent-readiness.js +40 -0
- package/scripts/profile-router.js +16 -1
- package/scripts/prompt-eval.js +564 -32
- package/scripts/prompt-programs.js +93 -0
- package/scripts/provider-action-normalizer.js +585 -0
- package/scripts/rule-validator.js +285 -0
- package/scripts/scaling-law-claims.js +60 -0
- package/scripts/security-scanner.js +1 -1
- package/scripts/self-distill-agent.js +7 -32
- package/scripts/seo-gsd.js +400 -43
- package/scripts/skill-rag-router.js +53 -0
- package/scripts/spec-gate.js +1 -1
- package/scripts/student-consistent-training.js +73 -0
- package/scripts/synthetic-data-provenance.js +98 -0
- package/scripts/task-context-result.js +81 -0
- package/scripts/telemetry-analytics.js +149 -0
- package/scripts/thompson-sampling.js +2 -2
- package/scripts/token-savings.js +7 -6
- package/scripts/token-tco.js +46 -0
- package/scripts/tool-registry.js +75 -3
- package/scripts/verification-loop.js +10 -1
- package/scripts/verifier-scoring.js +71 -0
- package/scripts/workflow-sentinel.js +284 -28
- package/scripts/workspace-agent-routines.js +118 -0
- package/skills/thumbgate/SKILL.md +1 -1
- package/src/api/server.js +434 -120
- package/.claude-plugin/README.md +0 -170
- package/adapters/README.md +0 -12
- package/scripts/analytics-report.js +0 -328
- package/scripts/autonomous-workflow.js +0 -377
- package/scripts/billing-setup.js +0 -109
- package/scripts/creator-campaigns.js +0 -239
- package/scripts/cross-encoder-reranker.js +0 -235
- package/scripts/daemon-manager.js +0 -108
- package/scripts/decision-trace.js +0 -354
- package/scripts/delegation-runtime.js +0 -896
- package/scripts/dispatch-brief.js +0 -159
- package/scripts/distribution-surfaces.js +0 -110
- package/scripts/feedback-history-distiller.js +0 -382
- package/scripts/funnel-analytics.js +0 -35
- package/scripts/history-distiller.js +0 -200
- package/scripts/hosted-job-launcher.js +0 -256
- package/scripts/intent-router.js +0 -392
- package/scripts/lesson-reranker.js +0 -263
- package/scripts/lesson-retrieval.js +0 -148
- package/scripts/managed-lesson-agent.js +0 -183
- package/scripts/operational-dashboard.js +0 -103
- package/scripts/operational-summary.js +0 -129
- package/scripts/operator-artifacts.js +0 -608
- package/scripts/optimize-context.js +0 -17
- package/scripts/org-dashboard.js +0 -206
- package/scripts/partner-orchestration.js +0 -146
- package/scripts/predictive-insights.js +0 -356
- package/scripts/pulse.js +0 -80
- package/scripts/reflector-agent.js +0 -221
- package/scripts/sales-pipeline.js +0 -681
- package/scripts/session-episode-store.js +0 -329
- package/scripts/session-health-sensor.js +0 -242
- package/scripts/session-report.js +0 -120
- package/scripts/swarm-coordinator.js +0 -81
- package/scripts/tool-kpi-tracker.js +0 -12
- package/scripts/webhook-delivery.js +0 -62
- package/scripts/workflow-sprint-intake.js +0 -475
- package/skills/agent-memory/SKILL.md +0 -97
- package/skills/solve-architecture-autonomy/SKILL.md +0 -17
- package/skills/solve-architecture-autonomy/tool.js +0 -33
- 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
|
|
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
|
+
};
|