thumbgate 1.35.0 → 1.37.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/.agents/skills/cyberstrike-compare-not-clone/SKILL.md +36 -0
  2. package/.agents/skills/gitlab-sandbox-allowlist-not-trust/SKILL.md +77 -0
  3. package/.agents/skills/jit-harness-compare-not-clone/SKILL.md +34 -0
  4. package/.agents/skills/openui-catalog-compose-honesty/SKILL.md +64 -0
  5. package/.agents/skills/zvec-grep-compare-not-clone/SKILL.md +34 -0
  6. package/.claude-plugin/plugin.json +1 -1
  7. package/.well-known/llms.txt +1 -0
  8. package/.well-known/mcp/server-card.json +1 -1
  9. package/CONTRIBUTING.md +95 -0
  10. package/README.md +195 -632
  11. package/THIRD_PARTY_NOTICES.md +89 -0
  12. package/adapters/claude/.mcp.json +2 -2
  13. package/adapters/forge/forge.yaml +3 -3
  14. package/adapters/future-agi/.mcp.json +8 -0
  15. package/adapters/future-agi/FUTURE_AGI.md +23 -0
  16. package/adapters/future-agi/config.toml +3 -0
  17. package/adapters/future-agi/future-agi-bridge.js +9 -0
  18. package/adapters/future-agi/opencode.json +8 -0
  19. package/adapters/herdr/herdr-plugin.toml +18 -0
  20. package/adapters/mcp/server-stdio.js +238 -25
  21. package/adapters/opencode/opencode.json +1 -1
  22. package/adapters/workos/WORKOS.md +52 -0
  23. package/bin/cli.js +336 -3
  24. package/bin/futureagi-bridge +9 -0
  25. package/config/gate-templates.json +581 -4
  26. package/config/gates/actor-critic-audit.json +34 -0
  27. package/config/gates/default.json +9 -3
  28. package/config/gates/five-walls-governance.json +34 -0
  29. package/config/gates/future-agi-guardrails.json +34 -0
  30. package/config/gates/radware-threat-defense-2026.json +61 -0
  31. package/config/gates/simatree-data-governance.json +33 -0
  32. package/config/mcp-allowlists.json +4 -0
  33. package/config/merge-quality-checks.json +6 -0
  34. package/config/model-candidates.json +312 -29
  35. package/config/model-tiers.json +18 -0
  36. package/config/post-deploy-marketing-pages.json +10 -0
  37. package/config/progressive/01-wire-only.json +11 -0
  38. package/config/progressive/02-dashboard-empty-ok.json +10 -0
  39. package/config/progressive/03-one-lesson.json +10 -0
  40. package/config/progressive/04-warn-fires.json +11 -0
  41. package/config/progressive/05-strict-optional.json +11 -0
  42. package/config/progressive/README.md +15 -0
  43. package/config/schemas/broker-execution-receipt.schema.json +139 -0
  44. package/config/schemas/provider-execution-attestation-v1.schema.json +58 -0
  45. package/conformance/provider-attestation/vectors.json +320 -0
  46. package/docs/specs/provider-execution-attestation-v1.md +69 -0
  47. package/openapi/openapi.yaml +15 -0
  48. package/package.json +390 -148
  49. package/public/about.html +2 -2
  50. package/public/ai-malpractice-prevention.html +7 -7
  51. package/public/blog/a-10-dollar-vps-is-not-a-computer.html +143 -0
  52. package/public/blog/a-receipt-is-not-world-state.html +388 -0
  53. package/public/blog/git-at-agent-scale.html +374 -0
  54. package/public/blog/no-llm-in-the-gate.html +133 -0
  55. package/public/blog.html +80 -0
  56. package/public/case-studies.html +16 -1
  57. package/public/compare.html +28 -0
  58. package/public/diagnostic.html +216 -7
  59. package/public/docs/connectors.html +39 -0
  60. package/public/federal.html +2 -2
  61. package/public/founders.html +639 -0
  62. package/public/index.html +87 -9
  63. package/public/install.html +8 -8
  64. package/public/learn.html +39 -0
  65. package/public/numbers.html +2 -2
  66. package/public/peter.html +310 -0
  67. package/public/platform-partners.html +119 -0
  68. package/public/pricing.html +24 -3
  69. package/public/privacy.html +117 -0
  70. package/public/pro.html +17 -0
  71. package/public/support.html +62 -0
  72. package/public/terms.html +130 -0
  73. package/public/third-party-notices.html +95 -0
  74. package/public/yt.html +351 -0
  75. package/scripts/action-receipts.js +133 -3
  76. package/scripts/adaptive-governance-arena.js +349 -0
  77. package/scripts/admin-override.js +205 -0
  78. package/scripts/agent-action-inventory.js +869 -0
  79. package/scripts/agent-audit-trace.js +42 -2
  80. package/scripts/agent-egress-policy.js +1117 -0
  81. package/scripts/agent-memory-lifecycle.js +141 -2
  82. package/scripts/agent-operations-planner.js +441 -1
  83. package/scripts/agent-readiness.js +68 -0
  84. package/scripts/agent-security-central.js +647 -0
  85. package/scripts/allowlist-bridge-honesty.js +417 -0
  86. package/scripts/async-job-runner.js +102 -11
  87. package/scripts/audit-trail.js +212 -0
  88. package/scripts/billing.js +1 -1
  89. package/scripts/broker-execution-receipts.js +719 -0
  90. package/scripts/budget-aware-gates-proof.js +423 -0
  91. package/scripts/claude-feedback-sync.js +29 -3
  92. package/scripts/claw-harness-production.js +237 -0
  93. package/scripts/cli-schema.js +163 -1
  94. package/scripts/codex-runbook-flywheel.js +318 -0
  95. package/scripts/context-footprint.js +186 -0
  96. package/scripts/contextfs.js +143 -61
  97. package/scripts/dashboard.js +251 -32
  98. package/scripts/deepseek-v4-runtime-guardrails.js +72 -6
  99. package/scripts/docker-sandbox-planner.js +18 -0
  100. package/scripts/double-blind-eval-protocol.js +252 -0
  101. package/scripts/edotenv-rl-gateway.js +259 -0
  102. package/scripts/ensure-production-search-corpus.js +162 -0
  103. package/scripts/eval-holdout.js +311 -0
  104. package/scripts/feedback-aggregate.js +21 -2
  105. package/scripts/feedback-loop.js +87 -5
  106. package/scripts/feedback-quality.js +9 -0
  107. package/scripts/file-ledger-lock.js +4 -1
  108. package/scripts/financial-control-plane.js +41 -1
  109. package/scripts/find-dormant-requires.js +118 -0
  110. package/scripts/fs-utils.js +84 -8
  111. package/scripts/gates-engine.js +810 -63
  112. package/scripts/generate-case-study-outreach.js +24 -15
  113. package/scripts/git-at-scale.js +628 -0
  114. package/scripts/governance-conflict-audit.js +1650 -0
  115. package/scripts/governance-difficulty-curriculum.js +328 -0
  116. package/scripts/graphrag-retrieval.js +275 -0
  117. package/scripts/gurobi-optimizer.js +324 -0
  118. package/scripts/gurobi_optimizer.py +485 -0
  119. package/scripts/harness-selector.js +82 -1
  120. package/scripts/hidden-entry-points.js +284 -0
  121. package/scripts/human-escalation.js +199 -1
  122. package/scripts/hybrid-feedback-context.js +152 -19
  123. package/scripts/intent-governed-execution.js +602 -0
  124. package/scripts/intervention-policy.js +123 -20
  125. package/scripts/jit-harness-compose.js +628 -0
  126. package/scripts/jsonl-watcher.js +10 -0
  127. package/scripts/lesson-embedding-index.js +95 -12
  128. package/scripts/lesson-retrieval.js +105 -19
  129. package/scripts/local-model-profile.js +19 -2
  130. package/scripts/mailer/resend-mailer.js +1 -1
  131. package/scripts/matryoshka-embedding.js +235 -0
  132. package/scripts/mcp-oauth.js +42 -4
  133. package/scripts/mcp-session-handles.js +1016 -0
  134. package/scripts/mcp-wiring-doctor.js +314 -0
  135. package/scripts/memory-firewall.js +115 -2
  136. package/scripts/memory-scope-readiness.js +299 -0
  137. package/scripts/memory-vs-rag-route.js +161 -0
  138. package/scripts/model-tier-router.js +148 -21
  139. package/scripts/nvidia-specdecode-al-doctor.js +536 -0
  140. package/scripts/openui-catalog-compose-honesty.js +593 -0
  141. package/scripts/operational-integrity.js +19 -1
  142. package/scripts/override-audit.js +213 -0
  143. package/scripts/package-manager-honesty-doctor.js +458 -0
  144. package/scripts/pr-manager.js +63 -1
  145. package/scripts/prove-herdr-adapter.js +52 -0
  146. package/scripts/prove-memory-pyramid-and-symbolic-canvas.js +95 -0
  147. package/scripts/prove-workos.js +73 -0
  148. package/scripts/provider-attestation-conformance.js +192 -0
  149. package/scripts/provider-receipt-contract.js +136 -0
  150. package/scripts/qwen38-max-cost-optimizer.js +401 -0
  151. package/scripts/radware-threat-defense.js +280 -0
  152. package/scripts/rag-embedding-identity.js +221 -0
  153. package/scripts/rag-precision-guardrails.js +112 -2
  154. package/scripts/remote-feedback-capture.js +159 -0
  155. package/scripts/research-agent-harness.js +256 -0
  156. package/scripts/rsi-safety-hillclimb.js +200 -0
  157. package/scripts/rule-sprawl.js +188 -0
  158. package/scripts/schedule-manager.js +147 -0
  159. package/scripts/self-heal.js +8 -0
  160. package/scripts/session-lease.js +415 -0
  161. package/scripts/simatree-data-governance.js +347 -0
  162. package/scripts/slo-alert-engine.js +172 -7
  163. package/scripts/solver-parity.js +539 -0
  164. package/scripts/stealth-memory-injection-gate.js +333 -0
  165. package/scripts/switchyard-router.js +366 -0
  166. package/scripts/telemetry-analytics.js +84 -27
  167. package/scripts/temporal-decay-weighting.js +138 -0
  168. package/scripts/test-all.js +165 -0
  169. package/scripts/token-savings.js +42 -0
  170. package/scripts/tool-kpi-tracker.js +108 -5
  171. package/scripts/tool-registry.js +193 -5
  172. package/scripts/universal-claim-evaluator.js +14 -2
  173. package/scripts/vector-store.js +279 -9
  174. package/scripts/workflow-notebook.js +391 -0
  175. package/scripts/workflow-sentinel.js +111 -12
  176. package/scripts/workos-production-guard.js +260 -0
  177. package/scripts/workspace-search-route.js +515 -0
  178. package/server.json +2 -2
  179. package/src/agent-identity-boundary.js +76 -0
  180. package/src/agent-retrieval-cache.js +155 -0
  181. package/src/alert-noise-ledger.js +502 -0
  182. package/src/api/server.js +724 -153
  183. package/src/git-fast-cache.js +220 -0
  184. package/src/git-wal-sync.js +156 -0
  185. package/src/hash-anchored-edit.js +82 -0
  186. package/src/hermes-platform-protocol.js +475 -0
  187. package/src/hermes-sync-plane.js +241 -0
  188. package/src/index.js +30 -1
  189. package/src/iso42001-compliance-guard.js +97 -0
  190. package/src/latency-budget.js +244 -0
  191. package/src/mcp-writeguard.js +316 -0
  192. package/src/miminions-adapter.js +106 -0
  193. package/src/pipeline-compass.js +104 -0
  194. package/src/ppl-alert-pipeline.js +284 -0
  195. package/src/rendezvous-router.js +90 -0
  196. package/src/security-questionnaire.js +195 -0
@@ -0,0 +1,417 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Allowlist-bridge honesty doctor (GitLab AI-sandbox FORMAT steal).
6
+ *
7
+ * Source: https://www.infoq.com/news/2026/09/gitlab-ai-sandbox-access/
8
+ * Upstream analysis: https://about.gitlab.com/blog/ai-agent-sandbox/
9
+ *
10
+ * Transfers (FORMAT, not a GitLab Duo sandbox clone):
11
+ * 1. Network allowlists are hops, not trust boundaries. Package registries,
12
+ * proxies, and Hugging Face hosts remain attack surface even when permitted.
13
+ * 2. Trust handoff: writes consumed by privileged out-of-sandbox processes
14
+ * (hooks, CI, MCP, git config) are not "contained" by the sandbox.
15
+ * 3. Observe→allowlist must not auto-promote bridge hosts into trusted allowHosts.
16
+ *
17
+ * Does NOT clone GitLab Duo Agent Platform, does not add a sandbox SKU, and
18
+ * does not treat an allowlisted registry as a safe place to send credentials.
19
+ */
20
+
21
+ const fs = require('node:fs');
22
+ const path = require('node:path');
23
+
24
+ const SOURCE_URL = 'https://www.infoq.com/news/2026/09/gitlab-ai-sandbox-access/';
25
+
26
+ const {
27
+ classifyHostRole,
28
+ requestCarriesSecrets,
29
+ draftPolicyFromObservations,
30
+ evaluateEgressStaticOnly,
31
+ BRIDGE_HOST_SUFFIXES,
32
+ } = require('./agent-egress-policy');
33
+
34
+ const HANDOFF_PATH_PATTERNS = [
35
+ /^\.github\/workflows\//i,
36
+ /^\.githooks\//i,
37
+ /^\.git\/hooks\//i,
38
+ /^\.git\/config$/i,
39
+ /^\.mcp\.json$/i,
40
+ /^config\/mcp-allowlists\.json$/i,
41
+ /^config\/gates\//i,
42
+ /^hooks\/hooks\.json$/i,
43
+ /^\.claude\/settings\.json$/i,
44
+ ];
45
+
46
+ function normalizeBoolean(value) {
47
+ if (value === true) return true;
48
+ if (value === false || value === undefined || value === null) return false;
49
+ return /^(1|true|yes|on)$/i.test(String(value).trim());
50
+ }
51
+
52
+ function normalizeHost(value) {
53
+ return String(value || '')
54
+ .trim()
55
+ .toLowerCase()
56
+ .replace(/^https?:\/\//, '')
57
+ .split('/')[0]
58
+ .split(':')[0];
59
+ }
60
+
61
+ function classifyTrustHandoffPath(filePath) {
62
+ const rel = String(filePath || '').replace(/\\/g, '/').replace(/^\.\//, '');
63
+ const hit = HANDOFF_PATH_PATTERNS.some((re) => re.test(rel));
64
+ return {
65
+ path: rel,
66
+ handoff: hit,
67
+ contained: false,
68
+ reason: hit
69
+ ? 'Write is consumed by a privileged process outside the agent sandbox (CSA trust-handoff).'
70
+ : 'Path is not a known privileged consumer.',
71
+ };
72
+ }
73
+
74
+ /**
75
+ * Escape every RegExp metacharacter, backslash included.
76
+ * CodeQL js/incomplete-sanitization flags replace(/\./g, '\\.') because a
77
+ * leading backslash would survive. Keep backslash in the character class.
78
+ */
79
+ function escapeRegExp(value) {
80
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
81
+ }
82
+
83
+ const EXTRA_GATE_HOSTS = [
84
+ 'github.com',
85
+ 'api.github.com',
86
+ 'api.anthropic.com',
87
+ 'thumbgate.ai',
88
+ 'thumbgate-production.up.railway.app',
89
+ 'localhost',
90
+ '127.0.0.1',
91
+ ];
92
+
93
+ function extractHostsFromGatePattern(pattern) {
94
+ const source = String(pattern || '');
95
+ const found = new Set();
96
+ for (const host of [...BRIDGE_HOST_SUFFIXES, ...EXTRA_GATE_HOSTS]) {
97
+ const escaped = escapeRegExp(host);
98
+ if (source.indexOf(escaped) !== -1) found.add(host);
99
+ }
100
+ return [...found];
101
+ }
102
+
103
+ function loadDenyNetworkEgressGate(rootDir) {
104
+ const gatePath = path.join(rootDir, 'config', 'gates', 'default.json');
105
+ if (!fs.existsSync(gatePath)) return { path: gatePath, hosts: [], present: false };
106
+ let parsed;
107
+ try {
108
+ parsed = JSON.parse(fs.readFileSync(gatePath, 'utf8'));
109
+ } catch {
110
+ return { path: gatePath, hosts: [], present: false, parseError: true };
111
+ }
112
+ const gates = parsed.gates || parsed.rules || [];
113
+ const gate = gates.find((g) => g && g.id === 'deny-network-egress');
114
+ if (!gate) return { path: gatePath, hosts: [], present: false };
115
+ return {
116
+ path: gatePath,
117
+ present: true,
118
+ hosts: extractHostsFromGatePattern(gate.pattern),
119
+ action: gate.action || null,
120
+ };
121
+ }
122
+
123
+ function auditAllowlist(allowHosts = [], options = {}) {
124
+ const findings = [];
125
+ const classified = [];
126
+ const treatAsTrust = normalizeBoolean(options.treatAllowlistAsTrustBoundary);
127
+ const trustedClaim = normalizeBoolean(options.trusted);
128
+
129
+ for (const raw of allowHosts) {
130
+ const host = normalizeHost(raw);
131
+ if (!host) continue;
132
+ const role = classifyHostRole(host);
133
+ classified.push(role);
134
+ if (role.role === 'bridge' && (treatAsTrust || trustedClaim || role.trusted === true)) {
135
+ findings.push({
136
+ id: 'allowlist_treated_as_trust_boundary',
137
+ severity: 'high',
138
+ host,
139
+ role: role.role,
140
+ message: `Allowlisted bridge host '${host}' is being treated as a trust boundary. GitLab's package-proxy escape shows the hop is still attack surface.`,
141
+ });
142
+ }
143
+ }
144
+ return { classified, findings };
145
+ }
146
+
147
+ function auditDraftPromotion(observations = [], options = {}) {
148
+ const drafted = draftPolicyFromObservations(observations, options);
149
+ const findings = [];
150
+ for (const host of drafted.allowHosts || []) {
151
+ const role = classifyHostRole(host);
152
+ if (role.role === 'bridge') {
153
+ findings.push({
154
+ id: 'observe_promoted_bridge_host',
155
+ severity: 'high',
156
+ host,
157
+ message: `Observe-mode traffic to bridge host '${host}' was promoted onto allowHosts. That is the GitLab allowlist-as-bridge failure.`,
158
+ });
159
+ }
160
+ }
161
+ return { drafted, findings };
162
+ }
163
+
164
+ function auditCredentialedBridge(request = {}, policy = {}) {
165
+ const verdict = evaluateEgressStaticOnly(request, policy);
166
+ const host = (verdict.target && verdict.target.host) || normalizeHost(request.url || request.host);
167
+ const role = classifyHostRole(host);
168
+ const findings = [];
169
+ if (role.role === 'bridge' && requestCarriesSecrets(request) && verdict.action === 'allow') {
170
+ findings.push({
171
+ id: 'bridge_host_accepted_credentials',
172
+ severity: 'high',
173
+ host,
174
+ message: `Credentialed request to bridge host '${host}' was allowed. Independent auth is required; allowlist is not enough.`,
175
+ });
176
+ }
177
+ return { verdict, findings };
178
+ }
179
+
180
+ function auditTrustHandoff(writes = [], options = {}) {
181
+ const findings = [];
182
+ const classified = [];
183
+ const claimedContained = normalizeBoolean(options.claimedContained);
184
+ for (const filePath of writes) {
185
+ const row = classifyTrustHandoffPath(filePath);
186
+ classified.push(row);
187
+ if (row.handoff && claimedContained) {
188
+ findings.push({
189
+ id: 'trust_handoff_claimed_contained',
190
+ severity: 'high',
191
+ path: row.path,
192
+ message: `Write to '${row.path}' is a trust handoff. The agent stayed in-sandbox; a privileged consumer outside will execute with higher privilege.`,
193
+ });
194
+ }
195
+ }
196
+ return { classified, findings };
197
+ }
198
+
199
+ function normalizeOptions(raw = {}) {
200
+ const allowHosts = [];
201
+ const rawHosts = raw.allowHosts || raw['allow-hosts'] || raw.hosts || [];
202
+ const hostList = Array.isArray(rawHosts) ? rawHosts : String(rawHosts || '').split(',');
203
+ for (const host of hostList) allowHosts.push(host);
204
+ if (raw.host) allowHosts.push(String(raw.host));
205
+
206
+ const writes = [];
207
+ const rawWrites = raw.writes || raw.write || raw['writes'] || [];
208
+ const writeList = Array.isArray(rawWrites) ? rawWrites : String(rawWrites || '').split(',');
209
+ for (const filePath of writeList) writes.push(filePath);
210
+
211
+ const observations = Array.isArray(raw.observations) ? raw.observations : [];
212
+
213
+ return {
214
+ root: raw.root ? path.resolve(String(raw.root)) : process.cwd(),
215
+ json: normalizeBoolean(raw.json),
216
+ strict: normalizeBoolean(raw.strict),
217
+ treatAllowlistAsTrustBoundary: normalizeBoolean(
218
+ raw.treatAllowlistAsTrustBoundary || raw['treat-allowlist-as-trust']
219
+ ),
220
+ trusted: normalizeBoolean(raw.trusted),
221
+ claimedContained: normalizeBoolean(raw.claimedContained || raw['claimed-contained']),
222
+ cloneGitLabDuo: normalizeBoolean(
223
+ raw.cloneGitLabDuo || raw['clone-gitlab-duo'] || raw.sandboxSku || raw['sandbox-sku']
224
+ ),
225
+ allowHosts: allowHosts.map(normalizeHost).filter(Boolean),
226
+ writes,
227
+ observations,
228
+ evaluateUrl: raw.evaluateUrl || raw['evaluate-url'] || raw.url || null,
229
+ authorization: raw.authorization || raw.Authorization || null,
230
+ independentAuth: normalizeBoolean(raw.independentAuth || raw['independent-auth']),
231
+ };
232
+ }
233
+
234
+ function buildAllowlistBridgeHonestyReport(raw = {}) {
235
+ const options = normalizeOptions(raw);
236
+ const findings = [];
237
+ const nextActions = [];
238
+
239
+ if (options.cloneGitLabDuo) {
240
+ findings.push({
241
+ id: 'refuse_sku_clone',
242
+ severity: 'high',
243
+ message: 'Refuse GitLab Duo / execution-sandbox SKU clone. Steal allowlist-as-bridge + trust-handoff honesty onto existing egress/PreToolUse rails.',
244
+ });
245
+ }
246
+
247
+ const gate = loadDenyNetworkEgressGate(options.root);
248
+ const gateHosts = gate.present ? gate.hosts : [];
249
+ const combinedHosts = [...new Set([...options.allowHosts, ...gateHosts])];
250
+
251
+ const allowAudit = auditAllowlist(combinedHosts, options);
252
+ findings.push(...allowAudit.findings);
253
+
254
+ const handoffAudit = auditTrustHandoff(options.writes, options);
255
+ findings.push(...handoffAudit.findings);
256
+
257
+ if (options.observations.length) {
258
+ const draftAudit = auditDraftPromotion(options.observations, { agentId: 'doctor' });
259
+ findings.push(...draftAudit.findings);
260
+ }
261
+
262
+ let evaluate = null;
263
+ if (options.evaluateUrl) {
264
+ const headers = {};
265
+ if (options.authorization) headers.Authorization = String(options.authorization);
266
+ evaluate = auditCredentialedBridge(
267
+ {
268
+ url: options.evaluateUrl,
269
+ headers,
270
+ independentAuth: options.independentAuth,
271
+ },
272
+ { allowHosts: combinedHosts }
273
+ );
274
+ findings.push(...evaluate.findings);
275
+ }
276
+
277
+ const bridgeHosts = allowAudit.classified.filter((row) => row.role === 'bridge');
278
+ if (bridgeHosts.length && !findings.some((f) => f.severity === 'high')) {
279
+ nextActions.push('Keep bridge hosts (registries/proxies/HF) on the hop list; never mark them trusted.');
280
+ nextActions.push('Do not send Authorization / tokens to a package proxy just because it is allowlisted.');
281
+ }
282
+ if (handoffAudit.classified.some((row) => row.handoff)) {
283
+ nextActions.push('Treat hook/CI/MCP writes as trust-handoff, not sandbox-contained execution.');
284
+ }
285
+ if (!nextActions.length) {
286
+ nextActions.push('Allowlists remain hops. Pair with independent auth and least privilege.');
287
+ }
288
+
289
+ const high = findings.filter((f) => f.severity === 'high').length;
290
+ let status = 'ready';
291
+ if (high > 0) status = 'fail';
292
+ else if (findings.length > 0) status = 'actionable';
293
+
294
+ return {
295
+ name: 'thumbgate-allowlist-bridge-honesty',
296
+ source: SOURCE_URL,
297
+ status,
298
+ ok: status !== 'fail',
299
+ allowlistIsTrustBoundary: false,
300
+ clonedGitLabDuo: false,
301
+ metrics: {
302
+ allowHostCount: combinedHosts.length,
303
+ bridgeHostCount: bridgeHosts.length,
304
+ handoffWriteCount: handoffAudit.classified.filter((row) => row.handoff).length,
305
+ findingCount: findings.length,
306
+ highFindingCount: high,
307
+ denyNetworkEgressGate: Boolean(gate.present),
308
+ },
309
+ classifiedHosts: allowAudit.classified,
310
+ handoffWrites: handoffAudit.classified,
311
+ evaluate: evaluate ? evaluate.verdict : null,
312
+ findings,
313
+ nextActions,
314
+ exampleCommand: 'npx thumbgate allowlist-bridge-honesty --json',
315
+ disclaimer: 'FORMAT steal of GitLab sandbox-network honesty. Not GitLab Duo, not a containment SKU.',
316
+ };
317
+ }
318
+
319
+ function formatAllowlistBridgeHonestyReport(report) {
320
+ const lines = [
321
+ `Allowlist-bridge honesty: ${report.status}`,
322
+ `Allowlist is trust boundary: ${report.allowlistIsTrustBoundary}`,
323
+ `Bridge hosts: ${report.metrics.bridgeHostCount}/${report.metrics.allowHostCount}`,
324
+ `Source : ${report.source}`,
325
+ ];
326
+ if (report.findings.length) {
327
+ lines.push('', 'Findings:');
328
+ for (const f of report.findings) {
329
+ lines.push(` - [${f.severity}] ${f.id}`);
330
+ lines.push(` ${f.message}`);
331
+ }
332
+ }
333
+ lines.push('', 'Next actions:');
334
+ for (const a of report.nextActions) lines.push(` - ${a}`);
335
+ lines.push('', `Example: ${report.exampleCommand}`);
336
+ lines.push(`Note: ${report.disclaimer}`, '');
337
+ return `${lines.join('\n')}\n`;
338
+ }
339
+
340
+ function parseCliArgs(argv) {
341
+ const options = { allowHosts: [], writes: [] };
342
+ for (const arg of argv) {
343
+ if (arg === '--json') { options.json = true; continue; }
344
+ if (arg === '--strict') { options.strict = true; continue; }
345
+ if (arg === '--help' || arg === '-h') { options.help = true; continue; }
346
+ const m = /^--([^=]+)(?:=(.*))?$/.exec(arg);
347
+ if (!m) continue;
348
+ const key = m[1];
349
+ const value = m[2] === undefined ? true : m[2];
350
+ if (key === 'allow-hosts' || key === 'host') {
351
+ String(value).split(',').forEach((h) => options.allowHosts.push(h));
352
+ continue;
353
+ }
354
+ if (key === 'write' || key === 'writes') {
355
+ String(value).split(',').forEach((w) => options.writes.push(w));
356
+ continue;
357
+ }
358
+ options[key] = value;
359
+ }
360
+ return options;
361
+ }
362
+
363
+ function printHelp() {
364
+ process.stdout.write(`Usage: node scripts/allowlist-bridge-honesty.js [flags]
365
+
366
+ Flags:
367
+ --root=DIR Repo root (default: cwd)
368
+ --allow-hosts=h1,h2 Extra allowlisted hosts to classify
369
+ --treat-allowlist-as-trust Fail closed if bridge hosts are treated as trusted
370
+ --write=path Privileged consumer path to classify as trust-handoff
371
+ --claimed-contained Fail if a handoff write is claimed sandbox-contained
372
+ --evaluate-url=URL Probe egress evaluation
373
+ --authorization=VALUE Fake credential header for the probe (never a live secret)
374
+ --clone-gitlab-duo Refuse SKU clone
375
+ --strict Exit 1 on fail
376
+ --json
377
+
378
+ Source: ${SOURCE_URL}
379
+ `);
380
+ }
381
+
382
+ function runCli(argv = process.argv.slice(2)) {
383
+ const args = parseCliArgs(argv);
384
+ if (args.help) {
385
+ printHelp();
386
+ return 0;
387
+ }
388
+ const report = buildAllowlistBridgeHonestyReport(args);
389
+ if (args.json) process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
390
+ else process.stdout.write(formatAllowlistBridgeHonestyReport(report));
391
+ if (args.strict && report.status === 'fail') return 1;
392
+ if (report.status === 'fail') return 1;
393
+ return 0;
394
+ }
395
+
396
+ module.exports = {
397
+ SOURCE_URL,
398
+ HANDOFF_PATH_PATTERNS,
399
+ EXTRA_GATE_HOSTS,
400
+ escapeRegExp,
401
+ classifyTrustHandoffPath,
402
+ extractHostsFromGatePattern,
403
+ loadDenyNetworkEgressGate,
404
+ auditAllowlist,
405
+ auditDraftPromotion,
406
+ auditCredentialedBridge,
407
+ auditTrustHandoff,
408
+ buildAllowlistBridgeHonestyReport,
409
+ formatAllowlistBridgeHonestyReport,
410
+ normalizeOptions,
411
+ parseCliArgs,
412
+ runCli,
413
+ };
414
+
415
+ if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(__filename)) {
416
+ process.exitCode = runCli(process.argv.slice(2));
417
+ }
@@ -448,18 +448,96 @@ function getJobStats() {
448
448
  };
449
449
  }
450
450
 
451
- function shellConfig(command) {
452
- if (process.platform === 'win32') {
453
- return {
454
- command: process.env.ComSpec || 'cmd.exe',
455
- args: ['/d', '/s', '/c', command],
456
- };
451
+ const SAFE_EXECUTABLE_RE = /^[A-Za-z0-9_./\\:+-]+$/;
452
+
453
+ /**
454
+ * Quote-aware argv split for command stages (no shell).
455
+ * Supports single/double quotes; rejects unquoted shell metacharacters.
456
+ */
457
+ function parseCommandArgv(command) {
458
+ const raw = String(command == null ? '' : command).trim();
459
+ if (!raw) {
460
+ const err = new Error('Command stage rejected: empty command');
461
+ err.code = 'JOB_STAGE_FAILED';
462
+ throw err;
457
463
  }
458
464
 
459
- return {
460
- command: process.env.SHELL || '/bin/sh',
461
- args: ['-lc', command],
465
+ const tokens = [];
466
+ let current = '';
467
+ let quote = null;
468
+ let tokenStarted = false;
469
+
470
+ const flushToken = ({ allowEmpty = false } = {}) => {
471
+ if (tokenStarted || (allowEmpty && current === '')) {
472
+ tokens.push(current);
473
+ current = '';
474
+ tokenStarted = false;
475
+ }
462
476
  };
477
+
478
+ for (let i = 0; i < raw.length; i += 1) {
479
+ const ch = raw[i];
480
+ if (quote) {
481
+ if (ch === quote) {
482
+ quote = null;
483
+ // Closing quotes always emits a token (including empty `-e ""`).
484
+ flushToken({ allowEmpty: true });
485
+ } else if (ch === '\\' && quote === '"' && i + 1 < raw.length) {
486
+ current += raw[i + 1];
487
+ tokenStarted = true;
488
+ i += 1;
489
+ } else {
490
+ current += ch;
491
+ tokenStarted = true;
492
+ }
493
+ continue;
494
+ }
495
+
496
+ if (ch === '"' || ch === "'") {
497
+ // Flush any unquoted token abutting the quote (`node -e"code"`).
498
+ flushToken();
499
+ quote = ch;
500
+ tokenStarted = true;
501
+ continue;
502
+ }
503
+
504
+ if (/\s/.test(ch)) {
505
+ flushToken();
506
+ continue;
507
+ }
508
+
509
+ // Unquoted shell metacharacters would require a shell; fail closed.
510
+ if (/[;&|`$<>(){}]/.test(ch)) {
511
+ const err = new Error('Command stage rejected: shell metacharacters are not allowed');
512
+ err.code = 'JOB_STAGE_FAILED';
513
+ throw err;
514
+ }
515
+
516
+ current += ch;
517
+ tokenStarted = true;
518
+ }
519
+
520
+ if (quote) {
521
+ const err = new Error('Command stage rejected: unmatched quote');
522
+ err.code = 'JOB_STAGE_FAILED';
523
+ throw err;
524
+ }
525
+
526
+ flushToken();
527
+
528
+ if (tokens.length === 0) {
529
+ const err = new Error('Command stage rejected: empty command');
530
+ err.code = 'JOB_STAGE_FAILED';
531
+ throw err;
532
+ }
533
+
534
+ if (!SAFE_EXECUTABLE_RE.test(tokens[0])) {
535
+ const err = new Error(`Command stage rejected: invalid executable "${tokens[0]}"`);
536
+ err.code = 'JOB_STAGE_FAILED';
537
+ throw err;
538
+ }
539
+
540
+ return tokens;
463
541
  }
464
542
 
465
543
  function normalizeStageResult(result, currentContext) {
@@ -495,14 +573,26 @@ function applyStageContext(currentContext, stageResult) {
495
573
  }
496
574
 
497
575
  function runCommandStage(stage) {
498
- const shell = shellConfig(stage.command);
499
- const result = spawnSync(shell.command, shell.args, {
576
+ const tokens = parseCommandArgv(stage.command);
577
+ const result = spawnSync(tokens[0], tokens.slice(1), {
500
578
  cwd: stage.workingDirectory || process.cwd(),
501
579
  env: process.env,
502
580
  encoding: 'utf8',
503
581
  stdio: 'pipe',
582
+ shell: false,
504
583
  });
505
584
 
585
+ if (result.error) {
586
+ const error = new Error([
587
+ `Stage "${stage.name}" command failed`,
588
+ result.error.message || 'spawn error',
589
+ ].join(': '));
590
+ error.code = 'JOB_STAGE_FAILED';
591
+ error.stdout = '';
592
+ error.stderr = result.error.message || '';
593
+ throw error;
594
+ }
595
+
506
596
  if (result.status !== 0) {
507
597
  const stderr = (result.stderr || '').trim();
508
598
  const stdout = (result.stdout || '').trim();
@@ -1044,6 +1134,7 @@ module.exports = {
1044
1134
  resumeJob,
1045
1135
  resumeManagedJobs,
1046
1136
  getJobRuntimePaths,
1137
+ parseCommandArgv,
1047
1138
  JOB_LOG_FILENAME,
1048
1139
  JOB_CONTROL_FILENAME,
1049
1140
  JOB_STATE_DIRNAME,