@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
@@ -2,6 +2,10 @@
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import os from 'node:os';
5
+ import { emptyGrant, extractGrant, grantIsEmpty, parseLooseToml, parseLooseYaml, stripJsonc } from './grant-extract.mjs';
6
+ import { readMcpServers } from '../detect/signals/mcp-config.mjs';
7
+ import { CURSOR_APP_USER_KEY, INTERPRETERS, readStateKeys } from './vscode-state.mjs';
8
+ import { projectRoots } from './project-roots.mjs';
5
9
 
6
10
  const HOME = os.homedir();
7
11
  const PLAT = process.platform;
@@ -15,14 +19,17 @@ export const TIER_MEANING = {
15
19
  none: 'No machine-readable permission surface. This agent appears in inventory only.',
16
20
  };
17
21
 
18
- function readJsonc(file) {
22
+ function readJsonc(file, maxBytes = MAX_SETTINGS_BYTES) {
19
23
  try {
20
24
  const st = fs.statSync(file);
21
- if (!st.isFile() || st.size > MAX_SETTINGS_BYTES) return { state: 'unreadable', reason: st.isFile() ? 'oversized' : 'not-a-file' };
25
+ if (!st.isFile() || st.size > maxBytes) return { state: 'unreadable', reason: st.isFile() ? 'oversized' : 'not-a-file' };
22
26
  const raw = fs.readFileSync(file, 'utf8');
27
+ if (/\.(?:toml|ya?ml)$/i.test(file)) {
28
+ const doc = /\.toml$/i.test(file) ? parseLooseToml(raw) : parseLooseYaml(raw);
29
+ return doc ? { state: 'read', json: doc } : { state: 'unreadable', reason: 'parse-failed' };
30
+ }
23
31
 
24
- const stripped = raw.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|[^:"])\/\/.*$/gm, '$1');
25
- return { state: 'read', json: JSON.parse(stripped) };
32
+ return { state: 'read', json: JSON.parse(stripJsonc(raw)) };
26
33
  } catch (err) {
27
34
  if (err && err.code === 'ENOENT') return { state: 'absent' };
28
35
 
@@ -30,6 +37,10 @@ function readJsonc(file) {
30
37
  }
31
38
  }
32
39
 
40
+ const COPILOT_KEYS = ['chat.', 'github.copilot'];
41
+
42
+ const EDITOR_APPS = ['Code', 'Code - Insiders', 'Cursor', 'Windsurf'];
43
+
33
44
  export const VENDOR_POSTURE = {
34
45
  'claude-code': {
35
46
  tier: 'declared',
@@ -40,6 +51,8 @@ export const VENDOR_POSTURE = {
40
51
  { rel: ['.claude', 'settings.local.json'], base: 'home', scope: 'user' },
41
52
  { rel: ['.claude', 'settings.json'], base: 'cwd', scope: 'project' },
42
53
  { rel: ['.claude', 'settings.local.json'], base: 'cwd', scope: 'project' },
54
+ { rel: ['managed-settings.json'], base: 'claude-managed', scope: 'managed' },
55
+ { rel: ['managed-settings.d'], base: 'claude-managed', scope: 'managed', dir: '.json' },
43
56
  ],
44
57
  },
45
58
  gemini: {
@@ -48,22 +61,43 @@ export const VENDOR_POSTURE = {
48
61
  sources: [
49
62
  { rel: ['.gemini', 'settings.json'], base: 'home', scope: 'user' },
50
63
  { rel: ['.gemini', 'settings.json'], base: 'cwd', scope: 'project' },
64
+ { rel: ['.gemini', 'policies'], base: 'home', scope: 'user', dir: '.toml' },
65
+ { rel: ['settings.json'], base: 'gemini-system', scope: 'managed' },
66
+ { rel: ['system-defaults.json'], base: 'gemini-system', scope: 'managed' },
67
+ ],
68
+ },
69
+ 'qwen-code': {
70
+ tier: 'declared',
71
+ label: 'Qwen Code',
72
+ sources: [
73
+ { rel: ['.qwen', 'settings.json'], base: 'home', scope: 'user' },
74
+ { rel: ['.qwen', 'settings.json'], base: 'cwd', scope: 'project' },
75
+ { rel: ['settings.json'], base: 'qwen-system', scope: 'managed' },
51
76
  ],
52
77
  },
53
78
  codex: {
54
79
  tier: 'declared',
55
80
  label: 'OpenAI Codex CLI',
56
81
  sources: [
82
+ { rel: ['.codex', 'config.toml'], base: 'home', scope: 'user' },
83
+ { rel: ['.codex', 'config.toml'], base: 'cwd', scope: 'project' },
57
84
  { rel: ['.codex', 'config.json'], base: 'home', scope: 'user' },
58
85
  { rel: ['.codex', 'settings.json'], base: 'home', scope: 'user' },
59
86
  ],
60
87
  },
61
88
  cursor: {
62
- tier: 'flag',
89
+ tier: 'declared',
63
90
  label: 'Cursor',
64
91
  sources: [
92
+ { stateDb: 'Cursor', dbKeys: [CURSOR_APP_USER_KEY], interpret: 'cursor', scope: 'user' },
93
+ { rel: ['.cursor', 'cli-config.json'], base: 'home', scope: 'user' },
94
+ { rel: ['.cursor', 'permissions.json'], base: 'home', scope: 'user' },
95
+ { rel: ['.cursor', 'sandbox.json'], base: 'home', scope: 'user' },
65
96
  { rel: ['.cursor', 'settings.json'], base: 'home', scope: 'user' },
66
97
  { rel: ['.cursor', 'mcp.json'], base: 'home', scope: 'user' },
98
+ { rel: ['.cursor', 'cli.json'], base: 'cwd', scope: 'project' },
99
+ { rel: ['.cursor', 'permissions.json'], base: 'cwd', scope: 'project' },
100
+ { rel: ['.cursor', 'sandbox.json'], base: 'cwd', scope: 'project' },
67
101
  { rel: ['.cursor', 'mcp.json'], base: 'cwd', scope: 'project' },
68
102
  { rel: ['.cursor', 'settings.json'], base: 'cwd', scope: 'project' },
69
103
  { rel: ['User', 'settings.json'], base: 'cursor-user', scope: 'user' },
@@ -76,31 +110,113 @@ export const VENDOR_POSTURE = {
76
110
  { rel: ['.codeium', 'windsurf', 'settings.json'], base: 'home', scope: 'user' },
77
111
  { rel: ['.codeium', 'windsurf', 'mcp_config.json'], base: 'home', scope: 'user' },
78
112
  { rel: ['.windsurf', 'mcp.json'], base: 'cwd', scope: 'project' },
113
+ { rel: ['settings.json'], base: 'windsurf-user', scope: 'user', keys: ['windsurf.', 'cascade'] },
114
+ { stateDb: 'Windsurf', dbKeys: ['windsurf%', 'codeium%'], interpret: 'windsurf', scope: 'user' },
79
115
  ],
80
116
  },
81
117
  cline: {
82
- tier: 'flag',
118
+ tier: 'declared',
83
119
  label: 'Cline',
84
120
  sources: [
85
121
  { rel: ['settings', 'cline_mcp_settings.json'], base: 'cline-storage', scope: 'user' },
86
122
  { rel: ['.cline', 'settings.json'], base: 'home', scope: 'user' },
123
+ { rel: ['.cline', 'data', 'globalState.json'], base: 'home', scope: 'user', interpret: 'cline', maxBytes: 8 * 1024 * 1024 },
124
+ { rel: ['settings.json'], base: 'vscode-user', scope: 'user', keys: ['cline.'] },
125
+ ...EDITOR_APPS.map((app) => ({ stateDb: app, dbKeys: ['saoudrizwan.claude-dev'], interpret: 'cline', scope: 'user' })),
87
126
  ],
88
127
  },
89
128
  roo: {
90
- tier: 'flag',
129
+ tier: 'declared',
91
130
  label: 'Roo Code',
92
131
  sources: [
93
132
  { rel: ['settings', 'mcp_settings.json'], base: 'roo-storage', scope: 'user' },
94
133
  { rel: ['.roo', 'settings.json'], base: 'cwd', scope: 'project' },
134
+ { rel: ['settings.json'], base: 'vscode-user', scope: 'user', keys: ['roo-cline.'] },
135
+ ...EDITOR_APPS.map((app) => ({ stateDb: app, dbKeys: ['rooveterinaryinc.roo-cline'], interpret: 'roo', scope: 'user' })),
95
136
  ],
96
137
  },
97
138
  copilot: {
98
139
  tier: 'flag',
99
- label: 'GitHub Copilot CLI',
100
- sources: [{ rel: ['.copilot', 'config.json'], base: 'home', scope: 'user' }],
140
+ label: 'GitHub Copilot',
141
+ sources: [
142
+ { rel: ['.copilot', 'config.json'], base: 'home', scope: 'user' },
143
+ { rel: ['.copilot', 'settings.json'], base: 'home', scope: 'user' },
144
+ { rel: ['settings.json'], base: 'vscode-user', scope: 'user', keys: COPILOT_KEYS },
145
+ { rel: ['settings.json'], base: 'vscode-insiders-user', scope: 'user', keys: COPILOT_KEYS },
146
+ { rel: ['.vscode', 'settings.json'], base: 'cwd', scope: 'project', keys: COPILOT_KEYS },
147
+ { rel: ['.github', 'copilot', 'settings.json'], base: 'cwd', scope: 'project' },
148
+ { rel: ['.github', 'copilot', 'settings.local.json'], base: 'cwd', scope: 'project' },
149
+ ],
150
+ },
151
+ kiro: {
152
+ tier: 'declared',
153
+ label: 'Kiro',
154
+ sources: [
155
+ { rel: ['.kiro', 'settings', 'permissions.yaml'], base: 'home', scope: 'user' },
156
+ { rel: ['.kiro', 'agents'], base: 'home', scope: 'user', dir: '.json' },
157
+ { rel: ['.kiro', 'agents'], base: 'cwd', scope: 'project', dir: '.json' },
158
+ { rel: ['.kiro', 'settings'], base: 'home', scope: 'user', dir: '.json' },
159
+ { rel: ['.kiro', 'settings'], base: 'cwd', scope: 'project', dir: '.json' },
160
+ { rel: ['settings.json'], base: 'vscode-user', scope: 'user', keys: ['kiroAgent.'] },
161
+ { rel: ['.aws', 'amazonq', 'cli-agents'], base: 'home', scope: 'user', dir: '.json' },
162
+ ],
163
+ },
164
+ opencode: {
165
+ tier: 'declared',
166
+ label: 'OpenCode',
167
+ sources: [
168
+ { rel: ['.config', 'opencode', 'opencode.json'], base: 'home', scope: 'user' },
169
+ { rel: ['.config', 'opencode', 'opencode.jsonc'], base: 'home', scope: 'user' },
170
+ { rel: ['opencode.json'], base: 'cwd', scope: 'project' },
171
+ { rel: ['opencode.jsonc'], base: 'cwd', scope: 'project' },
172
+ ],
173
+ },
174
+ zed: {
175
+ tier: 'declared',
176
+ label: 'Zed',
177
+ sources: [
178
+ { rel: ['settings.json'], base: 'zed-config', scope: 'user' },
179
+ { rel: ['.zed', 'settings.json'], base: 'cwd', scope: 'project' },
180
+ ],
181
+ },
182
+ augment: {
183
+ tier: 'declared',
184
+ label: 'Augment',
185
+ sources: [
186
+ { rel: ['.augment', 'settings.json'], base: 'home', scope: 'user' },
187
+ { rel: ['.augment', 'settings.json'], base: 'cwd', scope: 'project' },
188
+ ],
189
+ },
190
+ continue: {
191
+ tier: 'declared',
192
+ label: 'Continue',
193
+ sources: [{ rel: ['.continue', 'permissions.yaml'], base: 'home', scope: 'user' }],
194
+ },
195
+ goose: {
196
+ tier: 'flag',
197
+ label: 'Goose',
198
+ sources: [
199
+ { rel: ['config.yaml'], base: 'goose-config', scope: 'user' },
200
+ { rel: ['permission.yaml'], base: 'goose-config', scope: 'user' },
201
+ ],
202
+ },
203
+ amp: {
204
+ tier: 'flag',
205
+ label: 'Amp',
206
+ sources: [
207
+ { rel: ['settings.json'], base: 'vscode-user', scope: 'user', keys: ['amp.'] },
208
+ { rel: ['.config', 'amp', 'settings.json'], base: 'home', scope: 'user' },
209
+ { rel: ['.vscode', 'settings.json'], base: 'cwd', scope: 'project', keys: ['amp.'] },
210
+ ],
211
+ },
212
+ aider: {
213
+ tier: 'flag',
214
+ label: 'Aider',
215
+ sources: [
216
+ { rel: ['.aider.conf.yml'], base: 'home', scope: 'user' },
217
+ { rel: ['.aider.conf.yml'], base: 'cwd', scope: 'project' },
218
+ ],
101
219
  },
102
-
103
- aider: { tier: 'none', label: 'Aider', sources: [] },
104
220
  };
105
221
 
106
222
  function vscodeUserDir(variant) {
@@ -109,6 +225,12 @@ function vscodeUserDir(variant) {
109
225
  return path.join(HOME, '.config', variant, 'User');
110
226
  }
111
227
 
228
+ const APPDATA = process.env.APPDATA || path.join(HOME, 'AppData', 'Roaming');
229
+
230
+ function systemDir(win, mac, linux) {
231
+ return PLAT === 'win32' ? win : PLAT === 'darwin' ? mac : linux;
232
+ }
233
+
112
234
  function resolveBase(base, cwd) {
113
235
  switch (base) {
114
236
  case 'home': return HOME;
@@ -116,10 +238,39 @@ function resolveBase(base, cwd) {
116
238
  case 'cursor-user': return path.dirname(vscodeUserDir('Cursor'));
117
239
  case 'cline-storage': return path.join(vscodeUserDir('Code'), 'globalStorage', 'saoudrizwan.claude-dev');
118
240
  case 'roo-storage': return path.join(vscodeUserDir('Code'), 'globalStorage', 'rooveterinaryinc.roo-cline');
241
+ case 'vscode-user': return vscodeUserDir('Code');
242
+ case 'vscode-insiders-user': return vscodeUserDir('Code - Insiders');
243
+ case 'windsurf-user': return vscodeUserDir('Windsurf');
244
+ case 'claude-managed': return systemDir('C:\\Program Files\\ClaudeCode', '/Library/Application Support/ClaudeCode', '/etc/claude-code');
245
+ case 'gemini-system': return systemDir('C:\\ProgramData\\gemini-cli', '/Library/Application Support/GeminiCli', '/etc/gemini-cli');
246
+ case 'qwen-system': return systemDir('C:\\ProgramData\\qwen-code', '/Library/Application Support/QwenCode', '/etc/qwen-code');
247
+ case 'zed-config': return PLAT === 'win32' ? path.join(APPDATA, 'Zed') : path.join(HOME, '.config', 'zed');
248
+ case 'goose-config': return PLAT === 'win32' ? path.join(APPDATA, 'Block', 'goose', 'config') : path.join(HOME, '.config', 'goose');
119
249
  default: return cwd;
120
250
  }
121
251
  }
122
252
 
253
+ const MAX_DIR_FILES = 20;
254
+
255
+ function expandSource(src, cwd) {
256
+ const target = path.join(resolveBase(src.base, cwd), ...src.rel);
257
+ if (!src.dir) return [target];
258
+ try {
259
+ return fs.readdirSync(target)
260
+ .filter((n) => n.toLowerCase().endsWith(src.dir))
261
+ .sort()
262
+ .slice(0, MAX_DIR_FILES)
263
+ .map((n) => path.join(target, n));
264
+ } catch {
265
+ return [];
266
+ }
267
+ }
268
+
269
+ function pickKeys(json, prefixes) {
270
+ if (!prefixes || !json || typeof json !== 'object' || Array.isArray(json)) return json;
271
+ return Object.fromEntries(Object.entries(json).filter(([k]) => prefixes.some((p) => k.toLowerCase().startsWith(p.toLowerCase()))));
272
+ }
273
+
123
274
  const AUTO_APPROVE_KEYS = [
124
275
  { re: /^(yolo|yolomode|enableyolo)$/i, label: 'YOLO mode', severity: 'CRITICAL' },
125
276
  { re: /^(autoapprove|alwaysallow|autoallow|autoaccept|autoconfirm)$/i, label: 'auto-approve', severity: 'HIGH' },
@@ -147,53 +298,59 @@ function collectSwitches(node, out, depth = 0, trail = '') {
147
298
  }
148
299
  }
149
300
 
301
+
150
302
  function collectMcpServers(json) {
151
- const servers = json?.mcpServers ?? json?.servers ?? json?.mcp?.servers ?? null;
152
- if (!servers || typeof servers !== 'object' || Array.isArray(servers)) return [];
153
- return Object.entries(servers)
154
- .slice(0, 60)
155
- .map(([name, def]) => ({
156
- name,
157
- transport: def?.url ? 'http' : 'stdio',
158
-
159
- autoApprove: Array.isArray(def?.autoApprove) ? def.autoApprove.slice(0, 20) : Array.isArray(def?.alwaysAllow) ? def.alwaysAllow.slice(0, 20) : [],
160
- disabled: def?.disabled === true,
161
- }));
303
+ const servers = readMcpServers(json) ?? [];
304
+ return servers.slice(0, 60).map((s) => ({
305
+ name: s.name,
306
+ transport: s.url ? 'http' : 'stdio',
307
+ autoApprove: (s.trusted ? ['*', ...(s.autoApprove ?? [])] : s.autoApprove ?? []).slice(0, 20),
308
+ disabled: s.disabled === true,
309
+ }));
162
310
  }
163
311
 
164
- export function readVendorPosture(vendor, cwd = process.cwd()) {
312
+ function readSource(src, cwd) {
313
+ if (src.stateDb) {
314
+ const file = path.join(vscodeUserDir(src.stateDb), 'globalStorage', 'state.vscdb');
315
+ const res = readStateKeys(file, src.dbKeys);
316
+ const docs = res.state === 'read' ? res.values.map((v) => INTERPRETERS[src.interpret](v.json)) : [];
317
+ return [{ path: `${file}#${src.dbKeys.join('|')}`, res, docs }];
318
+ }
319
+ return expandSource(src, cwd).map((file) => {
320
+ const res = readJsonc(file, src.maxBytes);
321
+ const raw = res.state === 'read' ? pickKeys(res.json ?? {}, src.keys) : null;
322
+ const doc = raw && src.interpret ? INTERPRETERS[src.interpret](raw) : raw;
323
+ return { path: file, res, docs: doc ? [doc] : [] };
324
+ });
325
+ }
326
+
327
+ export function readVendorPosture(vendor, cwd = process.cwd(), roots = null) {
165
328
  const def = VENDOR_POSTURE[vendor];
166
329
  if (!def) return null;
167
330
 
168
331
  const sources = [];
169
332
  const switches = [];
170
333
  const mcpServers = [];
171
- let mode = null;
172
- const allow = [];
173
- const deny = [];
174
- const ask = [];
175
- let enableAllProjectMcpServers = null;
334
+ const grant = emptyGrant();
335
+ const extraRoots = (roots ?? []).filter((r) => r && path.resolve(r) !== path.resolve(cwd));
336
+ const projectsRead = new Set();
176
337
 
177
338
  for (const src of def.sources) {
178
- const file = path.join(resolveBase(src.base, cwd), ...src.rel);
179
- const res = readJsonc(file);
180
- sources.push({ path: file, scope: src.scope, state: res.state, reason: res.reason ?? null });
181
- if (res.state !== 'read') continue;
182
-
183
- const json = res.json ?? {};
184
- const perms = json.permissions ?? {};
185
-
186
- const m = perms.defaultMode ?? json.defaultMode ?? null;
187
- if (m && (!mode || String(m).toLowerCase() === 'bypasspermissions')) mode = String(m);
188
- for (const [key, sink] of [['allow', allow], ['deny', deny], ['ask', ask]]) {
189
- const list = Array.isArray(perms[key]) ? perms[key] : key === 'allow' && Array.isArray(json.allowedTools) ? json.allowedTools : [];
190
- for (const g of list) if (typeof g === 'string' && !sink.includes(g)) sink.push(g);
339
+ const bases = src.base === 'cwd' ? [cwd, ...extraRoots] : [cwd];
340
+ for (const base of bases) {
341
+ for (const { path: file, res, docs } of readSource(src, base)) {
342
+ const primary = base === cwd;
343
+ if (primary || res.state !== 'absent') sources.push({ path: file, scope: src.scope, state: res.state, reason: res.reason ?? null });
344
+ if (res.state !== 'read') continue;
345
+ if (src.base === 'cwd') projectsRead.add(base);
346
+ for (const json of docs) {
347
+ if ((src.keys || src.interpret) && !Object.keys(json).length) continue;
348
+ extractGrant(json, grant);
349
+ if (def.tier === 'flag') collectSwitches(json, switches);
350
+ for (const s of collectMcpServers(json)) if (!mcpServers.some((x) => x.name === s.name)) mcpServers.push(s);
351
+ }
352
+ }
191
353
  }
192
- if (json.enableAllProjectMcpServers === true) enableAllProjectMcpServers = true;
193
- else if (enableAllProjectMcpServers === null && json.enableAllProjectMcpServers === false) enableAllProjectMcpServers = false;
194
-
195
- if (def.tier === 'flag') collectSwitches(json, switches);
196
- for (const s of collectMcpServers(json)) if (!mcpServers.some((x) => x.name === s.name)) mcpServers.push(s);
197
354
  }
198
355
 
199
356
  const read = sources.filter((s) => s.state === 'read');
@@ -207,38 +364,60 @@ export function readVendorPosture(vendor, cwd = process.cwd()) {
207
364
  readable: read.length > 0,
208
365
  sources,
209
366
  unreadableCount: holes.length,
210
- mode,
211
- allow: allow.slice(0, 60),
212
- deny: deny.slice(0, 60),
213
- ask: ask.slice(0, 60),
214
- enableAllProjectMcpServers,
367
+ mode: grant.mode,
368
+ allow: grant.allow.slice(0, 60),
369
+ deny: grant.deny.slice(0, 60),
370
+ ask: grant.ask.slice(0, 60),
371
+ enableAllProjectMcpServers: grant.enableAllProjectMcpServers,
372
+ grant,
215
373
  switches: switches.slice(0, 20),
216
374
  mcpServers,
217
375
 
218
376
  autoApprovedMcp: mcpServers.filter((s) => s.autoApprove.length > 0 && !s.disabled).map((s) => s.name),
219
377
 
220
- claim: def.tier === 'declared' && read.length > 0 && holes.length === 0 ? 'complete' : 'floor',
378
+ truncated: grant.truncated === true,
379
+ projectsScanned: 1 + extraRoots.length,
380
+ projectsWithSettings: projectsRead.size,
381
+ claim: def.tier === 'declared' && read.length > 0 && holes.length === 0 && !grant.truncated ? 'complete' : 'floor',
221
382
  };
222
383
  }
223
384
 
224
385
  export function canonicalGrant(posture) {
225
386
  if (!posture) return null;
387
+ const g = posture.grant && !grantIsEmpty(posture.grant) ? posture.grant : null;
226
388
  const doc = {};
227
389
  const perms = {};
228
390
  if (posture.mode) perms.defaultMode = posture.mode;
229
- if (posture.allow.length) perms.allow = posture.allow;
230
- if (posture.deny.length) perms.deny = posture.deny;
231
- if (posture.ask.length) perms.ask = posture.ask;
391
+ if (posture.allow?.length) perms.allow = posture.allow;
392
+ if (posture.deny?.length) perms.deny = posture.deny;
393
+ if (posture.ask?.length) perms.ask = posture.ask;
394
+ if (g) {
395
+ if (g.directories.length) perms.additionalDirectories = g.directories;
396
+ if (g.trustedFolders.length) perms.trustedFolders = g.trustedFolders;
397
+ const fsGrant = Object.fromEntries(Object.entries(g.fileSystem).filter(([, v]) => v.length));
398
+ if (Object.keys(fsGrant).length) perms.fileSystem = fsGrant;
399
+ const net = Object.fromEntries(Object.entries(g.network).filter(([, v]) => v.length));
400
+ if (Object.keys(net).length) perms.network = net;
401
+ if (g.envNames.length) perms.environmentVariables = { allowList: g.envNames };
402
+ if (g.commands.length) perms.tools = { allowExecution: g.commands };
403
+ if (g.rules.length) perms.rules = g.rules;
404
+ if (g.permission) perms.permission = g.permission;
405
+ if (g.toolPermissions && Object.keys(g.toolPermissions).length) perms.tool_permissions = g.toolPermissions;
406
+ if (g.unixSockets.length) perms.sandbox = { network: { allowUnixSockets: g.unixSockets } };
407
+ if (g.mcpAllowlist.length) perms.mcpAllowlist = g.mcpAllowlist;
408
+ if (g.folderTrustDisabled) perms.folderTrust = { enabled: false };
409
+ for (const [k, v] of Object.entries(g.flags)) if (!(k in perms)) perms[k] = v;
410
+ }
232
411
  if (Object.keys(perms).length) doc.permissions = perms;
233
412
  if (posture.enableAllProjectMcpServers === true) doc.enableAllProjectMcpServers = true;
234
413
  return Object.keys(doc).length ? JSON.stringify(doc, null, 2) : null;
235
414
  }
236
415
 
237
- export function readPostures(vendors, cwd = process.cwd()) {
416
+ export function readPostures(vendors, cwd = process.cwd(), roots = projectRoots(cwd)) {
238
417
  const out = {};
239
418
  for (const v of vendors) {
240
419
  try {
241
- const p = readVendorPosture(v, cwd);
420
+ const p = readVendorPosture(v, cwd, roots);
242
421
  if (p) out[v] = p;
243
422
  } catch {
244
423
 
@@ -1,21 +1,30 @@
1
1
  import path from 'node:path';
2
2
  import { extOf } from './limits.mjs';
3
3
  import { SETTINGS_BASENAMES } from './marketplaces.mjs';
4
+ import { isInstructionPath } from '../../detect/signals/instruction-paths.mjs';
5
+ import { PLUGIN_MANIFEST_RE } from './plugins.mjs';
6
+ import { EXTENSION_MANIFEST_RE } from './extensions.mjs';
7
+
8
+ export const COMMAND_PATH_RE = /(^|\/)commands?\/|(^|\/)\.[\w-]+\/prompts?\/|(^|\/)\.(?:windsurf|devin|clinerules)\/workflows\//;
4
9
 
5
10
  export function classify(rel, vendor) {
6
11
  const lower = rel.toLowerCase().replace(/\\/g, '/');
7
12
  const base = path.basename(lower);
8
13
  const ext = extOf(lower);
9
14
 
15
+ if (vendor === 'claude-desktop') return EXTENSION_MANIFEST_RE.test(lower) ? 'extension' : null;
16
+ if (PLUGIN_MANIFEST_RE.test(lower)) return 'plugin';
10
17
  if (base === 'skill.md') return 'skill';
11
18
  if (SETTINGS_BASENAMES.has(base)) return 'hook';
19
+ if (vendor === 'copilot' && ext === 'json' && /(^|\/)hooks\/[^/]+\.json$/.test(lower)) return 'hook';
12
20
  if (/(^|\/)(sub)?agents?\//.test(lower) && ext === 'md') return 'subagent';
13
- if (/(^|\/)(commands?|prompts?|workflows?)\//.test(lower) && (ext === 'md' || ext === 'toml')) {
21
+ if (COMMAND_PATH_RE.test(lower) && !/(^|\/)\.github\/workflows\//.test(lower) && (ext === 'md' || ext === 'toml')) {
14
22
 
15
23
  if (vendor === 'copilot' && !/(^|\/)(prompts|chatmodes)\//.test(lower)) return null;
16
24
  return 'command';
17
25
  }
18
26
  if (vendor === 'copilot' && /(^|\/)chatmodes\//.test(lower) && ext === 'md') return 'subagent';
27
+ if (lower.split('/').length <= 5 && !/(^|\/)(extensions|plugins|marketplaces|node_modules|cache|projects)\//.test(lower) && isInstructionPath(lower)) return 'rules';
19
28
  return null;
20
29
  }
21
30
 
@@ -4,8 +4,12 @@ import { clampArtifact } from '../../core/wire-limits.mjs';
4
4
  import { classify, declaredName } from './classify.mjs';
5
5
  import { readText } from './file-read.mjs';
6
6
  import { canonicalHooks } from './hooks.mjs';
7
- import { MAX_ARTIFACTS, MAX_BUNDLED, MAX_DIRS, MAX_TOTAL_BYTES, TEXT_EXTS, extOf } from './limits.mjs';
7
+ import { bundleHookScripts } from './hook-scripts.mjs';
8
+ import { HOME, MAX_ARTIFACTS, MAX_BUNDLED, MAX_DIRS, MAX_TOTAL_BYTES, TEXT_EXTS, extOf } from './limits.mjs';
9
+ import { extractInstructionImports } from '../../detect/signals/instruction-paths.mjs';
8
10
  import { CATALOGUE_DIR_RE, PLUGIN_PATH_RE, installedMarketplaces } from './marketplaces.mjs';
11
+ import { MARKETPLACE_ROOT_RE, MAX_MANIFEST_BYTES, bundlePluginComponents, installedPluginManifests, manifestName, pluginRootOf } from './plugins.mjs';
12
+ import { bundleExtensionSource } from './extensions.mjs';
9
13
  import { artifactRoots } from './roots.mjs';
10
14
  import { walkRoot } from './walk.mjs';
11
15
 
@@ -16,10 +20,13 @@ function relativeToSite(site, absolutePath) {
16
20
  }
17
21
 
18
22
  function artifactName(kind, content, absolutePath) {
23
+ if (kind === 'plugin') return manifestName(content, path.basename(pluginRootOf(absolutePath)) || path.basename(absolutePath));
24
+ if (kind === 'extension') return manifestName(content, path.basename(path.dirname(absolutePath)));
19
25
  const declared = declaredName(content);
20
26
  if (declared) return declared;
21
27
  if (kind === 'skill') return path.basename(path.dirname(absolutePath));
22
28
  if (kind === 'hook') return `${path.basename(absolutePath)} · hooks`;
29
+ if (kind === 'rules') return path.basename(absolutePath);
23
30
  return path.basename(absolutePath).replace(/\.(md|toml)$/i, '');
24
31
  }
25
32
 
@@ -30,8 +37,10 @@ function resolveActivation(marketplace, installed) {
30
37
  }
31
38
 
32
39
  function readArtifactContent(kind, absolutePath) {
33
- const read = readText(absolutePath);
40
+ const manifest = kind === 'plugin' || kind === 'extension';
41
+ const read = readText(absolutePath, manifest ? MAX_MANIFEST_BYTES : undefined);
34
42
  if (!read) return null;
43
+ if (manifest && read.truncated) return { ...read, content: null, oversize: true };
35
44
  if (kind !== 'hook') return { ...read, content: read.text };
36
45
  const content = canonicalHooks(read.text);
37
46
  return content ? { ...read, content } : null;
@@ -77,7 +86,12 @@ function collectFromSite({ site, files, state, project }) {
77
86
  }
78
87
 
79
88
  const marketplace = PLUGIN_PATH_RE.exec(relativePath)?.[2] ?? null;
80
- const activation = resolveActivation(marketplace, installed);
89
+
90
+ if (kind === 'plugin' && marketplace && !MARKETPLACE_ROOT_RE.test(relativePath)) {
91
+ availableBy.set(marketplace, (availableBy.get(marketplace) ?? 0) + 1);
92
+ continue;
93
+ }
94
+ const activation = kind === 'plugin' && marketplace ? 'active' : resolveActivation(marketplace, installed);
81
95
  if (activation === 'not-installed') {
82
96
  availableBy.set(marketplace, (availableBy.get(marketplace) ?? 0) + 1);
83
97
  continue;
@@ -85,6 +99,10 @@ function collectFromSite({ site, files, state, project }) {
85
99
 
86
100
  const read = readArtifactContent(kind, absolutePath);
87
101
  if (!read) continue;
102
+ if (read.oversize) {
103
+ capped.push({ reason: 'manifest-too-large', path: relativePath });
104
+ continue;
105
+ }
88
106
  if (read.bytes > budget.bytes) {
89
107
  capped.push({ reason: 'byte-budget', path: relativePath });
90
108
  continue;
@@ -97,6 +115,76 @@ function collectFromSite({ site, files, state, project }) {
97
115
  consumed.add(absolutePath);
98
116
  artifacts.push(artifact);
99
117
  if (kind === 'skill') skills.push({ artifact, dir: path.dirname(absolutePath), site });
118
+ if (kind === 'rules') state.rules.push({ artifact, absolutePath });
119
+ if (kind === 'plugin') state.plugins.push({ artifact, absolutePath, site });
120
+ if (kind === 'extension') state.extensions.push({ artifact, absolutePath, site });
121
+ if (kind === 'hook') (state.hooks ??= []).push({ artifact, absolutePath, site });
122
+ }
123
+ }
124
+
125
+ function collectInstalledPlugins({ sites, state, project }) {
126
+ const { artifacts, capped, consumed, budget } = state;
127
+ for (const site of sites) {
128
+ if (site.vendor !== 'claude-code' || site.scope !== 'user') continue;
129
+ for (const { manifest, marketplace } of installedPluginManifests(site.dir)) {
130
+ if (consumed.has(manifest)) continue;
131
+ if (artifacts.length >= MAX_ARTIFACTS) { capped.push({ reason: 'artifact-cap', path: manifest }); return; }
132
+ const relativePath = relativeToSite(site, manifest);
133
+ const read = readArtifactContent('plugin', manifest);
134
+ if (!read) continue;
135
+ if (read.oversize) { capped.push({ reason: 'manifest-too-large', path: relativePath }); continue; }
136
+ if (read.bytes > budget.bytes) { capped.push({ reason: 'byte-budget', path: relativePath }); continue; }
137
+ budget.bytes -= Buffer.byteLength(read.content);
138
+ const artifact = buildArtifact({
139
+ kind: 'plugin', content: read.content, read, relativePath, site, project, marketplace, activation: 'active', absolutePath: manifest,
140
+ });
141
+ consumed.add(manifest);
142
+ artifacts.push(artifact);
143
+ state.plugins.push({ artifact, absolutePath: manifest, site });
144
+ }
145
+ }
146
+ }
147
+
148
+ const IMPORT_READABLE_RE = /\.(?:md|mdx|markdown|mdc|rst|txt)$/i;
149
+ const IMPORT_SECRET_RE = /(?:^|[\\/])(?:\.env|\.ssh|\.aws|\.gnupg|\.netrc|\.npmrc)|id_(?:rsa|dsa|ecdsa|ed25519)|secret|credential|password|\.pem$|\.key$/i;
150
+ const MAX_IMPORT_DEPTH = 5;
151
+
152
+ function resolveImport(ref, fromFile) {
153
+ if (ref.startsWith('~/')) return path.join(HOME, ref.slice(2));
154
+ if (path.isAbsolute(ref)) return ref;
155
+ return path.resolve(path.dirname(fromFile), ref);
156
+ }
157
+
158
+ function bundleRuleImports(state) {
159
+ const { capped, budget } = state;
160
+ for (const { artifact, absolutePath } of state.rules) {
161
+ const seen = new Set([absolutePath]);
162
+ const queue = [{ file: absolutePath, text: artifact.content, depth: 0 }];
163
+ while (queue.length) {
164
+ const { file, text, depth } = queue.shift();
165
+ if (depth >= MAX_IMPORT_DEPTH) continue;
166
+ for (const ref of extractInstructionImports(text)) {
167
+ if (!IMPORT_READABLE_RE.test(ref) || IMPORT_SECRET_RE.test(ref)) continue;
168
+ const target = resolveImport(ref, file);
169
+ if (seen.has(target)) continue;
170
+ seen.add(target);
171
+ if (artifact.files.length >= MAX_BUNDLED) {
172
+ capped.push({ reason: 'bundle-cap', path: artifact.path });
173
+ queue.length = 0;
174
+ break;
175
+ }
176
+ const read = readText(target);
177
+ if (!read) continue;
178
+ if (read.bytes > budget.bytes) {
179
+ capped.push({ reason: 'byte-budget', path: ref });
180
+ continue;
181
+ }
182
+ budget.bytes -= Buffer.byteLength(read.text);
183
+ artifact.files.push({ path: ref.replace(/^[~.]*\//, ''), content: read.text, binary: false });
184
+ queue.push({ file: target, text: read.text, depth: depth + 1 });
185
+ }
186
+ }
187
+ if (artifact.files.length) artifact.metadata.importCount = artifact.files.length;
100
188
  }
101
189
  }
102
190
 
@@ -164,12 +252,34 @@ export function discoverAgentArtifacts(cwd = process.cwd(), roots = null) {
164
252
  capped: [],
165
253
  consumed: new Set(),
166
254
  skills: [],
255
+ rules: [],
256
+ plugins: [],
257
+ extensions: [],
167
258
  availableBy: new Map(),
168
259
  };
169
260
 
170
261
  const walked = walkSites(sites, state.budget);
171
262
  for (const { site, files } of walked) collectFromSite({ site, files, state, project });
263
+ collectInstalledPlugins({ sites, state, project });
172
264
  bundleSkillFiles({ skills: state.skills, walked, state });
265
+ bundleRuleImports(state);
266
+ for (const { artifact, absolutePath, site } of state.hooks ?? []) {
267
+ bundleHookScripts(artifact, absolutePath, {
268
+ projectDir: site.scope === 'project' ? path.dirname(site.dir) : cwd,
269
+ relPath: (abs) => {
270
+ const rel = relativeToSite(site, abs);
271
+ return rel.startsWith('..') || path.isAbsolute(rel) ? null : rel;
272
+ },
273
+ budget: state.budget,
274
+ capped: state.capped,
275
+ });
276
+ }
277
+ for (const { artifact, absolutePath, site } of state.extensions) {
278
+ bundleExtensionSource(artifact, absolutePath, (abs) => relativeToSite(site, abs), state.budget, state.capped);
279
+ }
280
+ for (const { artifact, absolutePath, site } of state.plugins) {
281
+ bundlePluginComponents(artifact, absolutePath, (abs) => relativeToSite(site, abs), state.budget, state.capped);
282
+ }
173
283
 
174
284
  if (state.budget.dirs <= 0) state.capped.push({ reason: 'walk-budget', path: null });
175
285