@shomra/agent 0.3.28 → 0.3.30

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 (84) hide show
  1. package/package.json +1 -1
  2. package/src/agents/hook-command.mjs +1 -1
  3. package/src/artifacts/matchers.mjs +7 -0
  4. package/src/cli/flags.mjs +2 -2
  5. package/src/cli/help-sections.mjs +7 -0
  6. package/src/cli/help.mjs +1 -1
  7. package/src/commands/check.mjs +3 -11
  8. package/src/commands/gate.mjs +26 -4
  9. package/src/commands/git-hooks.mjs +2 -2
  10. package/src/commands/ledger.mjs +0 -1
  11. package/src/commands/mcp-add.mjs +2 -1
  12. package/src/commands/memory-scan.mjs +135 -47
  13. package/src/commands/pr.mjs +7 -10
  14. package/src/commands/provenance.mjs +8 -13
  15. package/src/commands/scan.mjs +7 -1
  16. package/src/commands/secrets.mjs +4 -5
  17. package/src/core/git-exec.mjs +79 -0
  18. package/src/core/yaml-lite.mjs +300 -0
  19. package/src/core/zip-lite.mjs +37 -0
  20. package/src/detect/local-redact.mjs +1 -3
  21. package/src/detect/sast/rules-config.mjs +1 -1
  22. package/src/detect/sast/rules-javascript.mjs +16 -3
  23. package/src/detect/sast/rules-python.mjs +8 -6
  24. package/src/detect/sast/scanner.mjs +1 -1
  25. package/src/detect/signals/agent-frameworks.mjs +231 -0
  26. package/src/detect/signals/agent-graph-surface.mjs +113 -0
  27. package/src/detect/signals/agentic-ci-surface.mjs +314 -0
  28. package/src/detect/signals/agentic-shim.mjs +82 -0
  29. package/src/detect/signals/artifacts.mjs +8 -36
  30. package/src/detect/signals/autonomy.mjs +9 -1
  31. package/src/detect/signals/chat-template.mjs +211 -0
  32. package/src/detect/signals/ci-workflow.mjs +169 -0
  33. package/src/detect/signals/credential-harvest.mjs +1 -1
  34. package/src/detect/signals/execution-hijack.mjs +4 -2
  35. package/src/detect/signals/gate.mjs +84 -11
  36. package/src/detect/signals/guardrail-shape.mjs +564 -0
  37. package/src/detect/signals/guardrail-surface.mjs +221 -0
  38. package/src/detect/signals/injection.mjs +8 -0
  39. package/src/detect/signals/inspect-shim.mjs +7 -0
  40. package/src/detect/signals/instruction-paths.mjs +60 -0
  41. package/src/detect/signals/manifests.mjs +302 -0
  42. package/src/detect/signals/masking.mjs +14 -1
  43. package/src/detect/signals/mcp-advisories.mjs +109 -0
  44. package/src/detect/signals/mcp-config.mjs +598 -0
  45. package/src/detect/signals/memory-directives.mjs +661 -0
  46. package/src/detect/signals/memory-locations.mjs +158 -0
  47. package/src/detect/signals/memory.mjs +56 -31
  48. package/src/detect/signals/model-config-rules.mjs +655 -0
  49. package/src/detect/signals/model-config.mjs +61 -0
  50. package/src/detect/signals/packages.mjs +2 -2
  51. package/src/detect/signals/prose-context.mjs +6 -9
  52. package/src/detect/signals/scan.mjs +4 -4
  53. package/src/detect/signals/secret-scanner.mjs +241 -0
  54. package/src/detect/signals/secrets.mjs +1 -48
  55. package/src/detect/signals/shell.mjs +10 -10
  56. package/src/gate/advisories.mjs +16 -0
  57. package/src/gate/batch.mjs +10 -0
  58. package/src/gate/environment.mjs +8 -53
  59. package/src/guard/artifact-paths.mjs +107 -0
  60. package/src/guard/classify.mjs +165 -7
  61. package/src/guard/command-resolve.mjs +35 -5
  62. package/src/guard/memory-write.mjs +218 -0
  63. package/src/guard/prompt-guard.mjs +0 -1
  64. package/src/guard/tool-guard.mjs +52 -77
  65. package/src/inventory/agent-posture.mjs +236 -57
  66. package/src/inventory/artifacts/classify.mjs +10 -1
  67. package/src/inventory/artifacts/discover.mjs +113 -3
  68. package/src/inventory/artifacts/extensions.mjs +70 -0
  69. package/src/inventory/artifacts/hook-scripts.mjs +128 -0
  70. package/src/inventory/artifacts/limits.mjs +1 -1
  71. package/src/inventory/artifacts/plugins.mjs +105 -0
  72. package/src/inventory/artifacts/roots.mjs +40 -0
  73. package/src/inventory/discovery/ai-dependencies.mjs +39 -12
  74. package/src/inventory/discovery/all.mjs +4 -0
  75. package/src/inventory/discovery/cloud-clis.mjs +472 -0
  76. package/src/inventory/discovery/coding-agents.mjs +19 -4
  77. package/src/inventory/discovery/mcp-clients.mjs +16 -10
  78. package/src/inventory/discovery/mcp-servers.mjs +125 -35
  79. package/src/inventory/discovery/mcp-stores.mjs +207 -0
  80. package/src/inventory/env-redirect.mjs +148 -0
  81. package/src/inventory/grant-extract.mjs +463 -0
  82. package/src/inventory/project-roots.mjs +108 -0
  83. package/src/inventory/vscode-state.mjs +153 -0
  84. package/src/mcp/server-tools.mjs +1 -1
@@ -0,0 +1,463 @@
1
+
2
+ const MAX_DEPTH = 12;
3
+ const MAX_NODES = 5000;
4
+ const MAX_ARRAY_OBJECTS = 200;
5
+ const LIST_CAP = 30;
6
+ const STR_CAP = 200;
7
+
8
+ export const norm = (k) => String(k).normalize('NFKC').toLowerCase().replace(/[^a-z0-9]/g, '');
9
+
10
+ export const MODE_KEYS = new Set([
11
+ 'defaultmode', 'mode', 'permissionmode', 'approvalmode', 'defaultapprovalmode', 'agentmode',
12
+ 'executionmode', 'runmode', 'autonomy', 'autonomylevel', 'approvalpolicy', 'askforapproval',
13
+ 'agentautonomy', 'autoexecutionlevel', 'autoexecutionpolicy',
14
+ ]);
15
+
16
+ const FULL_MODE_KEYS = new Set(['chatpermissionsdefault']);
17
+
18
+ const GENERIC_MODE_VALUES = new Set(['bypasspermissions', 'yolo', 'acceptedits', 'autoedit', 'autonomous', 'fullauto']);
19
+
20
+ export const MODE_RANK = {
21
+ bypasspermissions: 5, yolo: 5, never: 5,
22
+ autonomous: 4, fullauto: 4, unattended: 4, unrestricted: 4, autopilot: 4, allowall: 4, noapproval: 4, autoapprove: 4, turbo: 4, runeverything: 4,
23
+ acceptedits: 2, autoedit: 2,
24
+ };
25
+
26
+ const FLAG_KEY = new RegExp(
27
+ '^(?:' + [
28
+ 'yolo', 'yolomode', 'enableyolo', 'dangerouslyskippermissions', 'skippermissions', 'bypasspermissions', 'disableapprovals?', 'noconfirm',
29
+ 'autorun', 'autoexecute', 'runwithoutasking', 'executewithoutconfirmation', 'allowalltools', 'enablealltools', 'autoapprovealltools',
30
+ 'executeallcommands', 'chattoolsautoapprove', 'chattoolsglobalautoapprove', 'chattoolsurlsautoapprove',
31
+ 'alwaysallowtoolactions', 'yolomodetoggled', 'alwaysallowexecute', 'trustalltools', 'skippermissionrequests', 'dangerouslyallowall', 'yoloenableruneverything', 'useyolomode', 'editfilesexternally',
32
+ 'alwaysallowwriteoutsideworkspace', 'alwaysallowwriteprotected', 'alwaysallowreadonlyoutsideworkspace', 'autoapprovalenabled',
33
+ 'alwaysallowbrowser', 'alwaysallowsubtasks', 'allowbypass', 'goosemode', 'defaultpermissions',
34
+ '(?:bypass|skip|disable|suppress|ignore|no|without|turnoff)(?:all)?(?:user)?(?:consent|approvals?|confirmations?|confirm|prompts?|permissions?|permissionchecks?|humanreview|hitl|safetychecks?|guardrails?)(?:prompts?|checks?|dialogs?|requests?)?',
35
+ '(?:disable|no|bypass|skip|without|turnoff)sandbox(?:ing)?', 'dangerouslydisablesandbox', 'unsandboxed', 'allowunsandboxed',
36
+ 'sandbox', 'sandboxed', 'sandboxenabled', 'enablesandbox', 'usesandbox', 'sandboxing', 'sandboxmode',
37
+ 'autoapprove', 'alwaysallow', 'autoallow', 'autoaccept', 'autoconfirm', 'fullauto', 'yesalways', 'trustall', 'autoapprovetools',
38
+ 'autoacceptedits', 'alwaysallowwrite', 'autoapprovewrite',
39
+ 'requireapproval', 'requireconfirmation', 'requirehumanapproval', 'humanintheloop', 'hitl', 'confirmbeforeexecute', 'confirmbeforerun',
40
+ 'confirmcommands', 'askbeforerunning', 'promptbeforeexecute', 'approvalrequired', 'confirmationrequired', 'confirmactions',
41
+ 'autoapprovemcp', 'alwaysallowmcp', 'autotrustmcp', 'trustallmcpservers', 'enableallmcpservers',
42
+ 'exposeall', 'exposeallenv', 'inheritall', 'passall', 'passthroughall', 'shareallenv', 'exposeenvironment', 'inheritenvironment', 'ignoredefaultexcludes',
43
+ 'allowbackgroundexecution', 'backgroundexecution', 'allowbackgroundprocesses', 'allowbackgroundtasks', 'allowdetachedprocesses', 'persistentprocesses', 'allowdaemons',
44
+ 'disableallhooks', 'skipdangerousmodepermissionprompt', 'networkaccess', 'allownetwork', 'networkenabled',
45
+ ].join('|') + ')$',
46
+ );
47
+
48
+ const SKIP_SUBTREES = new Set([
49
+ 'mcpservers', 'servers', 'contextservers', 'hooks', 'env', 'environment', 'history', 'projects', 'oauthaccount',
50
+ 'deny', 'disallowedtools', 'ask', 'excludetools', 'blocklist', 'denylist',
51
+ ]);
52
+
53
+ const FS_PARENT = /^(?:filesystem|fs|files|fileaccess|filesystemaccess|paths|disk|storage)$/;
54
+ const NET_PARENT = /^(?:network|net|networking|networkaccess|sandboxnetwork|networkpolicy|networkproxy)$/;
55
+ const ANYWHERE_FS_KEYS = { writableroots: 'write', additionalreadwritepaths: 'write', additionalwritablepaths: 'write', additionalreadonlypaths: 'read' };
56
+ const PATH_LIST_PARENTS = { write: 'write', fswrite: 'write', edit: 'write', read: 'read', fsread: 'read' };
57
+ const ALLOW_DECISION = new Set(['allow', 'alwaysallow', 'autoapprove', 'approve', 'auto']);
58
+ const ENV_PARENT = /^(?:environmentvariables|envvars|envvariables|environmentvars|envaccess|environmentaccess|envpolicy|environmentpolicy|shellenvironmentpolicy|secrets)$/;
59
+ const TOOLS_PARENT = /^(?:tools|shell|terminal|commands|exec|execution|process|processes)$/;
60
+
61
+ const FS_KEYS = {
62
+ read: 'read', readpaths: 'read', allowread: 'read', readable: 'read', readonly: 'read',
63
+ write: 'write', writepaths: 'write', allowwrite: 'write', writable: 'write', readwrite: 'write', modify: 'write',
64
+ execute: 'execute', exec: 'execute', executepaths: 'execute', allowexecute: 'execute', executable: 'execute', run: 'execute',
65
+ };
66
+ const OUTBOUND_KEYS = /^(?:outbound|egress|alloweddomains|allowedhosts|allowlist|allow|domains|hosts|destinations|allowedurls|allowedorigins|networkallowlist)$/;
67
+ const INBOUND_KEYS = /^(?:inbound|ingress|listen|allowinbound|incoming)$/;
68
+ const ENV_LIST_KEYS = /^(?:read|allow|allowlist|allowed|expose|exposed|include|includeonly|passthrough|share|whitelist|keys|names)$/;
69
+ const TOOLS_EXEC_KEYS = /^(?:allowexecution|allowexec|allowedexecution|execute|exec|commands|allowedcommands|executables|binaries|programs)$/;
70
+ const EXEC_LIST_KEYS = /^(?:allow|allowed|allowlist|allowexecution|allowexec|execute|exec|run|commands|allowedcommands|executables|binaries|programs)$/;
71
+ const COMMAND_LIST_KEYS = /^(?:allowedcommands|allowcommands|commandallowlist|shellcommands|allowedexecutables|allowedbinaries|terminalallowlist|cascadecommandsallowlist|trustedcommands|ampcommandsallowlist|yolocommandallowlist)$/;
72
+ const DIRECTORY_KEYS = /^(?:additionaldirectories|alloweddirectories|workspaces|workingdirectories)$/;
73
+ const TRUST_KEYS = /^(?:trustedfolders|trustedprojects)$/;
74
+
75
+ const SETTINGS_MARKER = /^(?:permissions|permission|defaultmode|allowedtools|disallowedtools|enableallprojectmcpservers|approvalpolicy|approvalmode|defaultapprovalmode|sandboxmode|defaultpermissions|autoapprovalsettings|dangerouslyskippermissions|skipdangerousmodepermissionprompt|filesystem|network|networkpolicy|allowbackgroundexecution|environmentvariables|chattoolsautoapprove|chattoolsglobalautoapprove|chattoolsterminalautoapprove|chatpermissionsdefault|yolo|yolomodetoggled|autoapprove|alwaysallow|alwaysallowexecute|alwaysallowtoolactions|autoapprovalenabled|additionaldirectories|additionalreadwritepaths|writableroots|trustedfolders|shellenvironmentpolicy|toolpermissions|goosemode|yesalways|rule|terminalallowlist|mcpallowlist|allowedcommands|cascadecommandsallowlist|agentautonomy|skippermissionrequests|foldertrust|type|dangerouslyallowall|ampcommandsallowlist)$/;
76
+
77
+ const SECRETISH = /(?:sk-[A-Za-z0-9_-]{10,}|gh[pousr]_[A-Za-z0-9]{20,}|AKIA[0-9A-Z]{16}|xox[abpr]-|eyJ[A-Za-z0-9_-]{10,}\.|-----BEGIN)/;
78
+
79
+ const isObj = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
80
+
81
+ const asList = (v) => (Array.isArray(v) ? v.filter((x) => typeof x === 'string') : typeof v === 'string' ? [v] : []);
82
+
83
+ function clean(s) {
84
+ const t = String(s).slice(0, STR_CAP).replace(/^([a-z]+:\/\/)[^/@\s]*@/i, '$1');
85
+ return SECRETISH.test(t) ? null : t;
86
+ }
87
+
88
+ function addAll(sink, values) {
89
+ for (const v of values) {
90
+ const c = clean(v);
91
+ if (c !== null && !sink.includes(c) && sink.length < LIST_CAP) sink.push(c);
92
+ }
93
+ }
94
+
95
+ function flagValue(v) {
96
+ if (typeof v === 'boolean' || (typeof v === 'number' && Number.isFinite(v))) return v;
97
+ if (typeof v === 'string' && v.length <= 40 && /^[\w .:/*@-]*$/.test(v) && !SECRETISH.test(v)) return v;
98
+ if (Array.isArray(v) && v.some((x) => typeof x === 'string' && /^\s*(?:\*+|all|\.\*|any)\s*$/i.test(x))) return ['*'];
99
+ return undefined;
100
+ }
101
+
102
+ function isSettingsNode(node) {
103
+ if (!isObj(node)) return false;
104
+ if (Array.isArray(node.allow) && ['ask', 'exclude', 'deny'].some((k) => Array.isArray(node[k]))) return true;
105
+ if ((node.decision ?? node.effect) !== undefined && (node.toolName ?? node.capability ?? node.tool ?? node.mcpName) !== undefined) return true;
106
+ return Object.entries(node).some(([k, v]) => (norm(k) === 'permissions' ? isObj(v) : SETTINGS_MARKER.test(norm(k)) || SETTINGS_MARKER.test(leafNorm(k))));
107
+ }
108
+
109
+ export function findSettingsNode(doc) {
110
+ const queue = [{ node: doc, depth: 0 }];
111
+ let seen = 0;
112
+ while (queue.length && seen++ < MAX_NODES) {
113
+ const { node, depth } = queue.shift();
114
+ if (isSettingsNode(node)) return node;
115
+ if (depth >= 5 || !node || typeof node !== 'object') continue;
116
+ if (Array.isArray(node)) {
117
+ for (const el of node.slice(0, MAX_ARRAY_OBJECTS)) if (isObj(el)) queue.push({ node: el, depth: depth + 1 });
118
+ continue;
119
+ }
120
+ for (const [k, v] of Object.entries(node)) if (v && typeof v === 'object' && !SKIP_SUBTREES.has(leafNorm(k))) queue.push({ node: v, depth: depth + 1 });
121
+ }
122
+ return null;
123
+ }
124
+
125
+ export function emptyGrant() {
126
+ return {
127
+ mode: null,
128
+ allow: [], deny: [], ask: [],
129
+ flags: {},
130
+ fileSystem: { read: [], write: [], execute: [] },
131
+ network: { outbound: [], inbound: [] },
132
+ envNames: [],
133
+ commands: [],
134
+ directories: [],
135
+ trustedFolders: [],
136
+ rules: [],
137
+ permission: null,
138
+ toolPermissions: null,
139
+ unixSockets: [],
140
+ mcpAllowlist: [],
141
+ folderTrustDisabled: false,
142
+ enableAllProjectMcpServers: null,
143
+ };
144
+ }
145
+
146
+ const DECISIONS = new Set(['allow', 'ask', 'deny']);
147
+
148
+ function collectRule(g, node) {
149
+ const perm = node.permission;
150
+ const decision = node.decision ?? node.effect ?? node.action ?? (typeof perm === 'string' ? perm : isObj(perm) ? perm.type : undefined);
151
+ if (typeof decision !== 'string' || !ALLOW_DECISION.has(norm(decision)) || g.rules.length >= LIST_CAP) return;
152
+ const tools = asList(node.toolName ?? node.tool_name ?? node.tool ?? node.capability).map(clean).filter(Boolean).slice(0, 10);
153
+ const mcpName = typeof node.mcpName === 'string' ? clean(node.mcpName) : null;
154
+ if (!tools.length && !mcpName) return;
155
+ const rawPattern = asList(node.commandPrefix ?? node.commandRegex ?? node.match ?? node.shellInputRegex ?? node.pattern ?? node.argsPattern)[0];
156
+ const pattern = rawPattern !== undefined ? clean(rawPattern) : null;
157
+ g.rules.push({ ...(tools.length ? { toolName: tools } : {}), ...(mcpName ? { mcpName } : {}), ...(pattern ? { pattern } : {}), decision: 'allow' });
158
+ }
159
+
160
+ function collectPermissionMap(g, value) {
161
+ if (typeof value === 'string') {
162
+ if (norm(value) === 'allow') g.permission = 'allow';
163
+ return;
164
+ }
165
+ if (!isObj(value)) return;
166
+ const out = isObj(g.permission) ? g.permission : {};
167
+ for (const [tool, decision] of Object.entries(value).slice(0, 40)) {
168
+ const t = clean(tool);
169
+ if (!t) continue;
170
+ if (typeof decision === 'string' && DECISIONS.has(norm(decision))) out[t] = norm(decision);
171
+ else if (isObj(decision)) {
172
+ const map = {};
173
+ for (const [pat, d] of Object.entries(decision).slice(0, LIST_CAP)) {
174
+ const cp = clean(pat);
175
+ if (cp !== null && typeof d === 'string' && DECISIONS.has(norm(d))) map[cp] = norm(d);
176
+ }
177
+ if (Object.keys(map).length) out[t] = map;
178
+ }
179
+ }
180
+ if (Object.keys(out).length && g.permission !== 'allow') g.permission = out;
181
+ }
182
+
183
+ function collectToolPermissions(g, k, value) {
184
+ const tp = g.toolPermissions ?? (g.toolPermissions = {});
185
+ if (k === 'default' && typeof value === 'string') tp.default = norm(value).slice(0, 20);
186
+ if (k !== 'tools' || !isObj(value)) return;
187
+ tp.tools = tp.tools ?? {};
188
+ for (const [tool, cfg] of Object.entries(value).slice(0, 40)) {
189
+ if (!isObj(cfg)) continue;
190
+ const entry = {};
191
+ if (typeof cfg.default === 'string') entry.default = norm(cfg.default);
192
+ const patterns = (Array.isArray(cfg.always_allow) ? cfg.always_allow : [])
193
+ .map((e) => (typeof e === 'string' ? e : typeof e?.pattern === 'string' ? e.pattern : null))
194
+ .filter((x) => x !== null).map(clean).filter((x) => x !== null).slice(0, LIST_CAP);
195
+ if (patterns.length) entry.always_allow = patterns.map((pattern) => ({ pattern }));
196
+ if (Object.keys(entry).length) tp.tools[clean(tool) ?? 'tool'] = entry;
197
+ }
198
+ }
199
+
200
+ function takeMode(g, value) {
201
+ const v = String(value);
202
+ const rank = MODE_RANK[norm(v)] ?? 1;
203
+ const cur = g.mode ? MODE_RANK[norm(g.mode)] ?? 1 : 0;
204
+ if (!g.mode || rank > cur) g.mode = v.slice(0, 40);
205
+ }
206
+
207
+ const TOGGLE_CONTAINER = /^(?:autoapprovalsettings|autoapprove|autorun|autoexecute|yolo|yolomode|fullauto|autoaccept)$/;
208
+ const PRIORITY_CHILD = /^(?:permissions?|agentsettings|settings|agent|agents|sandbox|sandboxworkspacewrite|tools|toolssettings|toolpermissions|filesystem|network|environmentvariables|shellenvironmentpolicy|autoapprovalsettings|security|general|profiles|chat)$/;
209
+
210
+ export const leafNorm = (rawKey) => {
211
+ const s = String(rawKey);
212
+ return s.includes('.') ? norm(s.slice(s.lastIndexOf('.') + 1)) : norm(s);
213
+ };
214
+
215
+ function ordered(node) {
216
+ const rank = ([k, v]) => (!v || typeof v !== 'object' ? 0 : PRIORITY_CHILD.test(leafNorm(k)) ? 1 : 2);
217
+ return Object.entries(node).sort((a, b) => rank(a) - rank(b));
218
+ }
219
+
220
+ function walk(g, node, depth, parent, budget) {
221
+ if (!node || typeof node !== 'object') return;
222
+ if (depth > MAX_DEPTH || budget.n >= MAX_NODES) {
223
+ g.truncated = true;
224
+ return;
225
+ }
226
+ budget.n++;
227
+ if (Array.isArray(node)) {
228
+ if (node.length > MAX_ARRAY_OBJECTS) g.truncated = true;
229
+ for (const el of node.slice(0, MAX_ARRAY_OBJECTS)) if (isObj(el)) walk(g, el, depth + 1, parent, budget);
230
+ return;
231
+ }
232
+ const p = leafNorm(parent);
233
+ if ((node.enabled === false || node.enable === false) && TOGGLE_CONTAINER.test(p)) return;
234
+ collectRule(g, node);
235
+ if (depth === 0 && Array.isArray(node.allow) && ['ask', 'exclude', 'deny'].some((s) => Array.isArray(node[s]))) addAll(g.allow, asList(node.allow));
236
+ const inFs = FS_PARENT.test(p);
237
+ const inNet = NET_PARENT.test(p);
238
+ for (const [rawKey, value] of ordered(node)) {
239
+ const kFull = norm(rawKey);
240
+ const k = leafNorm(rawKey);
241
+
242
+ if (k === 'deny' && p === 'permissions') addAll(g.deny, asList(value));
243
+ if (k === 'ask' && p === 'permissions') addAll(g.ask, asList(value));
244
+ if (k === 'disallowedtools') addAll(g.deny, asList(value));
245
+
246
+ if (k === 'permission' && !(isObj(value) && 'type' in value)) collectPermissionMap(g, value);
247
+ if (p === 'toolpermissions') collectToolPermissions(g, k, value);
248
+ if (k === 'allowunixsockets') addAll(g.unixSockets, asList(value));
249
+ if (k === 'mcpallowlist') addAll(g.mcpAllowlist, asList(value).filter((v) => /^\s*\*\s*(?::\s*\*)?\s*$/.test(v)));
250
+ if (p === 'foldertrust' && k === 'enabled' && value === false) g.folderTrustDisabled = true;
251
+ if (kFull === 'chattoolseditsautoapprove' && isObj(value)) addAll(g.fileSystem.write, Object.entries(value).filter(([, on]) => on === true).map(([glob]) => glob));
252
+ if (k === 'type' && typeof value === 'string' && norm(value) === 'insecurenone') g.flags.type = 'insecure_none';
253
+ if (ANYWHERE_FS_KEYS[k]) addAll(g.fileSystem[ANYWHERE_FS_KEYS[k]], asList(value));
254
+ if (PATH_LIST_PARENTS[p] && /^(?:allowedpaths|allowpaths|paths)$/.test(k)) addAll(g.fileSystem[PATH_LIST_PARENTS[p]], asList(value));
255
+ if (inFs && typeof value === 'string' && /^(?:read|write)$/i.test(value) && /[\\/~*]|^\.{1,2}$/.test(rawKey)) addAll(g.fileSystem[value.toLowerCase()], [rawKey]);
256
+ if (inNet && k === 'default' && /^allow$/i.test(String(value))) addAll(g.network.outbound, ['*']);
257
+ if (inNet && k === 'enabled' && value === true) g.flags.networkEnabled = true;
258
+
259
+ if ((MODE_KEYS.has(k) || FULL_MODE_KEYS.has(kFull)) && typeof value === 'string' && (k !== 'mode' || GENERIC_MODE_VALUES.has(norm(value)))) takeMode(g, value);
260
+ else if (k === 'enableallprojectmcpservers') {
261
+ if (value === true) g.enableAllProjectMcpServers = true;
262
+ else if (value === false && g.enableAllProjectMcpServers === null) g.enableAllProjectMcpServers = false;
263
+ } else if (FLAG_KEY.test(kFull) || FLAG_KEY.test(k)) {
264
+ const fv = flagValue(value);
265
+ if (fv !== undefined && !(rawKey in g.flags)) g.flags[String(rawKey).slice(0, 60)] = fv;
266
+ }
267
+
268
+ if ((k === 'allow' && p === 'permissions') || k === 'allowedtools' || (k === 'allowed' && p === 'tools')) addAll(g.allow, asList(value));
269
+
270
+ if (FS_PARENT.test(p) && FS_KEYS[k]) addAll(g.fileSystem[FS_KEYS[k]], value === true ? ['/**'] : asList(value));
271
+ else if (/^(?:readpaths|writepaths|executepaths|allowread|allowwrite|allowexecute)$/.test(k)) addAll(g.fileSystem[FS_KEYS[k]], asList(value));
272
+
273
+ const listy = Array.isArray(value) || typeof value === 'string' || value === true;
274
+ if (NET_PARENT.test(p) && OUTBOUND_KEYS.test(k)) addAll(g.network.outbound, value === true ? ['*'] : asList(value));
275
+ else if (NET_PARENT.test(p) && INBOUND_KEYS.test(k)) addAll(g.network.inbound, value === true ? ['*'] : asList(value));
276
+ else if (listy && /^(?:egress|outbound|alloweddomains|networkallowlist|allowedurls)$/.test(k)) addAll(g.network.outbound, asList(value));
277
+ else if (listy && /^(?:inbound|ingress)$/.test(k)) addAll(g.network.inbound, asList(value));
278
+
279
+ if (ENV_PARENT.test(p) && ENV_LIST_KEYS.test(k)) addAll(g.envNames, asList(value).filter((n) => /^(?:\*+|[A-Za-z_][A-Za-z0-9_]*\*?)$/.test(n.trim())));
280
+
281
+ if ((TOOLS_PARENT.test(p) && (p === 'tools' ? TOOLS_EXEC_KEYS : EXEC_LIST_KEYS).test(k)) || COMMAND_LIST_KEYS.test(k) || COMMAND_LIST_KEYS.test(kFull)) addAll(g.commands, asList(value));
282
+
283
+ if (DIRECTORY_KEYS.test(k)) addAll(g.directories, asList(value));
284
+ if (TRUST_KEYS.test(k)) addAll(g.trustedFolders, asList(value));
285
+ if (k === 'projects' && isObj(value)) {
286
+ addAll(g.trustedFolders, Object.entries(value).filter(([, v]) => norm(v?.trust_level ?? v?.trustLevel ?? '') === 'trusted').map(([path]) => path));
287
+ }
288
+
289
+ if (value && typeof value === 'object' && !SKIP_SUBTREES.has(k)) {
290
+ if (isObj(value) && (kFull === 'chattoolsterminalautoapprove' || kFull === 'terminalautoapprove')) {
291
+ const wild = Object.entries(value).find(([cmd, on]) => on === true && /^(?:\*+|\/\.\*\/[a-z]*|\.\*)$/.test(cmd.trim()));
292
+ if (wild && !(rawKey in g.flags)) g.flags[String(rawKey).slice(0, 60)] = ['*'];
293
+ continue;
294
+ }
295
+ walk(g, value, depth + 1, rawKey, budget);
296
+ }
297
+ }
298
+ }
299
+
300
+ export function extractGrant(doc, into = emptyGrant()) {
301
+ if (!isObj(doc)) return into;
302
+ if (!findSettingsNode(doc) && !isObj(doc.projects)) return into;
303
+ walk(into, doc, 0, '', { n: 0 });
304
+ return into;
305
+ }
306
+
307
+ export function grantIsEmpty(g) {
308
+ return !g || (
309
+ !g.mode && !g.allow.length && !g.deny.length && !g.ask.length && !Object.keys(g.flags).length &&
310
+ !g.fileSystem.read.length && !g.fileSystem.write.length && !g.fileSystem.execute.length &&
311
+ !g.network.outbound.length && !g.network.inbound.length && !g.envNames.length && !g.commands.length &&
312
+ !g.rules.length && !g.permission && !g.toolPermissions && !g.unixSockets.length && !g.mcpAllowlist.length && !g.folderTrustDisabled &&
313
+ !g.directories.length && !g.trustedFolders.length && g.enableAllProjectMcpServers !== true
314
+ );
315
+ }
316
+
317
+ function scanJson(raw, onChar) {
318
+ let out = '';
319
+ let inStr = false;
320
+ for (let i = 0; i < raw.length; i++) {
321
+ const c = raw[i];
322
+ if (inStr) {
323
+ out += c;
324
+ if (c === '\\') out += raw[++i] ?? '';
325
+ else if (c === '"') inStr = false;
326
+ continue;
327
+ }
328
+ if (c === '"') { inStr = true; out += c; continue; }
329
+ const r = onChar(raw, i);
330
+ if (r === undefined) out += c;
331
+ else { out += r.emit; i = r.next; }
332
+ }
333
+ return out;
334
+ }
335
+
336
+ export function stripJsonc(raw) {
337
+ const noComments = scanJson(String(raw).replace(/^/, ''), (s, i) => {
338
+ if (s[i] === '/' && s[i + 1] === '/') {
339
+ let j = i;
340
+ while (j < s.length && s[j] !== '\n') j++;
341
+ return { emit: '\n', next: j };
342
+ }
343
+ if (s[i] === '/' && s[i + 1] === '*') {
344
+ const end = s.indexOf('*/', i + 2);
345
+ return { emit: ' ', next: end === -1 ? s.length : end + 1 };
346
+ }
347
+ return undefined;
348
+ });
349
+ return scanJson(noComments, (s, i) => {
350
+ if (s[i] !== ',') return undefined;
351
+ let j = i + 1;
352
+ while (j < s.length && /\s/.test(s[j])) j++;
353
+ return s[j] === '}' || s[j] === ']' ? { emit: '', next: i } : undefined;
354
+ });
355
+ }
356
+
357
+ function yamlScalar(raw) {
358
+ const hash = raw.search(/\s#/);
359
+ const v = (hash === -1 ? raw : raw.slice(0, hash)).trim();
360
+ if (/^(?:true|yes|on)$/i.test(v)) return true;
361
+ if (/^(?:false|no|off)$/i.test(v)) return false;
362
+ if (v === 'null' || v === '~' || v === '') return null;
363
+ if (/^-?\d+(\.\d+)?$/.test(v)) return Number(v);
364
+ if (/^\[.*\]$/.test(v)) return v.slice(1, -1).split(',').map((t) => yamlScalar(t)).filter((t) => t !== null);
365
+ return v.replace(/^(["'])(.*)\1$/, '$2');
366
+ }
367
+
368
+ export function parseLooseYaml(text) {
369
+ if (typeof text !== 'string' || text.length > 512 * 1024) return null;
370
+ const root = {};
371
+ const stack = [{ indent: -1, node: root }];
372
+ let pending = null;
373
+ let sawAny = false;
374
+ for (const rawLine of text.split(/\r?\n/)) {
375
+ if (!rawLine.trim() || /^\s*#/.test(rawLine) || /^\s*---\s*$/.test(rawLine)) continue;
376
+ const indent = rawLine.length - rawLine.trimStart().length;
377
+ const line = rawLine.trim();
378
+ const item = /^-\s*(.*)$/.exec(line);
379
+ if (pending && (indent > pending.indent || (item && indent >= pending.indent))) {
380
+ const node = item ? [] : {};
381
+ pending.parent[pending.key] = node;
382
+ stack.push({ indent: pending.indent, node });
383
+ }
384
+ pending = null;
385
+ while (stack.length > 1 && indent <= stack[stack.length - 1].indent && !(item && Array.isArray(stack[stack.length - 1].node) && indent === stack[stack.length - 1].indent)) stack.pop();
386
+ const frame = stack[stack.length - 1];
387
+ if (item) {
388
+ if (!Array.isArray(frame.node)) continue;
389
+ sawAny = true;
390
+ const inner = /^["']?([^:"'\s][^:"']*)["']?\s*:\s*(.*)$/.exec(item[1]);
391
+ if (inner && !/^\w+\(/.test(item[1])) {
392
+ const obj = {};
393
+ frame.node.push(obj);
394
+ stack.push({ indent: indent + 1, node: obj });
395
+ if (inner[2]) obj[inner[1].trim()] = yamlScalar(inner[2]);
396
+ else pending = { parent: obj, key: inner[1].trim(), indent: indent + 1 };
397
+ } else if (item[1]) frame.node.push(yamlScalar(item[1]));
398
+ continue;
399
+ }
400
+ const kv = /^["']?([^:"'][^:"']*?)["']?\s*:(?:\s+(.*)|\s*)$/.exec(line);
401
+ if (!kv || Array.isArray(frame.node)) continue;
402
+ sawAny = true;
403
+ const key = kv[1].trim();
404
+ const rest = (kv[2] ?? '').trim();
405
+ if (!rest) {
406
+ frame.node[key] = null;
407
+ pending = { parent: frame.node, key, indent };
408
+ } else frame.node[key] = yamlScalar(rest);
409
+ }
410
+ return sawAny ? root : null;
411
+ }
412
+
413
+ function tomlValue(raw) {
414
+ const v = raw.trim();
415
+ if (/^(true|false)$/.test(v)) return v === 'true';
416
+ if (/^-?\d+(\.\d+)?$/.test(v)) return Number(v);
417
+ if (/^"(?:[^"\\]|\\.)*"$/.test(v)) {
418
+ try { return JSON.parse(v); } catch { return v.slice(1, -1); }
419
+ }
420
+ if (/^'[^']*'$/.test(v)) return v.slice(1, -1);
421
+ if (v.startsWith('[')) {
422
+ const out = [];
423
+ for (const m of v.matchAll(/"((?:[^"\\]|\\.)*)"|'([^']*)'/g)) out.push(m[1] !== undefined ? m[1] : m[2]);
424
+ return out;
425
+ }
426
+ return undefined;
427
+ }
428
+
429
+ function tomlKey(raw) {
430
+ const parts = [];
431
+ for (const m of raw.trim().matchAll(/"((?:[^"\\]|\\.)*)"|'([^']*)'|([^.\s"']+)/g)) parts.push(m[1] ?? m[2] ?? m[3]);
432
+ return parts;
433
+ }
434
+
435
+ export function parseLooseToml(text) {
436
+ if (typeof text !== 'string' || text.length > 512 * 1024) return null;
437
+ const root = {};
438
+ let cur = root;
439
+ let pending = null;
440
+ const lines = text.split(/\r?\n/);
441
+ for (let i = 0; i < lines.length; i++) {
442
+ let line = lines[i].replace(/(^|\s)#.*$/, '').trim();
443
+ if (!line) continue;
444
+ const section = /^\[\[?\s*(.+?)\s*\]\]?$/.exec(line);
445
+ if (section && !line.includes('=')) {
446
+ cur = root;
447
+ for (const k of tomlKey(section[1])) cur = isObj(cur[k]) ? cur[k] : (cur[k] = {});
448
+ continue;
449
+ }
450
+ const eq = line.indexOf('=');
451
+ if (eq === -1) continue;
452
+ const keys = tomlKey(line.slice(0, eq));
453
+ let rhs = line.slice(eq + 1).trim();
454
+ if (rhs.startsWith('[') && !rhs.includes(']')) {
455
+ while (++i < lines.length && !rhs.includes(']')) rhs += ' ' + lines[i].replace(/(^|\s)#.*$/, '').trim();
456
+ }
457
+ pending = cur;
458
+ for (const k of keys.slice(0, -1)) pending = isObj(pending[k]) ? pending[k] : (pending[k] = {});
459
+ const val = tomlValue(rhs);
460
+ if (val !== undefined && keys.length) pending[keys[keys.length - 1]] = val;
461
+ }
462
+ return root;
463
+ }
@@ -0,0 +1,108 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { parseLooseToml, stripJsonc } from './grant-extract.mjs';
6
+
7
+ const HOME = os.homedir();
8
+ const PLAT = process.platform;
9
+ const MAX_ROOTS = 40;
10
+ const MAX_WORKSPACES = 150;
11
+ const MAX_CLAUDE_JSON = 8 * 1024 * 1024;
12
+
13
+ function editorUserDir(app) {
14
+ if (PLAT === 'win32') return path.join(process.env.APPDATA || path.join(HOME, 'AppData', 'Roaming'), app, 'User');
15
+ if (PLAT === 'darwin') return path.join(HOME, 'Library', 'Application Support', app, 'User');
16
+ return path.join(HOME, '.config', app, 'User');
17
+ }
18
+
19
+ function readJson(file, max) {
20
+ try {
21
+ const st = fs.statSync(file);
22
+ if (!st.isFile() || st.size > max) return null;
23
+ return JSON.parse(stripJsonc(fs.readFileSync(file, 'utf8')));
24
+ } catch {
25
+ return null;
26
+ }
27
+ }
28
+
29
+ function claudeProjects() {
30
+ const json = readJson(path.join(HOME, '.claude.json'), MAX_CLAUDE_JSON);
31
+ return json && typeof json.projects === 'object' && !Array.isArray(json.projects) ? Object.keys(json.projects).map((p) => ({ dir: p, at: 0 })) : [];
32
+ }
33
+
34
+ function codexProjects() {
35
+ try {
36
+ const doc = parseLooseToml(fs.readFileSync(path.join(HOME, '.codex', 'config.toml'), 'utf8'));
37
+ return doc && typeof doc.projects === 'object' ? Object.keys(doc.projects).map((p) => ({ dir: p, at: 0 })) : [];
38
+ } catch {
39
+ return [];
40
+ }
41
+ }
42
+
43
+ function folderFromUri(uri) {
44
+ if (typeof uri !== 'string' || !uri.startsWith('file://')) return null;
45
+ try {
46
+ return fileURLToPath(uri);
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+
52
+ function editorWorkspaces() {
53
+ const out = [];
54
+ for (const app of ['Code', 'Code - Insiders', 'Cursor', 'Windsurf']) {
55
+ const dir = path.join(editorUserDir(app), 'workspaceStorage');
56
+ let entries;
57
+ try {
58
+ entries = fs.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory());
59
+ } catch {
60
+ continue;
61
+ }
62
+ const stamped = [];
63
+ for (const e of entries) {
64
+ const file = path.join(dir, e.name, 'workspace.json');
65
+ try {
66
+ stamped.push({ file, at: fs.statSync(file).mtimeMs });
67
+ } catch {}
68
+ }
69
+ stamped.sort((a, b) => b.at - a.at);
70
+ for (const { file, at } of stamped.slice(0, MAX_WORKSPACES)) {
71
+ const folder = folderFromUri(readJson(file, 64 * 1024)?.folder);
72
+ if (folder) out.push({ dir: folder, at });
73
+ }
74
+ }
75
+ return out;
76
+ }
77
+
78
+ function isProjectDir(dir) {
79
+ try {
80
+ const resolved = path.resolve(dir);
81
+ if (resolved === path.resolve(HOME) || resolved === path.parse(resolved).root) return false;
82
+ return fs.statSync(resolved).isDirectory();
83
+ } catch {
84
+ return false;
85
+ }
86
+ }
87
+
88
+ export function projectRoots(cwd = process.cwd(), extra = []) {
89
+ const candidates = [
90
+ ...extra.map((dir) => ({ dir, at: Infinity })),
91
+ ...editorWorkspaces(),
92
+ ...claudeProjects(),
93
+ ...codexProjects(),
94
+ ];
95
+ candidates.sort((a, b) => b.at - a.at);
96
+ const seen = new Set([path.resolve(cwd).toLowerCase()]);
97
+ const roots = [];
98
+ for (const { dir } of candidates) {
99
+ if (!dir) continue;
100
+ const key = path.resolve(dir).toLowerCase();
101
+ if (seen.has(key)) continue;
102
+ seen.add(key);
103
+ if (!isProjectDir(dir)) continue;
104
+ roots.push(path.resolve(dir));
105
+ if (roots.length >= MAX_ROOTS) break;
106
+ }
107
+ return roots;
108
+ }