thumbgate 1.34.3 → 1.37.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/cyberstrike-compare-not-clone/SKILL.md +36 -0
- package/.agents/skills/gitlab-sandbox-allowlist-not-trust/SKILL.md +77 -0
- package/.agents/skills/jit-harness-compare-not-clone/SKILL.md +34 -0
- package/.agents/skills/openui-catalog-compose-honesty/SKILL.md +64 -0
- package/.agents/skills/zvec-grep-compare-not-clone/SKILL.md +34 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.well-known/llms.txt +1 -0
- package/.well-known/mcp/server-card.json +1 -1
- package/CONTRIBUTING.md +95 -0
- package/README.md +195 -632
- package/THIRD_PARTY_NOTICES.md +89 -0
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/forge/forge.yaml +3 -3
- package/adapters/future-agi/.mcp.json +8 -0
- package/adapters/future-agi/FUTURE_AGI.md +23 -0
- package/adapters/future-agi/config.toml +3 -0
- package/adapters/future-agi/future-agi-bridge.js +9 -0
- package/adapters/future-agi/opencode.json +8 -0
- package/adapters/herdr/herdr-plugin.toml +18 -0
- package/adapters/mcp/server-stdio.js +238 -25
- package/adapters/opencode/opencode.json +1 -1
- package/adapters/workos/WORKOS.md +52 -0
- package/bin/cli.js +373 -5
- package/bin/futureagi-bridge +9 -0
- package/config/gate-templates.json +653 -4
- package/config/gates/actor-critic-audit.json +34 -0
- package/config/gates/default.json +21 -2
- package/config/gates/five-walls-governance.json +34 -0
- package/config/gates/future-agi-guardrails.json +34 -0
- package/config/gates/radware-threat-defense-2026.json +61 -0
- package/config/gates/simatree-data-governance.json +33 -0
- package/config/mcp-allowlists.json +4 -0
- package/config/merge-quality-checks.json +10 -1
- package/config/model-candidates.json +382 -24
- package/config/model-tiers.json +18 -0
- package/config/post-deploy-marketing-pages.json +10 -0
- package/config/progressive/01-wire-only.json +11 -0
- package/config/progressive/02-dashboard-empty-ok.json +10 -0
- package/config/progressive/03-one-lesson.json +10 -0
- package/config/progressive/04-warn-fires.json +11 -0
- package/config/progressive/05-strict-optional.json +11 -0
- package/config/progressive/README.md +15 -0
- package/config/schemas/broker-execution-receipt.schema.json +139 -0
- package/config/schemas/provider-execution-attestation-v1.schema.json +58 -0
- package/conformance/provider-attestation/vectors.json +320 -0
- package/docs/specs/provider-execution-attestation-v1.md +69 -0
- package/openapi/openapi.yaml +15 -0
- package/package.json +401 -147
- package/public/about.html +2 -2
- package/public/ai-malpractice-prevention.html +7 -7
- package/public/blog/a-10-dollar-vps-is-not-a-computer.html +143 -0
- package/public/blog/a-receipt-is-not-world-state.html +388 -0
- package/public/blog/git-at-agent-scale.html +374 -0
- package/public/blog/no-llm-in-the-gate.html +133 -0
- package/public/blog.html +80 -0
- package/public/case-studies.html +16 -1
- package/public/compare.html +28 -0
- package/public/diagnostic.html +216 -7
- package/public/docs/connectors.html +39 -0
- package/public/federal.html +2 -2
- package/public/founders.html +639 -0
- package/public/index.html +87 -9
- package/public/install.html +8 -8
- package/public/learn.html +39 -0
- package/public/numbers.html +2 -2
- package/public/peter.html +310 -0
- package/public/platform-partners.html +119 -0
- package/public/pricing.html +24 -3
- package/public/privacy.html +117 -0
- package/public/pro.html +17 -0
- package/public/support.html +62 -0
- package/public/terms.html +130 -0
- package/public/third-party-notices.html +95 -0
- package/public/yt.html +351 -0
- package/scripts/action-receipts.js +133 -3
- package/scripts/adaptive-governance-arena.js +349 -0
- package/scripts/admin-override.js +205 -0
- package/scripts/agent-action-inventory.js +869 -0
- package/scripts/agent-audit-trace.js +42 -2
- package/scripts/agent-egress-policy.js +1117 -0
- package/scripts/agent-memory-lifecycle.js +141 -2
- package/scripts/agent-operations-planner.js +441 -1
- package/scripts/agent-readiness.js +68 -0
- package/scripts/agent-security-central.js +647 -0
- package/scripts/allowlist-bridge-honesty.js +417 -0
- package/scripts/async-job-runner.js +102 -11
- package/scripts/audit-trail.js +212 -0
- package/scripts/auto-promote-gates.js +178 -27
- package/scripts/billing.js +1 -1
- package/scripts/broker-execution-receipts.js +719 -0
- package/scripts/budget-aware-gates-proof.js +423 -0
- package/scripts/claude-feedback-sync.js +29 -3
- package/scripts/claw-harness-production.js +237 -0
- package/scripts/cli-progress.js +111 -0
- package/scripts/cli-schema.js +163 -1
- package/scripts/codex-runbook-flywheel.js +318 -0
- package/scripts/context-footprint.js +186 -0
- package/scripts/contextfs.js +143 -61
- package/scripts/dashboard-limits.js +27 -0
- package/scripts/dashboard.js +279 -9
- package/scripts/deepseek-v4-runtime-guardrails.js +72 -6
- package/scripts/docker-sandbox-planner.js +18 -0
- package/scripts/double-blind-eval-protocol.js +252 -0
- package/scripts/edotenv-rl-gateway.js +259 -0
- package/scripts/ensure-production-search-corpus.js +162 -0
- package/scripts/eval-holdout.js +311 -0
- package/scripts/feedback-aggregate.js +21 -2
- package/scripts/feedback-loop.js +87 -5
- package/scripts/feedback-quality.js +9 -0
- package/scripts/file-ledger-lock.js +4 -1
- package/scripts/financial-control-plane.js +41 -1
- package/scripts/find-dormant-requires.js +118 -0
- package/scripts/fs-utils.js +84 -8
- package/scripts/gate-stats.js +2 -2
- package/scripts/gates-engine.js +859 -58
- package/scripts/generate-case-study-outreach.js +24 -15
- package/scripts/git-at-scale.js +628 -0
- package/scripts/governance-conflict-audit.js +1650 -0
- package/scripts/governance-difficulty-curriculum.js +328 -0
- package/scripts/graphrag-retrieval.js +275 -0
- package/scripts/gurobi-optimizer.js +324 -0
- package/scripts/gurobi_optimizer.py +485 -0
- package/scripts/harness-selector.js +82 -1
- package/scripts/hidden-entry-points.js +284 -0
- package/scripts/human-escalation.js +199 -1
- package/scripts/hybrid-feedback-context.js +152 -19
- package/scripts/intent-governed-execution.js +602 -0
- package/scripts/intervention-policy.js +123 -20
- package/scripts/jit-harness-compose.js +628 -0
- package/scripts/jsonl-watcher.js +10 -0
- package/scripts/lesson-embedding-index.js +95 -12
- package/scripts/lesson-retrieval.js +105 -19
- package/scripts/local-model-profile.js +19 -2
- package/scripts/mailer/resend-mailer.js +1 -1
- package/scripts/matryoshka-embedding.js +235 -0
- package/scripts/mcp-oauth.js +42 -4
- package/scripts/mcp-session-handles.js +1016 -0
- package/scripts/mcp-wiring-doctor.js +314 -0
- package/scripts/memory-firewall.js +115 -2
- package/scripts/memory-scope-readiness.js +299 -0
- package/scripts/memory-vs-rag-route.js +161 -0
- package/scripts/model-tier-router.js +148 -21
- package/scripts/nvidia-specdecode-al-doctor.js +536 -0
- package/scripts/openui-catalog-compose-honesty.js +593 -0
- package/scripts/operational-integrity.js +19 -1
- package/scripts/override-audit.js +213 -0
- package/scripts/package-manager-honesty-doctor.js +458 -0
- package/scripts/pr-manager.js +63 -1
- package/scripts/prove-herdr-adapter.js +52 -0
- package/scripts/prove-memory-pyramid-and-symbolic-canvas.js +95 -0
- package/scripts/prove-workos.js +73 -0
- package/scripts/provider-attestation-conformance.js +192 -0
- package/scripts/provider-receipt-contract.js +136 -0
- package/scripts/qwen38-max-cost-optimizer.js +401 -0
- package/scripts/radware-threat-defense.js +280 -0
- package/scripts/rag-embedding-identity.js +221 -0
- package/scripts/rag-precision-guardrails.js +112 -2
- package/scripts/remote-feedback-capture.js +159 -0
- package/scripts/research-agent-harness.js +256 -0
- package/scripts/rsi-safety-hillclimb.js +200 -0
- package/scripts/rule-sprawl.js +188 -0
- package/scripts/schedule-manager.js +147 -0
- package/scripts/self-heal.js +8 -0
- package/scripts/session-lease.js +415 -0
- package/scripts/simatree-data-governance.js +347 -0
- package/scripts/slo-alert-engine.js +172 -7
- package/scripts/solver-parity.js +539 -0
- package/scripts/stealth-memory-injection-gate.js +333 -0
- package/scripts/switchyard-router.js +366 -0
- package/scripts/telemetry-analytics.js +84 -27
- package/scripts/temporal-decay-weighting.js +138 -0
- package/scripts/test-all.js +165 -0
- package/scripts/token-savings.js +42 -0
- package/scripts/tool-kpi-tracker.js +108 -5
- package/scripts/tool-registry.js +193 -5
- package/scripts/universal-claim-evaluator.js +14 -2
- package/scripts/vector-store.js +279 -9
- package/scripts/workflow-notebook.js +391 -0
- package/scripts/workflow-sentinel.js +111 -12
- package/scripts/workos-production-guard.js +260 -0
- package/scripts/workspace-search-route.js +515 -0
- package/server.json +2 -2
- package/src/agent-identity-boundary.js +76 -0
- package/src/agent-retrieval-cache.js +155 -0
- package/src/alert-noise-ledger.js +502 -0
- package/src/api/server.js +802 -185
- package/src/git-fast-cache.js +220 -0
- package/src/git-wal-sync.js +156 -0
- package/src/hash-anchored-edit.js +82 -0
- package/src/hermes-platform-protocol.js +475 -0
- package/src/hermes-sync-plane.js +241 -0
- package/src/index.js +30 -1
- package/src/iso42001-compliance-guard.js +97 -0
- package/src/latency-budget.js +244 -0
- package/src/mcp-writeguard.js +316 -0
- package/src/miminions-adapter.js +106 -0
- package/src/pipeline-compass.js +104 -0
- package/src/ppl-alert-pipeline.js +284 -0
- package/src/rendezvous-router.js +90 -0
- 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
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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
|
|
499
|
-
const result = spawnSync(
|
|
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,
|