trustdex 0.4.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/LICENSE +21 -0
- package/README.md +321 -0
- package/SECURITY.md +47 -0
- package/bin/trustdex.mjs +498 -0
- package/docs/ARCHITECTURE.md +43 -0
- package/docs/RELEASING.md +53 -0
- package/docs/ROADMAP.md +33 -0
- package/docs/THREAT_MODEL.md +47 -0
- package/docs/TRUST_STORE.md +89 -0
- package/package.json +47 -0
- package/src/canonical.mjs +17 -0
- package/src/codex-config.mjs +308 -0
- package/src/extensions.mjs +112 -0
- package/src/gate.mjs +39 -0
- package/src/inspect.mjs +121 -0
- package/src/policy-packs.mjs +87 -0
- package/src/policy.mjs +102 -0
- package/src/provenance-online.mjs +394 -0
- package/src/provenance.mjs +194 -0
- package/src/review.mjs +39 -0
- package/src/snapshot.mjs +76 -0
- package/src/trust-record.mjs +59 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Trust store and provenance
|
|
2
|
+
|
|
3
|
+
TrustDex does not guess whether a publisher is "official" from a package name, GitHub stars, or branding.
|
|
4
|
+
|
|
5
|
+
A source can receive a `verified` provenance assertion only when the user or an organization places an explicit claim in a local trust store.
|
|
6
|
+
|
|
7
|
+
## Format
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"version": 1,
|
|
12
|
+
"claims": [
|
|
13
|
+
{
|
|
14
|
+
"type": "package",
|
|
15
|
+
"subject": "example-mcp-server",
|
|
16
|
+
"publisher": "Example Publisher",
|
|
17
|
+
"status": "verified",
|
|
18
|
+
"evidence": {
|
|
19
|
+
"kind": "manual-review",
|
|
20
|
+
"reference": "https://example.invalid/security-review",
|
|
21
|
+
"checkedAt": "2026-09-19T00:00:00Z"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Supported claim types:
|
|
29
|
+
|
|
30
|
+
- `package` - npm/Python-style package subject
|
|
31
|
+
- `remote` - remote MCP hostname
|
|
32
|
+
- `repository` - repository identity for future repository adapters
|
|
33
|
+
|
|
34
|
+
The word `verified` means **verified according to the evidence recorded by the trust-store maintainer**. It does not mean OpenAI, TrustDex, GitHub, npm, or another platform certified the extension.
|
|
35
|
+
|
|
36
|
+
## Use it
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
node ./bin/trustdex.mjs inspect ./examples/mcp.json \
|
|
40
|
+
--pack official-first \
|
|
41
|
+
--trust-store ./examples/trust-store.example.json
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Unknown third-party sources remain blocked in `official-first`. A verified provenance assertion can establish initial trust, but blocking capability signals such as shell execution can still override that trust.
|
|
45
|
+
|
|
46
|
+
## Recommended practice
|
|
47
|
+
|
|
48
|
+
- keep trust stores in source control when they are team policy
|
|
49
|
+
- link each claim to durable evidence
|
|
50
|
+
- pin package versions or source commits where possible
|
|
51
|
+
- re-review claims when ownership or capabilities change
|
|
52
|
+
- do not record credentials, tokens, private URLs, or secrets in evidence fields
|
|
53
|
+
|
|
54
|
+
## Integrity-backed package evidence
|
|
55
|
+
|
|
56
|
+
The optional `--artifact` flag on `trust-source npm` and `trust-source mcp` verifies a user-supplied file before recording artifact evidence.
|
|
57
|
+
|
|
58
|
+
- npm observations require an SRI value using SHA-256, SHA-384, or SHA-512
|
|
59
|
+
- MCP Registry observations require a single package with a SHA-256 digest
|
|
60
|
+
- the file is read locally and is never executed or uploaded
|
|
61
|
+
- the trust-store claim records the registry identifier, version, expected integrity metadata, computed digest, and verification time
|
|
62
|
+
- the configured package version must match the verified artifact version
|
|
63
|
+
|
|
64
|
+
Example evidence shape:
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"kind": "npm-registry",
|
|
69
|
+
"reference": "https://www.npmjs.com/package/example-mcp-server",
|
|
70
|
+
"checkedAt": "2026-09-19T00:00:00Z",
|
|
71
|
+
"artifact": {
|
|
72
|
+
"registryType": "npm",
|
|
73
|
+
"identifier": "example-mcp-server",
|
|
74
|
+
"version": "1.2.3",
|
|
75
|
+
"integrity": "sha512-..."
|
|
76
|
+
},
|
|
77
|
+
"artifactVerification": {
|
|
78
|
+
"verified": true,
|
|
79
|
+
"algorithm": "sha512",
|
|
80
|
+
"digest": "...",
|
|
81
|
+
"encoding": "base64",
|
|
82
|
+
"version": "1.2.3",
|
|
83
|
+
"identifier": "example-mcp-server",
|
|
84
|
+
"verifiedAt": "2026-09-19T00:00:01Z"
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Artifact integrity proves only that the supplied bytes match the registry metadata observed at that time. It does not prove that the bytes are safe, that the registry account was uncompromised, or that the publisher name is legitimate.
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "trustdex",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "A local-first trust, provenance, and review gate for AI agent tools, MCP servers, skills, and plugins.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"trustdex": "./bin/trustdex.mjs"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "node --test",
|
|
11
|
+
"check": "node --check bin/trustdex.mjs && node --check src/canonical.mjs && node --check src/codex-config.mjs && node --check src/extensions.mjs && node --check src/gate.mjs && node --check src/inspect.mjs && node --check src/policy.mjs && node --check src/policy-packs.mjs && node --check src/provenance.mjs && node --check src/provenance-online.mjs && node --check src/review.mjs && node --check src/snapshot.mjs && node --check src/trust-record.mjs",
|
|
12
|
+
"prepublishOnly": "npm test && npm run check"
|
|
13
|
+
},
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=20"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"ai-agents",
|
|
19
|
+
"mcp",
|
|
20
|
+
"security",
|
|
21
|
+
"supply-chain",
|
|
22
|
+
"codex",
|
|
23
|
+
"agent-skills",
|
|
24
|
+
"trust-policy",
|
|
25
|
+
"provenance"
|
|
26
|
+
],
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/grigent/trustdex.git"
|
|
31
|
+
},
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/grigent/trustdex/issues"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://github.com/grigent/trustdex#readme",
|
|
36
|
+
"files": [
|
|
37
|
+
"bin",
|
|
38
|
+
"src",
|
|
39
|
+
"docs",
|
|
40
|
+
"README.md",
|
|
41
|
+
"SECURITY.md",
|
|
42
|
+
"LICENSE"
|
|
43
|
+
],
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
export function canonicalJson(value) {
|
|
4
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`;
|
|
5
|
+
if (value && typeof value === 'object') {
|
|
6
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(',')}}`;
|
|
7
|
+
}
|
|
8
|
+
return JSON.stringify(value);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function sha256Text(value) {
|
|
12
|
+
return crypto.createHash('sha256').update(String(value)).digest('hex');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function sha256Json(value) {
|
|
16
|
+
return sha256Text(canonicalJson(value));
|
|
17
|
+
}
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
const CODEX_MCP_NESTED_SECTIONS = new Set(['env', 'http_headers', 'env_http_headers']);
|
|
2
|
+
|
|
3
|
+
function stripComment(line) {
|
|
4
|
+
let quote = null;
|
|
5
|
+
let escaped = false;
|
|
6
|
+
for (let i = 0; i < line.length; i += 1) {
|
|
7
|
+
const char = line[i];
|
|
8
|
+
if (escaped) {
|
|
9
|
+
escaped = false;
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
if (quote && char === '\\') {
|
|
13
|
+
escaped = true;
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
if (char === '"' || char === "'") {
|
|
17
|
+
if (quote === char) quote = null;
|
|
18
|
+
else if (!quote) quote = char;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (char === '#' && !quote) return line.slice(0, i);
|
|
22
|
+
}
|
|
23
|
+
return line;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function splitTopLevel(value) {
|
|
27
|
+
const out = [];
|
|
28
|
+
let current = '';
|
|
29
|
+
let quote = null;
|
|
30
|
+
let escaped = false;
|
|
31
|
+
let depth = 0;
|
|
32
|
+
|
|
33
|
+
for (const char of value) {
|
|
34
|
+
if (escaped) {
|
|
35
|
+
current += char;
|
|
36
|
+
escaped = false;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (quote && char === '\\') {
|
|
40
|
+
current += char;
|
|
41
|
+
escaped = true;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (char === '"' || char === "'") {
|
|
45
|
+
if (quote === char) quote = null;
|
|
46
|
+
else if (!quote) quote = char;
|
|
47
|
+
current += char;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (!quote && (char === '[' || char === '{')) depth += 1;
|
|
51
|
+
if (!quote && (char === ']' || char === '}')) depth -= 1;
|
|
52
|
+
if (char === ',' && !quote && depth === 0) {
|
|
53
|
+
out.push(current.trim());
|
|
54
|
+
current = '';
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
current += char;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (current.trim()) out.push(current.trim());
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function unquote(value) {
|
|
65
|
+
const raw = value.trim();
|
|
66
|
+
if ((raw.startsWith('"') && raw.endsWith('"')) || (raw.startsWith("'") && raw.endsWith("'"))) {
|
|
67
|
+
return raw.slice(1, -1);
|
|
68
|
+
}
|
|
69
|
+
return raw;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function parseValue(raw) {
|
|
73
|
+
const value = raw.trim();
|
|
74
|
+
if (!value) return '';
|
|
75
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
76
|
+
return unquote(value);
|
|
77
|
+
}
|
|
78
|
+
if (value.startsWith('[') && value.endsWith(']')) {
|
|
79
|
+
return splitTopLevel(value.slice(1, -1)).map(parseValue);
|
|
80
|
+
}
|
|
81
|
+
if (value.startsWith('{') && value.endsWith('}')) {
|
|
82
|
+
const object = {};
|
|
83
|
+
for (const part of splitTopLevel(value.slice(1, -1))) {
|
|
84
|
+
const index = part.indexOf('=');
|
|
85
|
+
if (index < 1) throw new Error(`Unsupported TOML inline table entry: ${part}`);
|
|
86
|
+
object[unquote(part.slice(0, index).trim())] = parseValue(part.slice(index + 1));
|
|
87
|
+
}
|
|
88
|
+
return object;
|
|
89
|
+
}
|
|
90
|
+
if (value === 'true') return true;
|
|
91
|
+
if (value === 'false') return false;
|
|
92
|
+
if (/^-?\d+(?:\.\d+)?$/.test(value)) return Number(value);
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function valueComplete(value) {
|
|
97
|
+
let square = 0;
|
|
98
|
+
let curly = 0;
|
|
99
|
+
let quote = null;
|
|
100
|
+
let escaped = false;
|
|
101
|
+
|
|
102
|
+
for (const char of value) {
|
|
103
|
+
if (escaped) {
|
|
104
|
+
escaped = false;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (quote && char === '\\') {
|
|
108
|
+
escaped = true;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (char === '"' || char === "'") {
|
|
112
|
+
if (quote === char) quote = null;
|
|
113
|
+
else if (!quote) quote = char;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (quote) continue;
|
|
117
|
+
if (char === '[') square += 1;
|
|
118
|
+
if (char === ']') square -= 1;
|
|
119
|
+
if (char === '{') curly += 1;
|
|
120
|
+
if (char === '}') curly -= 1;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return !quote && square === 0 && curly === 0;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function codexServerNameFromHeader(header) {
|
|
127
|
+
const match = String(header || '').trim().match(
|
|
128
|
+
/^mcp_servers\.(?:"([^"]+)"|'([^']+)'|([A-Za-z0-9_-]+))(?:\.(.*))?$/
|
|
129
|
+
);
|
|
130
|
+
if (!match) return null;
|
|
131
|
+
return {
|
|
132
|
+
name: match[1] || match[2] || match[3],
|
|
133
|
+
nested: match[4] || ''
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function envNames(value) {
|
|
138
|
+
if (!Array.isArray(value)) return [];
|
|
139
|
+
return value.map((item) => {
|
|
140
|
+
if (typeof item === 'string') return item;
|
|
141
|
+
if (item && typeof item === 'object' && typeof item.name === 'string') return item.name;
|
|
142
|
+
return null;
|
|
143
|
+
}).filter(Boolean);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function applyAssignment(server, nested, key, value) {
|
|
147
|
+
const signals = server._trustdexSignals;
|
|
148
|
+
|
|
149
|
+
if (nested === 'env') {
|
|
150
|
+
server.env[key] = '<redacted>';
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (nested === 'http_headers') {
|
|
154
|
+
server._trustdexFingerprintData.httpHeaderKeys.push(key);
|
|
155
|
+
signals.add('static-http-headers');
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (nested === 'env_http_headers') {
|
|
159
|
+
server._trustdexFingerprintData.envHttpHeaderKeys.push(key);
|
|
160
|
+
signals.add('env-http-headers');
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (nested) {
|
|
165
|
+
throw new Error(`Unsupported Codex MCP subsection "${nested}".`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (key === 'command' && typeof value === 'string') server.command = value;
|
|
169
|
+
else if (key === 'args' && Array.isArray(value)) server.args = value.map(String);
|
|
170
|
+
else if (key === 'url' && typeof value === 'string') server.url = value;
|
|
171
|
+
else if (key === 'cwd' && typeof value === 'string') {
|
|
172
|
+
server.args ??= [];
|
|
173
|
+
server._trustdexFingerprintData.cwd = value;
|
|
174
|
+
} else if (key === 'env' && value && typeof value === 'object' && !Array.isArray(value)) {
|
|
175
|
+
for (const envKey of Object.keys(value)) server.env[envKey] = '<redacted>';
|
|
176
|
+
} else if (key === 'env_vars') {
|
|
177
|
+
for (const envKey of envNames(value)) server.env[envKey] = '<forwarded>';
|
|
178
|
+
if (envNames(value).length) signals.add('env-forwarding');
|
|
179
|
+
} else if (key === 'bearer_token_env_var' && typeof value === 'string') {
|
|
180
|
+
server.env[value] = '<bearer-token>';
|
|
181
|
+
signals.add('secret-env');
|
|
182
|
+
} else if (key === 'http_headers' && value && typeof value === 'object') {
|
|
183
|
+
server._trustdexFingerprintData.httpHeaderKeys.push(...Object.keys(value));
|
|
184
|
+
if (Object.keys(value).length) signals.add('static-http-headers');
|
|
185
|
+
} else if (key === 'env_http_headers' && value && typeof value === 'object') {
|
|
186
|
+
server._trustdexFingerprintData.envHttpHeaderKeys.push(...Object.keys(value));
|
|
187
|
+
if (Object.keys(value).length) signals.add('env-http-headers');
|
|
188
|
+
} else if (key === 'http_headers_helper' && typeof value === 'string') {
|
|
189
|
+
server._trustdexFingerprintData.httpHeadersHelper = value;
|
|
190
|
+
signals.add('header-helper-command');
|
|
191
|
+
} else if (key === 'enabled_tools' && Array.isArray(value)) {
|
|
192
|
+
server._trustdexFingerprintData.enabledTools = value.map(String).sort();
|
|
193
|
+
if (value.some((item) => String(item) === '*')) signals.add('wildcard-tool-scope');
|
|
194
|
+
} else if (key === 'disabled_tools' && Array.isArray(value)) {
|
|
195
|
+
server._trustdexFingerprintData.disabledTools = value.map(String).sort();
|
|
196
|
+
} else if (key === 'default_tools_approval_mode' && typeof value === 'string') {
|
|
197
|
+
server._trustdexFingerprintData.defaultToolsApprovalMode = value;
|
|
198
|
+
if (value === 'approve') signals.add('auto-approve-tools');
|
|
199
|
+
} else if (key === 'enabled' && typeof value === 'boolean') {
|
|
200
|
+
server._trustdexFingerprintData.enabled = value;
|
|
201
|
+
} else if (key === 'startup_timeout_sec' && typeof value === 'number') {
|
|
202
|
+
server._trustdexFingerprintData.startupTimeoutSec = value;
|
|
203
|
+
} else if (key === 'tool_timeout_sec' && typeof value === 'number') {
|
|
204
|
+
server._trustdexFingerprintData.toolTimeoutSec = value;
|
|
205
|
+
} else {
|
|
206
|
+
throw new Error(`Unsupported or invalid Codex MCP key "${key}".`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function parseCodexMcpToml(text) {
|
|
211
|
+
const lines = String(text || '').split(/\r?\n/);
|
|
212
|
+
const servers = {};
|
|
213
|
+
let section = null;
|
|
214
|
+
|
|
215
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
216
|
+
const cleaned = stripComment(lines[i]).trim();
|
|
217
|
+
if (!cleaned) continue;
|
|
218
|
+
|
|
219
|
+
const headerMatch = cleaned.match(/^\[([^\]]+)\]$/);
|
|
220
|
+
if (headerMatch) {
|
|
221
|
+
const rawHeader = headerMatch[1].trim();
|
|
222
|
+
section = codexServerNameFromHeader(rawHeader);
|
|
223
|
+
if ((rawHeader === 'mcp_servers' || rawHeader.startsWith('mcp_servers.')) && !section) {
|
|
224
|
+
throw new Error(`Unsupported Codex MCP section "[${rawHeader}]".`);
|
|
225
|
+
}
|
|
226
|
+
if (section?.nested && !CODEX_MCP_NESTED_SECTIONS.has(section.nested)) {
|
|
227
|
+
throw new Error(`Unsupported Codex MCP subsection "${section.nested}".`);
|
|
228
|
+
}
|
|
229
|
+
if (section && !servers[section.name]) {
|
|
230
|
+
servers[section.name] = {
|
|
231
|
+
env: {},
|
|
232
|
+
_trustdexSignals: new Set(),
|
|
233
|
+
_trustdexFingerprintData: {
|
|
234
|
+
format: 'codex-toml',
|
|
235
|
+
cwd: null,
|
|
236
|
+
httpHeaderKeys: [],
|
|
237
|
+
envHttpHeaderKeys: [],
|
|
238
|
+
httpHeadersHelper: null,
|
|
239
|
+
enabledTools: [],
|
|
240
|
+
disabledTools: [],
|
|
241
|
+
defaultToolsApprovalMode: null,
|
|
242
|
+
enabled: null,
|
|
243
|
+
startupTimeoutSec: null,
|
|
244
|
+
toolTimeoutSec: null
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (!section) continue;
|
|
252
|
+
const eq = cleaned.indexOf('=');
|
|
253
|
+
if (eq < 1) throw new Error(`Unsupported Codex TOML line ${i + 1}: expected key = value`);
|
|
254
|
+
|
|
255
|
+
const key = unquote(cleaned.slice(0, eq).trim());
|
|
256
|
+
let raw = cleaned.slice(eq + 1).trim();
|
|
257
|
+
while (!valueComplete(raw)) {
|
|
258
|
+
i += 1;
|
|
259
|
+
if (i >= lines.length) throw new Error(`Unterminated TOML value for ${key}`);
|
|
260
|
+
raw += `\n${stripComment(lines[i]).trim()}`;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
applyAssignment(servers[section.name], section.nested, key, parseValue(raw));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
for (const server of Object.values(servers)) {
|
|
267
|
+
server._trustdexSignals = [...server._trustdexSignals].sort();
|
|
268
|
+
server._trustdexFingerprintData.httpHeaderKeys =
|
|
269
|
+
[...new Set(server._trustdexFingerprintData.httpHeaderKeys)].sort();
|
|
270
|
+
server._trustdexFingerprintData.envHttpHeaderKeys =
|
|
271
|
+
[...new Set(server._trustdexFingerprintData.envHttpHeaderKeys)].sort();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return { mcpServers: servers };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function gateCodexToml(text, results, options = {}) {
|
|
278
|
+
const includeAsk = Boolean(options.includeAsk);
|
|
279
|
+
const decisions = new Map(results.map((result) => [result.name, result.action]));
|
|
280
|
+
const lines = String(text || '').split(/\r?\n/);
|
|
281
|
+
const kept = [];
|
|
282
|
+
let keepSection = true;
|
|
283
|
+
|
|
284
|
+
for (const line of lines) {
|
|
285
|
+
const cleaned = stripComment(line).trim();
|
|
286
|
+
const headerMatch = cleaned.match(/^\[([^\]]+)\]$/);
|
|
287
|
+
if (headerMatch) {
|
|
288
|
+
const rawHeader = headerMatch[1].trim();
|
|
289
|
+
const section = codexServerNameFromHeader(rawHeader);
|
|
290
|
+
if ((rawHeader === 'mcp_servers' || rawHeader.startsWith('mcp_servers.')) && !section) {
|
|
291
|
+
throw new Error(`Unsupported Codex MCP section "[${rawHeader}]".`);
|
|
292
|
+
}
|
|
293
|
+
if (section?.nested && !CODEX_MCP_NESTED_SECTIONS.has(section.nested)) {
|
|
294
|
+
throw new Error(`Unsupported Codex MCP subsection "${section.nested}".`);
|
|
295
|
+
}
|
|
296
|
+
if (section) {
|
|
297
|
+
const action = decisions.get(section.name);
|
|
298
|
+
keepSection = action === 'allow' || (includeAsk && action === 'ask');
|
|
299
|
+
} else {
|
|
300
|
+
keepSection = true;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (keepSection) kept.push(line);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const output = kept.join('\n');
|
|
307
|
+
return text.endsWith('\n') && !output.endsWith('\n') ? `${output}\n` : output;
|
|
308
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { sha256Json, sha256Text } from './canonical.mjs';
|
|
2
|
+
|
|
3
|
+
const URL_RE = /https?:\/\/[^\s)>'"]+/gi;
|
|
4
|
+
const SENSITIVE_PATH_RE = /(?:~\/\.(?:ssh|aws|config)|\/(?:etc|var\/run\/secrets)\/|\.env(?:\b|\/)|credentials?\b)/i;
|
|
5
|
+
const SHELL_INSTRUCTION_RE = /(?:curl\s+[^\n|]+\|\s*(?:sh|bash)|wget\s+[^\n|]+\|\s*(?:sh|bash)|\brm\s+-rf\b|\bsudo\b|\bpowershell\b|\bchmod\s+\+x\b)/i;
|
|
6
|
+
const INSTALL_RE = /(?:\bnpm\s+(?:i|install)\s+-g\b|\bpipx?\s+install\b|\bcargo\s+install\b|\bcurl\b|\bwget\b)/i;
|
|
7
|
+
|
|
8
|
+
function parseScalar(value) {
|
|
9
|
+
const raw = value.trim();
|
|
10
|
+
if (!raw) return '';
|
|
11
|
+
if ((raw.startsWith('"') && raw.endsWith('"')) || (raw.startsWith("'") && raw.endsWith("'"))) {
|
|
12
|
+
return raw.slice(1, -1);
|
|
13
|
+
}
|
|
14
|
+
if (raw.startsWith('[') && raw.endsWith(']')) {
|
|
15
|
+
return raw.slice(1, -1).split(',').map((item) => parseScalar(item)).filter(Boolean);
|
|
16
|
+
}
|
|
17
|
+
if (raw === 'true') return true;
|
|
18
|
+
if (raw === 'false') return false;
|
|
19
|
+
return raw;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function parseFrontmatter(text) {
|
|
23
|
+
const value = String(text || '');
|
|
24
|
+
if (!value.startsWith('---\n') && !value.startsWith('---\r\n')) return {};
|
|
25
|
+
const lines = value.split(/\r?\n/);
|
|
26
|
+
const result = {};
|
|
27
|
+
for (let i = 1; i < lines.length; i += 1) {
|
|
28
|
+
if (lines[i].trim() === '---') break;
|
|
29
|
+
const match = lines[i].match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
|
|
30
|
+
if (match) result[match[1]] = parseScalar(match[2]);
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function wildcardTools(metadata) {
|
|
36
|
+
const tools = metadata['allowed-tools'] ?? metadata.allowed_tools ?? metadata.tools;
|
|
37
|
+
if (tools === '*') return true;
|
|
38
|
+
if (Array.isArray(tools)) return tools.some((tool) => String(tool).trim() === '*');
|
|
39
|
+
return typeof tools === 'string' && /(^|[,\s])\*($|[,\s])/.test(tools);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function inspectSkillText(text, sourceName = 'SKILL.md') {
|
|
43
|
+
const value = String(text || '');
|
|
44
|
+
const metadata = parseFrontmatter(value);
|
|
45
|
+
const signals = new Set();
|
|
46
|
+
|
|
47
|
+
if (URL_RE.test(value)) signals.add('network-instructions');
|
|
48
|
+
URL_RE.lastIndex = 0;
|
|
49
|
+
if (SENSITIVE_PATH_RE.test(value)) signals.add('sensitive-path-instructions');
|
|
50
|
+
if (SHELL_INSTRUCTION_RE.test(value)) signals.add('shell-instructions');
|
|
51
|
+
if (INSTALL_RE.test(value)) signals.add('install-instructions');
|
|
52
|
+
if (wildcardTools(metadata)) signals.add('wildcard-tool-scope');
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
name: String(metadata.name || sourceName),
|
|
56
|
+
kind: 'skill',
|
|
57
|
+
source: { type: 'skill-file', path: sourceName },
|
|
58
|
+
fingerprint: sha256Text(value),
|
|
59
|
+
metadata: {
|
|
60
|
+
name: metadata.name || null,
|
|
61
|
+
description: metadata.description || null,
|
|
62
|
+
allowedTools: metadata['allowed-tools'] ?? metadata.allowed_tools ?? null
|
|
63
|
+
},
|
|
64
|
+
signals: [...signals].sort(),
|
|
65
|
+
envKeys: []
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function walk(value, path = [], out = []) {
|
|
70
|
+
if (Array.isArray(value)) {
|
|
71
|
+
value.forEach((item, index) => walk(item, [...path, String(index)], out));
|
|
72
|
+
} else if (value && typeof value === 'object') {
|
|
73
|
+
Object.entries(value).forEach(([key, item]) => walk(item, [...path, key], out));
|
|
74
|
+
} else {
|
|
75
|
+
out.push({ path: path.join('.'), value });
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function inspectPluginManifest(manifest, sourceName = 'plugin.json') {
|
|
81
|
+
if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) {
|
|
82
|
+
throw new Error('Expected a plugin manifest JSON object.');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const signals = new Set();
|
|
86
|
+
const leaves = walk(manifest);
|
|
87
|
+
const textLeaves = leaves.filter((entry) => typeof entry.value === 'string');
|
|
88
|
+
|
|
89
|
+
if (!manifest.version) signals.add('missing-version');
|
|
90
|
+
if (textLeaves.some((entry) => /^https?:\/\//i.test(entry.value))) signals.add('network-reference');
|
|
91
|
+
if (textLeaves.some((entry) => SENSITIVE_PATH_RE.test(entry.value))) signals.add('sensitive-path-reference');
|
|
92
|
+
if (textLeaves.some((entry) => SHELL_INSTRUCTION_RE.test(entry.value))) signals.add('shell-reference');
|
|
93
|
+
|
|
94
|
+
const toolish = leaves.filter((entry) => /(?:tool|permission|capabilit)/i.test(entry.path));
|
|
95
|
+
if (toolish.some((entry) => entry.value === '*' || (Array.isArray(entry.value) && entry.value.includes('*')))) {
|
|
96
|
+
signals.add('wildcard-tool-scope');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
name: String(manifest.name || sourceName),
|
|
101
|
+
kind: 'plugin',
|
|
102
|
+
source: { type: 'plugin-manifest', path: sourceName },
|
|
103
|
+
fingerprint: sha256Json(manifest),
|
|
104
|
+
metadata: {
|
|
105
|
+
name: manifest.name || null,
|
|
106
|
+
version: manifest.version || null,
|
|
107
|
+
description: manifest.description || null
|
|
108
|
+
},
|
|
109
|
+
signals: [...signals].sort(),
|
|
110
|
+
envKeys: []
|
|
111
|
+
};
|
|
112
|
+
}
|
package/src/gate.mjs
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export function gateMcpConfig(config, results, options = {}) {
|
|
2
|
+
let containerKey = null;
|
|
3
|
+
let servers = config;
|
|
4
|
+
|
|
5
|
+
if (config?.mcpServers && typeof config.mcpServers === 'object') {
|
|
6
|
+
containerKey = 'mcpServers';
|
|
7
|
+
servers = config.mcpServers;
|
|
8
|
+
} else if (config?.servers && typeof config.servers === 'object') {
|
|
9
|
+
containerKey = 'servers';
|
|
10
|
+
servers = config.servers;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (!servers || typeof servers !== 'object' || Array.isArray(servers)) {
|
|
14
|
+
throw new Error('Expected an MCP configuration object, or an object containing mcpServers/servers.');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const includeAsk = Boolean(options.includeAsk);
|
|
18
|
+
const decisions = new Map(results.map((result) => [result.name, result.action]));
|
|
19
|
+
const allowed = {};
|
|
20
|
+
|
|
21
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
22
|
+
const action = decisions.get(name);
|
|
23
|
+
if (action === 'allow' || (includeAsk && action === 'ask')) {
|
|
24
|
+
allowed[name] = server;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const output = containerKey ? { ...config, [containerKey]: allowed } : allowed;
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
config: output,
|
|
32
|
+
summary: {
|
|
33
|
+
total: Object.keys(servers).length,
|
|
34
|
+
exposed: Object.keys(allowed).length,
|
|
35
|
+
blocked: results.filter((result) => result.action === 'block').map((result) => result.name),
|
|
36
|
+
needsReview: results.filter((result) => result.action === 'ask').map((result) => result.name)
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|