trustdex 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.ko.md +391 -0
  2. package/README.md +39 -7
  3. package/SECURITY.md +4 -0
  4. package/bin/trustdex.mjs +24 -1
  5. package/docs/ARCHITECTURE.ko.md +52 -0
  6. package/docs/ARCHITECTURE.md +12 -1
  7. package/docs/CLAUDE.ko.md +122 -0
  8. package/docs/CLAUDE.md +50 -1
  9. package/docs/CODEX.ko.md +98 -0
  10. package/docs/CODEX.md +41 -4
  11. package/docs/DEFENSIVE_USE_CASE.ko.md +85 -0
  12. package/docs/DEFENSIVE_USE_CASE.md +89 -0
  13. package/docs/EVALUATION.ko.md +67 -0
  14. package/docs/EVALUATION.md +36 -1
  15. package/docs/RELEASING.ko.md +70 -0
  16. package/docs/RELEASING.md +4 -0
  17. package/docs/ROADMAP.ko.md +46 -0
  18. package/docs/ROADMAP.md +14 -2
  19. package/docs/SECURITY_RESEARCH.ko.md +69 -0
  20. package/docs/SECURITY_RESEARCH.md +9 -0
  21. package/docs/THREAT_MODEL.ko.md +56 -0
  22. package/docs/THREAT_MODEL.md +10 -1
  23. package/docs/TRUST_STORE.ko.md +93 -0
  24. package/docs/TRUST_STORE.md +4 -0
  25. package/examples/adversarial/README.ko.md +31 -0
  26. package/examples/adversarial/README.md +31 -0
  27. package/examples/adversarial/drift-after.json +11 -0
  28. package/examples/adversarial/drift-before.json +8 -0
  29. package/examples/adversarial/expected.json +30 -0
  30. package/examples/adversarial/mcp-config.json +32 -0
  31. package/examples/adversarial/policy.json +10 -0
  32. package/examples/claude.mcp.json +24 -0
  33. package/examples/claude.policy.json +6 -0
  34. package/examples/claude.settings.json +26 -0
  35. package/examples/codex.config.toml +16 -0
  36. package/examples/codex.permissions.toml +13 -0
  37. package/examples/mcp.json +22 -0
  38. package/examples/trust-store.example.json +16 -0
  39. package/examples/trustdex.policy.json +14 -0
  40. package/package.json +4 -3
  41. package/src/claude-settings.mjs +173 -0
  42. package/src/codex-config.mjs +9 -7
  43. package/src/codex-settings.mjs +247 -0
  44. package/src/inspect.mjs +60 -3
  45. package/src/policy-packs.mjs +38 -2
@@ -0,0 +1,247 @@
1
+ import { parseValue, stripComment, unquote, valueComplete } from './codex-config.mjs';
2
+
3
+ const APPROVAL_POLICIES = new Set(['on-request', 'never']);
4
+ const APPROVAL_REVIEWERS = new Set(['user', 'auto_review']);
5
+ const SANDBOX_MODES = new Set(['read-only', 'workspace-write', 'danger-full-access']);
6
+ const BUILTIN_PERMISSIONS = new Set([':read-only', ':workspace', ':danger-full-access']);
7
+ const WEB_SEARCH_MODES = new Set(['disabled', 'cached', 'indexed', 'live']);
8
+ const GRANULAR_KEYS = new Set([
9
+ 'sandbox_approval',
10
+ 'rules',
11
+ 'mcp_elicitations',
12
+ 'request_permissions',
13
+ 'skill_approval'
14
+ ]);
15
+ const SANDBOX_KEYS = new Set([
16
+ 'network_access',
17
+ 'writable_roots',
18
+ 'exclude_slash_tmp',
19
+ 'exclude_tmpdir_env_var'
20
+ ]);
21
+ const INSPECTED_ROOT_KEYS = new Set([
22
+ 'approval_policy',
23
+ 'approvals_reviewer',
24
+ 'sandbox_mode',
25
+ 'default_permissions',
26
+ 'web_search'
27
+ ]);
28
+
29
+ function isObject(value) {
30
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
31
+ }
32
+
33
+ function requireBoolean(value, key) {
34
+ if (typeof value !== 'boolean') throw new Error(`Codex ${key} must be a boolean.`);
35
+ return value;
36
+ }
37
+
38
+ function requireStringArray(value, key) {
39
+ if (!Array.isArray(value) || value.some((item) => typeof item !== 'string' || !item.trim())) {
40
+ throw new Error(`Codex ${key} must be an array of non-empty strings.`);
41
+ }
42
+ return value;
43
+ }
44
+
45
+ function parseApprovalPolicy(value) {
46
+ if (typeof value === 'string') {
47
+ if (!APPROVAL_POLICIES.has(value)) throw new Error('Unsupported Codex approval_policy value.');
48
+ return { mode: value, promptCategories: [] };
49
+ }
50
+ if (!isObject(value) || !isObject(value.granular) || Object.keys(value).some((key) => key !== 'granular')) {
51
+ throw new Error('Codex approval_policy must be "on-request", "never", or a granular policy.');
52
+ }
53
+ for (const [key, enabled] of Object.entries(value.granular)) {
54
+ if (!GRANULAR_KEYS.has(key) || typeof enabled !== 'boolean') {
55
+ throw new Error(`Unsupported Codex granular approval setting: ${key}`);
56
+ }
57
+ }
58
+ return {
59
+ mode: 'granular',
60
+ promptCategories: Object.entries(value.granular)
61
+ .filter(([, enabled]) => enabled)
62
+ .map(([key]) => key)
63
+ .sort()
64
+ };
65
+ }
66
+
67
+ function parseTomlAssignments(text) {
68
+ const lines = String(text || '').split(/\r?\n/);
69
+ const root = Object.create(null);
70
+ const sandbox = Object.create(null);
71
+ const agents = Object.create(null);
72
+ const approvalGranular = Object.create(null);
73
+ const sections = new Set();
74
+ let section = '';
75
+
76
+ for (let i = 0; i < lines.length; i += 1) {
77
+ const cleaned = stripComment(lines[i]).trim();
78
+ if (!cleaned) continue;
79
+
80
+ const header = cleaned.match(/^\[([^\]]+)\]$/);
81
+ if (header) {
82
+ section = header[1].trim();
83
+ if (section.startsWith('sandbox_workspace_write.') ||
84
+ (section.startsWith('approval_policy.') && section !== 'approval_policy.granular')) {
85
+ throw new Error(`Unsupported Codex security section: ${section}`);
86
+ }
87
+ sections.add(section);
88
+ continue;
89
+ }
90
+
91
+ const eq = cleaned.indexOf('=');
92
+ if (eq < 1) {
93
+ if (section === 'sandbox_workspace_write' || !section) {
94
+ throw new Error(`Unsupported Codex TOML line ${i + 1}: expected key = value`);
95
+ }
96
+ continue;
97
+ }
98
+
99
+ const key = unquote(cleaned.slice(0, eq).trim());
100
+ let raw = cleaned.slice(eq + 1).trim();
101
+ while (!valueComplete(raw)) {
102
+ i += 1;
103
+ if (i >= lines.length) throw new Error(`Unterminated TOML value for ${key}`);
104
+ raw += `\n${stripComment(lines[i]).trim()}`;
105
+ }
106
+ const value = parseValue(raw);
107
+
108
+ if (!section) {
109
+ if (key.startsWith('sandbox_workspace_write.')) {
110
+ const nestedKey = key.slice('sandbox_workspace_write.'.length);
111
+ if (!nestedKey || nestedKey.includes('.')) throw new Error(`Unsupported Codex sandbox key: ${key}`);
112
+ if (Object.hasOwn(sandbox, nestedKey)) throw new Error(`Duplicate Codex sandbox setting: ${nestedKey}`);
113
+ sandbox[nestedKey] = value;
114
+ } else if (key.startsWith('approval_policy.granular.')) {
115
+ const nestedKey = key.slice('approval_policy.granular.'.length);
116
+ if (!nestedKey || nestedKey.includes('.')) throw new Error(`Unsupported Codex approval setting: ${key}`);
117
+ if (Object.hasOwn(approvalGranular, nestedKey)) throw new Error(`Duplicate Codex approval setting: ${nestedKey}`);
118
+ approvalGranular[nestedKey] = value;
119
+ } else {
120
+ if (Object.hasOwn(root, key)) throw new Error(`Duplicate Codex root setting: ${key}`);
121
+ root[key] = value;
122
+ }
123
+ } else if (section === 'sandbox_workspace_write') {
124
+ if (Object.hasOwn(sandbox, key)) throw new Error(`Duplicate Codex sandbox setting: ${key}`);
125
+ sandbox[key] = value;
126
+ } else if (section === 'agents') {
127
+ if (Object.hasOwn(agents, key)) throw new Error(`Duplicate Codex agents setting: ${key}`);
128
+ agents[key] = value;
129
+ } else if (section === 'approval_policy.granular') {
130
+ if (Object.hasOwn(approvalGranular, key)) throw new Error(`Duplicate Codex approval setting: ${key}`);
131
+ approvalGranular[key] = value;
132
+ }
133
+ }
134
+
135
+ if (Object.keys(approvalGranular).length) {
136
+ if (root.approval_policy !== undefined) {
137
+ throw new Error('Codex approval_policy cannot be declared in more than one form.');
138
+ }
139
+ root.approval_policy = { granular: approvalGranular };
140
+ }
141
+
142
+ return { root, sandbox, agents, sections: [...sections].sort() };
143
+ }
144
+
145
+ function validateSandbox(sandbox) {
146
+ for (const key of Object.keys(sandbox)) {
147
+ if (!SANDBOX_KEYS.has(key)) throw new Error(`Unsupported Codex sandbox_workspace_write setting: ${key}`);
148
+ }
149
+ if (sandbox.network_access !== undefined) requireBoolean(sandbox.network_access, 'sandbox network_access');
150
+ if (sandbox.writable_roots !== undefined) requireStringArray(sandbox.writable_roots, 'sandbox writable_roots');
151
+ if (sandbox.exclude_slash_tmp !== undefined) requireBoolean(sandbox.exclude_slash_tmp, 'sandbox exclude_slash_tmp');
152
+ if (sandbox.exclude_tmpdir_env_var !== undefined) {
153
+ requireBoolean(sandbox.exclude_tmpdir_env_var, 'sandbox exclude_tmpdir_env_var');
154
+ }
155
+ }
156
+
157
+ export function inspectCodexSettingsText(text, sourceName = 'config.toml') {
158
+ const { root, sandbox, agents, sections } = parseTomlAssignments(text);
159
+ validateSandbox(sandbox);
160
+
161
+ const signals = new Set();
162
+ let approval = { mode: null, promptCategories: [] };
163
+ if (root.approval_policy !== undefined) {
164
+ approval = parseApprovalPolicy(root.approval_policy);
165
+ if (approval.mode === 'never') signals.add('codex-no-approvals');
166
+ }
167
+
168
+ if (root.approvals_reviewer !== undefined) {
169
+ if (!APPROVAL_REVIEWERS.has(root.approvals_reviewer)) {
170
+ throw new Error('Unsupported Codex approvals_reviewer value.');
171
+ }
172
+ if (root.approvals_reviewer === 'auto_review') signals.add('codex-auto-review');
173
+ }
174
+
175
+ if (root.sandbox_mode !== undefined) {
176
+ if (!SANDBOX_MODES.has(root.sandbox_mode)) throw new Error('Unsupported Codex sandbox_mode value.');
177
+ if (root.sandbox_mode === 'workspace-write') signals.add('codex-workspace-write');
178
+ if (root.sandbox_mode === 'danger-full-access') signals.add('codex-full-access');
179
+ }
180
+
181
+ if (root.default_permissions !== undefined) {
182
+ if (typeof root.default_permissions !== 'string' || !root.default_permissions.trim()) {
183
+ throw new Error('Codex default_permissions must be a non-empty string.');
184
+ }
185
+ if (root.sandbox_mode !== undefined || sections.includes('sandbox_workspace_write') || Object.keys(sandbox).length) {
186
+ throw new Error('Codex default_permissions cannot be combined with sandbox_mode or sandbox_workspace_write.');
187
+ }
188
+ if (root.default_permissions === ':workspace') signals.add('codex-workspace-write');
189
+ else if (root.default_permissions === ':danger-full-access') signals.add('codex-full-access');
190
+ else if (!BUILTIN_PERMISSIONS.has(root.default_permissions)) signals.add('codex-custom-permissions');
191
+ }
192
+
193
+ if (root.web_search !== undefined) {
194
+ if (!WEB_SEARCH_MODES.has(root.web_search)) throw new Error('Unsupported Codex web_search value.');
195
+ if (root.web_search === 'live') signals.add('codex-live-web-search');
196
+ }
197
+
198
+ if (sandbox.network_access === true) signals.add('codex-network-access');
199
+ const writableRoots = sandbox.writable_roots || [];
200
+ if (writableRoots.length) signals.add('codex-additional-writable-roots');
201
+ if (sandbox.exclude_slash_tmp === false || sandbox.exclude_tmpdir_env_var === false) {
202
+ signals.add('codex-temp-write-access');
203
+ }
204
+ if (agents.enabled !== undefined) {
205
+ requireBoolean(agents.enabled, 'agents.enabled');
206
+ if (agents.enabled) signals.add('codex-subagents-enabled');
207
+ }
208
+ const uninspectedAgentKeys = Object.keys(agents).filter((key) => key !== 'enabled').sort();
209
+ if (uninspectedAgentKeys.length) signals.add('codex-subagent-settings');
210
+
211
+ const uninspectedRootKeys = Object.keys(root).filter((key) => !INSPECTED_ROOT_KEYS.has(key)).sort();
212
+ if (uninspectedRootKeys.length) signals.add('uninspected-codex-settings');
213
+
214
+ const sectionKinds = new Set(sections.map((section) => section.split('.')[0]));
215
+ sectionKinds.delete('sandbox_workspace_write');
216
+ sectionKinds.delete('approval_policy');
217
+ sectionKinds.delete('mcp_servers');
218
+ if (!uninspectedAgentKeys.length) sectionKinds.delete('agents');
219
+ if (sectionKinds.has('permissions')) signals.add('codex-custom-permissions');
220
+ if (sectionKinds.has('apps') || sectionKinds.has('plugins')) signals.add('codex-extension-settings');
221
+ if (sectionKinds.size) signals.add('uninspected-codex-sections');
222
+
223
+ return {
224
+ name: sourceName,
225
+ kind: 'codex-settings',
226
+ source: { type: 'local', format: 'codex-settings', path: sourceName },
227
+ metadata: {
228
+ approvalPolicy: approval.mode,
229
+ granularPromptCategories: approval.promptCategories,
230
+ approvalsReviewer: root.approvals_reviewer || null,
231
+ sandboxMode: root.sandbox_mode || null,
232
+ defaultPermissionsKind: BUILTIN_PERMISSIONS.has(root.default_permissions)
233
+ ? root.default_permissions
234
+ : root.default_permissions ? 'custom' : null,
235
+ webSearch: root.web_search || null,
236
+ networkAccess: sandbox.network_access === true,
237
+ additionalWritableRootCount: writableRoots.length,
238
+ tempRootsExcluded: sandbox.exclude_slash_tmp === true && sandbox.exclude_tmpdir_env_var === true,
239
+ subagentsEnabled: agents.enabled === true,
240
+ uninspectedAgentKeys,
241
+ uninspectedRootKeys,
242
+ uninspectedSectionKinds: [...sectionKinds].sort()
243
+ },
244
+ signals: [...signals].sort(),
245
+ envKeys: []
246
+ };
247
+ }
package/src/inspect.mjs CHANGED
@@ -42,6 +42,57 @@ function looksLikePath(value) {
42
42
  value.startsWith('../') || /^[A-Za-z]:\\/.test(value);
43
43
  }
44
44
 
45
+ function fingerprintUrl(value) {
46
+ try {
47
+ const url = new URL(String(value));
48
+ return {
49
+ origin: `${url.protocol.toLowerCase()}//${url.host.toLowerCase()}`,
50
+ pathname: url.pathname,
51
+ queryKeys: [...new Set(url.searchParams.keys())].sort()
52
+ };
53
+ } catch {
54
+ return { invalid: true };
55
+ }
56
+ }
57
+
58
+ function fingerprintArgs(args) {
59
+ const values = Array.isArray(args) ? args.map(String) : [];
60
+ const sanitized = [];
61
+ let redactNext = false;
62
+
63
+ for (const value of values) {
64
+ if (redactNext) {
65
+ sanitized.push('<redacted>');
66
+ redactNext = false;
67
+ continue;
68
+ }
69
+
70
+ const assignment = value.match(/^(-{1,2}[^=]+)=(.*)$/);
71
+ if (assignment && SECRET_KEY_RE.test(assignment[1])) {
72
+ sanitized.push(`${assignment[1]}=<redacted>`);
73
+ continue;
74
+ }
75
+ if (/^-{1,2}/.test(value) && SECRET_KEY_RE.test(value)) {
76
+ sanitized.push(value);
77
+ redactNext = true;
78
+ continue;
79
+ }
80
+ if (value === '-H' || value === '--header') {
81
+ sanitized.push(value);
82
+ redactNext = true;
83
+ continue;
84
+ }
85
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(value)) {
86
+ sanitized.push(fingerprintUrl(value));
87
+ continue;
88
+ }
89
+
90
+ sanitized.push(value);
91
+ }
92
+
93
+ return sanitized;
94
+ }
95
+
45
96
  function extractRemoteEndpoint(server) {
46
97
  const candidate = server?.url || server?.endpoint;
47
98
  if (!candidate) return null;
@@ -53,6 +104,8 @@ function extractRemoteEndpoint(server) {
53
104
  host: url.hostname.toLowerCase(),
54
105
  origin: `${protocol}//${url.host.toLowerCase()}`,
55
106
  protocol,
107
+ pathname: url.pathname,
108
+ queryKeys: [...new Set(url.searchParams.keys())].sort(),
56
109
  hasEmbeddedCredentials: Boolean(url.username || url.password),
57
110
  hasQuery: Boolean(url.search)
58
111
  };
@@ -95,9 +148,13 @@ function normalizeServer(name, server = {}) {
95
148
 
96
149
  const fingerprint = sha256Json({
97
150
  command,
98
- args,
99
- url: server.url || null,
100
- endpoint: server.endpoint || null,
151
+ args: fingerprintArgs(args),
152
+ remoteEndpoint: remote ? {
153
+ origin: remote.origin,
154
+ pathname: remote.pathname,
155
+ queryKeys: remote.queryKeys
156
+ } : null,
157
+ invalidEndpoint: (server.url || server.endpoint) && !remote ? true : null,
101
158
  envKeys,
102
159
  adapter: server._trustdexFingerprintData || null
103
160
  });
@@ -14,7 +14,39 @@ const BASE_SIGNALS = {
14
14
  'embedded-credentials': 'block',
15
15
  'insecure-transport': 'block',
16
16
  'url-query-parameters': 'ask',
17
- 'artifact-version-mismatch': 'block'
17
+ 'artifact-version-mismatch': 'block',
18
+ 'claude-auto-approve': 'ask',
19
+ 'claude-broad-auto-approve': 'block',
20
+ 'claude-shell-auto-approve': 'block',
21
+ 'claude-filesystem-auto-approve': 'ask',
22
+ 'claude-network-auto-approve': 'ask',
23
+ 'claude-subagent-auto-approve': 'ask',
24
+ 'claude-mcp-auto-approve': 'ask',
25
+ 'claude-mcp-wildcard': 'ask',
26
+ 'claude-bypass-permissions': 'block',
27
+ 'claude-auto-mode': 'ask',
28
+ 'claude-additional-directories': 'ask',
29
+ 'claude-outside-read-access': 'ask',
30
+ 'claude-hooks': 'block',
31
+ 'claude-credential-helper': 'block',
32
+ 'claude-environment': 'ask',
33
+ 'claude-sandbox-settings': 'ask',
34
+ 'claude-extension-loading': 'ask',
35
+ 'uninspected-claude-settings': 'ask',
36
+ 'codex-no-approvals': 'ask',
37
+ 'codex-auto-review': 'ask',
38
+ 'codex-workspace-write': 'ask',
39
+ 'codex-full-access': 'block',
40
+ 'codex-network-access': 'ask',
41
+ 'codex-additional-writable-roots': 'ask',
42
+ 'codex-temp-write-access': 'ask',
43
+ 'codex-live-web-search': 'ask',
44
+ 'codex-custom-permissions': 'ask',
45
+ 'codex-subagents-enabled': 'ask',
46
+ 'codex-subagent-settings': 'ask',
47
+ 'codex-extension-settings': 'ask',
48
+ 'uninspected-codex-settings': 'ask',
49
+ 'uninspected-codex-sections': 'ask'
18
50
  };
19
51
 
20
52
  export const POLICY_PACKS = Object.freeze({
@@ -59,7 +91,11 @@ export const POLICY_PACKS = Object.freeze({
59
91
  signalActions: {
60
92
  ...BASE_SIGNALS,
61
93
  'shell-execution': 'ask',
62
- 'insecure-transport': 'ask'
94
+ 'insecure-transport': 'ask',
95
+ 'claude-broad-auto-approve': 'ask',
96
+ 'claude-shell-auto-approve': 'ask',
97
+ 'claude-hooks': 'ask',
98
+ 'claude-credential-helper': 'ask'
63
99
  }
64
100
  })
65
101
  });