sentinelayer-cli 0.4.4 → 0.6.2

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 (149) hide show
  1. package/README.md +996 -998
  2. package/bin/create-sentinelayer.js +5 -5
  3. package/bin/sentinelayer-cli.js +4 -4
  4. package/bin/sl.js +5 -5
  5. package/package.json +63 -63
  6. package/src/agents/jules/config/definition.js +160 -209
  7. package/src/agents/jules/config/system-prompt.js +182 -175
  8. package/src/agents/jules/error-intake.js +51 -51
  9. package/src/agents/jules/fix-cycle.js +17 -377
  10. package/src/agents/jules/loop.js +450 -367
  11. package/src/agents/jules/pulse.js +10 -327
  12. package/src/agents/jules/stream.js +186 -186
  13. package/src/agents/jules/swarm/file-scanner.js +74 -74
  14. package/src/agents/jules/swarm/index.js +11 -11
  15. package/src/agents/jules/swarm/orchestrator.js +362 -362
  16. package/src/agents/jules/swarm/pattern-hunter.js +123 -123
  17. package/src/agents/jules/swarm/sub-agent.js +309 -308
  18. package/src/agents/jules/tools/aidenid-email.js +189 -0
  19. package/src/agents/jules/tools/auth-audit.js +1691 -557
  20. package/src/agents/jules/tools/dispatch.js +335 -327
  21. package/src/agents/jules/tools/file-edit.js +2 -180
  22. package/src/agents/jules/tools/file-read.js +2 -100
  23. package/src/agents/jules/tools/frontend-analyze.js +570 -570
  24. package/src/agents/jules/tools/glob.js +2 -168
  25. package/src/agents/jules/tools/grep.js +2 -228
  26. package/src/agents/jules/tools/index.js +29 -29
  27. package/src/agents/jules/tools/path-guards.js +2 -161
  28. package/src/agents/jules/tools/runtime-audit.js +507 -503
  29. package/src/agents/jules/tools/shell.js +2 -383
  30. package/src/agents/jules/tools/url-policy.js +100 -100
  31. package/src/agents/persona-visuals.js +61 -0
  32. package/src/agents/shared-tools/dispatch-core.js +315 -0
  33. package/src/agents/shared-tools/file-edit.js +180 -0
  34. package/src/agents/shared-tools/file-read.js +100 -0
  35. package/src/agents/shared-tools/glob.js +168 -0
  36. package/src/agents/shared-tools/grep.js +228 -0
  37. package/src/agents/shared-tools/index.js +46 -0
  38. package/src/agents/shared-tools/path-guards.js +161 -0
  39. package/src/agents/shared-tools/shell.js +383 -0
  40. package/src/ai/aidenid.js +1009 -972
  41. package/src/ai/client.js +553 -508
  42. package/src/ai/domain-target-store.js +268 -268
  43. package/src/ai/identity-store.js +270 -270
  44. package/src/ai/proxy.js +137 -0
  45. package/src/ai/site-store.js +145 -145
  46. package/src/audit/agents/architecture.js +180 -180
  47. package/src/audit/agents/compliance.js +179 -179
  48. package/src/audit/agents/documentation.js +165 -165
  49. package/src/audit/agents/performance.js +145 -145
  50. package/src/audit/agents/security.js +215 -215
  51. package/src/audit/agents/testing.js +172 -172
  52. package/src/audit/orchestrator.js +557 -557
  53. package/src/audit/package.js +204 -204
  54. package/src/audit/registry.js +284 -284
  55. package/src/audit/replay.js +103 -103
  56. package/src/auth/gate.js +371 -126
  57. package/src/auth/http.js +611 -270
  58. package/src/auth/service.js +1106 -891
  59. package/src/auth/session-store.js +813 -359
  60. package/src/cli.js +252 -252
  61. package/src/commands/ai/identity-lifecycle.js +1338 -1338
  62. package/src/commands/ai/provision-governance.js +1272 -1272
  63. package/src/commands/ai/shared.js +147 -147
  64. package/src/commands/ai.js +11 -11
  65. package/src/commands/apply.js +12 -12
  66. package/src/commands/audit.js +1166 -1166
  67. package/src/commands/auth.js +419 -375
  68. package/src/commands/chat.js +191 -191
  69. package/src/commands/config.js +184 -184
  70. package/src/commands/cost.js +311 -311
  71. package/src/commands/daemon/core.js +850 -850
  72. package/src/commands/daemon/extended.js +1048 -1048
  73. package/src/commands/daemon/shared.js +213 -213
  74. package/src/commands/daemon.js +11 -11
  75. package/src/commands/guide.js +174 -174
  76. package/src/commands/ingest.js +58 -58
  77. package/src/commands/init.js +55 -55
  78. package/src/commands/legacy-args.js +10 -10
  79. package/src/commands/mcp.js +461 -461
  80. package/src/commands/omargate.js +29 -21
  81. package/src/commands/persona.js +20 -20
  82. package/src/commands/plugin.js +260 -260
  83. package/src/commands/policy.js +132 -132
  84. package/src/commands/prompt.js +238 -238
  85. package/src/commands/review.js +704 -704
  86. package/src/commands/scan.js +872 -866
  87. package/src/commands/spec.js +716 -716
  88. package/src/commands/swarm.js +651 -651
  89. package/src/commands/telemetry.js +202 -202
  90. package/src/commands/watch.js +511 -510
  91. package/src/config/agent-dictionary.js +182 -182
  92. package/src/config/io.js +56 -56
  93. package/src/config/paths.js +18 -18
  94. package/src/config/schema.js +55 -55
  95. package/src/config/service.js +184 -184
  96. package/src/cost/budget.js +235 -235
  97. package/src/cost/history.js +188 -188
  98. package/src/cost/tracker.js +171 -171
  99. package/src/daemon/artifact-lineage.js +534 -534
  100. package/src/daemon/assignment-ledger.js +770 -770
  101. package/src/daemon/ast-parser-layer.js +258 -258
  102. package/src/daemon/budget-governor.js +633 -633
  103. package/src/daemon/callgraph-overlay.js +646 -646
  104. package/src/daemon/error-worker.js +626 -626
  105. package/src/daemon/fix-cycle.js +377 -0
  106. package/src/daemon/hybrid-mapper.js +929 -929
  107. package/src/daemon/jira-lifecycle.js +632 -632
  108. package/src/daemon/operator-control.js +657 -657
  109. package/src/daemon/pulse.js +327 -0
  110. package/src/daemon/reliability-lane.js +471 -471
  111. package/src/daemon/watchdog.js +971 -971
  112. package/src/guide/generator.js +316 -316
  113. package/src/ingest/engine.js +918 -918
  114. package/src/interactive/index.js +97 -95
  115. package/src/legacy-cli.js +2994 -2592
  116. package/src/mcp/registry.js +695 -695
  117. package/src/memory/blackboard.js +301 -301
  118. package/src/memory/retrieval.js +581 -581
  119. package/src/plugin/manifest.js +553 -553
  120. package/src/policy/packs.js +144 -144
  121. package/src/prompt/generator.js +118 -118
  122. package/src/review/ai-review.js +679 -669
  123. package/src/review/local-review.js +1305 -1295
  124. package/src/review/omargate-interactive.js +68 -0
  125. package/src/review/omargate-orchestrator.js +300 -0
  126. package/src/review/persona-prompts.js +296 -0
  127. package/src/review/replay.js +235 -235
  128. package/src/review/report.js +664 -664
  129. package/src/review/scan-modes.js +42 -0
  130. package/src/review/spec-binding.js +487 -487
  131. package/src/scaffold/generator.js +67 -67
  132. package/src/scaffold/templates.js +150 -150
  133. package/src/scan/generator.js +418 -418
  134. package/src/scan/gh-secrets.js +107 -107
  135. package/src/spec/generator.js +519 -519
  136. package/src/spec/regenerate.js +237 -237
  137. package/src/spec/templates.js +91 -91
  138. package/src/swarm/dashboard.js +247 -247
  139. package/src/swarm/factory.js +363 -363
  140. package/src/swarm/pentest.js +934 -934
  141. package/src/swarm/registry.js +419 -419
  142. package/src/swarm/report.js +158 -158
  143. package/src/swarm/runtime.js +576 -576
  144. package/src/swarm/scenario-dsl.js +272 -272
  145. package/src/telemetry/ledger.js +302 -302
  146. package/src/telemetry/session-tracker.js +234 -118
  147. package/src/telemetry/sync.js +203 -199
  148. package/src/ui/command-hints.js +13 -0
  149. package/src/ui/markdown.js +220 -220
@@ -0,0 +1,168 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ const MAX_RESULTS = 200;
5
+ const IGNORE_DIRS = new Set([
6
+ ".git", "node_modules", ".next", "dist", "build", "coverage",
7
+ ".turbo", ".idea", ".vscode", "__pycache__", ".venv", ".cache",
8
+ ".parcel-cache", ".svelte-kit", ".nuxt", ".output", ".vercel",
9
+ ]);
10
+
11
+ /**
12
+ * Fast file pattern matching sorted by modification time (newest first).
13
+ *
14
+ * @param {object} input
15
+ * @param {string} input.pattern - Glob pattern (e.g., "**\/*.tsx", "src/**\/*.js").
16
+ * @param {string} [input.path] - Directory to search (default: cwd).
17
+ * @param {number} [input.limit] - Max results (default: 200).
18
+ * @returns {{ filenames, numFiles, truncated, durationMs }}
19
+ */
20
+ export function glob(input) {
21
+ if (!input.pattern || typeof input.pattern !== "string") {
22
+ throw new GlobError("pattern is required and must be a non-empty string.");
23
+ }
24
+
25
+ const searchPath = input.path ? path.resolve(input.path) : process.cwd();
26
+ const limit = input.limit ?? MAX_RESULTS;
27
+ const startMs = Date.now();
28
+
29
+ if (!fs.existsSync(searchPath)) {
30
+ throw new GlobError(`Directory not found: ${searchPath}`);
31
+ }
32
+
33
+ const stat = fs.statSync(searchPath);
34
+ if (!stat.isDirectory()) {
35
+ throw new GlobError(`Path is not a directory: ${searchPath}`);
36
+ }
37
+
38
+ const matcher = buildMatcher(input.pattern);
39
+ const ignorePatterns = loadIgnorePatterns(searchPath);
40
+ const results = [];
41
+
42
+ walk(searchPath, searchPath, matcher, ignorePatterns, results, limit);
43
+
44
+ // Sort by mtime descending (newest first)
45
+ results.sort((a, b) => b.mtime - a.mtime);
46
+
47
+ const truncated = results.length >= limit;
48
+ const filenames = results.map((r) => r.relativePath);
49
+
50
+ return {
51
+ filenames,
52
+ numFiles: filenames.length,
53
+ truncated,
54
+ durationMs: Date.now() - startMs,
55
+ };
56
+ }
57
+
58
+ function walk(rootPath, currentPath, matcher, ignorePatterns, results, limit) {
59
+ let entries;
60
+ try {
61
+ entries = fs.readdirSync(currentPath, { withFileTypes: true });
62
+ } catch {
63
+ return; // skip unreadable directories
64
+ }
65
+
66
+ for (const entry of entries) {
67
+ if (results.length >= limit) return;
68
+
69
+ const name = entry.name;
70
+ if (IGNORE_DIRS.has(name)) continue;
71
+
72
+ const fullPath = path.join(currentPath, name);
73
+ const relativePath = path.relative(rootPath, fullPath);
74
+
75
+ if (ignorePatterns.some((p) => p(relativePath))) continue;
76
+
77
+ if (entry.isDirectory()) {
78
+ walk(rootPath, fullPath, matcher, ignorePatterns, results, limit);
79
+ } else if (entry.isFile() && matcher(relativePath)) {
80
+ let mtime = 0;
81
+ try {
82
+ mtime = fs.statSync(fullPath).mtimeMs;
83
+ } catch { /* use 0 if stat fails */ }
84
+ results.push({ relativePath, mtime });
85
+ }
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Build a filename matcher from a glob pattern.
91
+ * Supports: *.ext, **\/*.ext, *.{ext1,ext2}, prefix*, *suffix
92
+ */
93
+ function buildMatcher(pattern) {
94
+ // Handle brace expansion: *.{ts,tsx} → ["*.ts", "*.tsx"]
95
+ const expanded = expandBraces(pattern);
96
+
97
+ const matchers = expanded.map((p) => {
98
+ // ** recursive match
99
+ if (p.includes("**/")) {
100
+ const suffix = p.split("**/").pop();
101
+ const suffixMatcher = buildSimpleMatcher(suffix);
102
+ return (filepath) => {
103
+ const basename = path.basename(filepath);
104
+ const segments = filepath.split(path.sep);
105
+ return segments.some((_, i) =>
106
+ suffixMatcher(segments.slice(i).join(path.sep)),
107
+ ) || suffixMatcher(basename);
108
+ };
109
+ }
110
+ return buildSimpleMatcher(p);
111
+ });
112
+
113
+ return (filepath) => matchers.some((m) => m(filepath));
114
+ }
115
+
116
+ function buildSimpleMatcher(pattern) {
117
+ if (pattern.startsWith("*.")) {
118
+ const ext = pattern.slice(1);
119
+ return (filepath) => filepath.endsWith(ext) || path.basename(filepath).endsWith(ext);
120
+ }
121
+ if (pattern.endsWith("*")) {
122
+ const prefix = pattern.slice(0, -1);
123
+ return (filepath) => filepath.startsWith(prefix) || path.basename(filepath).startsWith(prefix);
124
+ }
125
+ if (pattern.includes("*")) {
126
+ const regex = new RegExp(
127
+ "^" + pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + "$",
128
+ );
129
+ return (filepath) => regex.test(filepath) || regex.test(path.basename(filepath));
130
+ }
131
+ return (filepath) => filepath === pattern || path.basename(filepath) === pattern;
132
+ }
133
+
134
+ function expandBraces(pattern) {
135
+ const braceMatch = pattern.match(/\{([^}]+)\}/);
136
+ if (!braceMatch) return [pattern];
137
+ const alternatives = braceMatch[1].split(",");
138
+ return alternatives.map((alt) =>
139
+ pattern.replace(braceMatch[0], alt.trim()),
140
+ );
141
+ }
142
+
143
+ function loadIgnorePatterns(rootPath) {
144
+ const patterns = [];
145
+ const gitignorePath = path.join(rootPath, ".gitignore");
146
+ const slignorePath = path.join(rootPath, ".sentinelayerignore");
147
+
148
+ for (const ignorePath of [gitignorePath, slignorePath]) {
149
+ try {
150
+ const content = fs.readFileSync(ignorePath, "utf-8");
151
+ for (const line of content.split("\n")) {
152
+ const trimmed = line.trim();
153
+ if (!trimmed || trimmed.startsWith("#")) continue;
154
+ const matcher = buildSimpleMatcher(trimmed);
155
+ patterns.push(matcher);
156
+ }
157
+ } catch { /* ignore missing files */ }
158
+ }
159
+
160
+ return patterns;
161
+ }
162
+
163
+ export class GlobError extends Error {
164
+ constructor(message) {
165
+ super(message);
166
+ this.name = "GlobError";
167
+ }
168
+ }
@@ -0,0 +1,228 @@
1
+ import fs from "node:fs";
2
+ import { execFileSync } from "node:child_process";
3
+ import path from "node:path";
4
+
5
+ const DEFAULT_HEAD_LIMIT = 250;
6
+ const MAX_LINE_LENGTH = 500;
7
+ const VCS_EXCLUDE_DIRS = [
8
+ ".git", ".svn", ".hg", "node_modules", ".next", "dist", "build",
9
+ "coverage", ".turbo", ".idea", ".vscode", "__pycache__", ".venv",
10
+ ];
11
+
12
+ /**
13
+ * Search file contents using ripgrep.
14
+ * Falls back to a naive line-by-line search if rg is not installed.
15
+ *
16
+ * @param {object} input
17
+ * @param {string} input.pattern - Regex pattern to search for.
18
+ * @param {string} [input.path] - Directory to search (default: cwd).
19
+ * @param {string} [input.glob] - Glob filter (e.g., "*.tsx").
20
+ * @param {string} [input.output_mode] - "content" | "files_with_matches" | "count"
21
+ * @param {number} [input.context] - Lines of context before and after match.
22
+ * @param {boolean} [input.case_insensitive] - Case-insensitive search.
23
+ * @param {number} [input.head_limit] - Max results (default 250).
24
+ * @param {boolean} [input.multiline] - Enable multiline matching.
25
+ * @returns {{ mode, numFiles, filenames, content, numMatches, appliedLimit }}
26
+ */
27
+ export function grep(input) {
28
+ if (!input.pattern || typeof input.pattern !== "string") {
29
+ throw new GrepError("pattern is required and must be a non-empty string.");
30
+ }
31
+
32
+ const searchPath = input.path ? path.resolve(input.path) : process.cwd();
33
+ const outputMode = input.output_mode || "files_with_matches";
34
+ const headLimit = input.head_limit ?? DEFAULT_HEAD_LIMIT;
35
+
36
+ const args = buildRgArgs(input, searchPath, outputMode);
37
+
38
+ let stdout;
39
+ try {
40
+ stdout = execFileSync("rg", args, {
41
+ cwd: searchPath,
42
+ encoding: "utf-8",
43
+ maxBuffer: 10 * 1024 * 1024,
44
+ timeout: 30_000,
45
+ });
46
+ } catch (err) {
47
+ // rg exits with code 1 when no matches found — that's normal
48
+ if (err.status === 1) {
49
+ return {
50
+ mode: outputMode,
51
+ numFiles: 0,
52
+ filenames: [],
53
+ content: "",
54
+ numMatches: 0,
55
+ appliedLimit: headLimit,
56
+ };
57
+ }
58
+ // rg not installed — fall back to naive search
59
+ if (err.code === "ENOENT") {
60
+ return naiveFallbackGrep(input, searchPath, outputMode, headLimit);
61
+ }
62
+ throw new GrepError(`ripgrep failed: ${err.message}`);
63
+ }
64
+
65
+ return parseRgOutput(stdout, outputMode, headLimit);
66
+ }
67
+
68
+ function buildRgArgs(input, searchPath, outputMode) {
69
+ const args = ["--no-heading", "--color", "never"];
70
+
71
+ // Output mode flags
72
+ if (outputMode === "files_with_matches") {
73
+ args.push("-l");
74
+ } else if (outputMode === "count") {
75
+ args.push("-c");
76
+ } else {
77
+ args.push("-n"); // line numbers for content mode
78
+ }
79
+
80
+ // Context
81
+ if (input.context && outputMode === "content") {
82
+ args.push("-C", String(input.context));
83
+ }
84
+
85
+ // Case insensitive
86
+ if (input.case_insensitive) {
87
+ args.push("-i");
88
+ }
89
+
90
+ // Multiline
91
+ if (input.multiline) {
92
+ args.push("-U", "--multiline-dotall");
93
+ }
94
+
95
+ // Glob filter
96
+ if (input.glob) {
97
+ args.push("--glob", input.glob);
98
+ }
99
+
100
+ // Exclude VCS and build directories
101
+ for (const dir of VCS_EXCLUDE_DIRS) {
102
+ args.push("--glob", `!${dir}`);
103
+ }
104
+
105
+ // Max line length to prevent base64/minified content noise
106
+ args.push("--max-columns", String(MAX_LINE_LENGTH));
107
+ args.push("--max-columns-preview");
108
+
109
+ args.push("--", input.pattern, searchPath);
110
+ return args;
111
+ }
112
+
113
+ function parseRgOutput(stdout, outputMode, headLimit) {
114
+ const lines = stdout.split("\n").filter(Boolean);
115
+ const limited = headLimit > 0 ? lines.slice(0, headLimit) : lines;
116
+
117
+ if (outputMode === "files_with_matches") {
118
+ return {
119
+ mode: outputMode,
120
+ numFiles: limited.length,
121
+ filenames: limited,
122
+ content: "",
123
+ numMatches: limited.length,
124
+ appliedLimit: headLimit,
125
+ };
126
+ }
127
+
128
+ if (outputMode === "count") {
129
+ let totalMatches = 0;
130
+ const filenames = [];
131
+ for (const line of limited) {
132
+ const colonIdx = line.lastIndexOf(":");
133
+ if (colonIdx > 0) {
134
+ filenames.push(line.slice(0, colonIdx));
135
+ totalMatches += parseInt(line.slice(colonIdx + 1), 10) || 0;
136
+ }
137
+ }
138
+ return {
139
+ mode: outputMode,
140
+ numFiles: filenames.length,
141
+ filenames,
142
+ content: limited.join("\n"),
143
+ numMatches: totalMatches,
144
+ appliedLimit: headLimit,
145
+ };
146
+ }
147
+
148
+ // Content mode
149
+ const fileSet = new Set();
150
+ for (const line of limited) {
151
+ const colonIdx = line.indexOf(":");
152
+ if (colonIdx > 0) {
153
+ fileSet.add(line.slice(0, colonIdx));
154
+ }
155
+ }
156
+ return {
157
+ mode: outputMode,
158
+ numFiles: fileSet.size,
159
+ filenames: [...fileSet],
160
+ content: limited.join("\n"),
161
+ numMatches: limited.length,
162
+ appliedLimit: headLimit,
163
+ };
164
+ }
165
+
166
+ /**
167
+ * Naive line-by-line fallback when ripgrep is not installed.
168
+ * Significantly slower but functional.
169
+ */
170
+ function naiveFallbackGrep(input, searchPath, outputMode, headLimit) {
171
+ const { readdirSync, readFileSync, statSync } = fs;
172
+ const regex = new RegExp(input.pattern, input.case_insensitive ? "gi" : "g");
173
+ const globPattern = input.glob;
174
+ const results = [];
175
+ const filenames = new Set();
176
+
177
+ function walk(dir) {
178
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
179
+ if (VCS_EXCLUDE_DIRS.includes(entry.name)) continue;
180
+ const fullPath = path.join(dir, entry.name);
181
+ if (entry.isDirectory()) {
182
+ walk(fullPath);
183
+ } else if (entry.isFile()) {
184
+ if (globPattern && !matchGlob(entry.name, globPattern)) continue;
185
+ try {
186
+ const content = readFileSync(fullPath, "utf-8");
187
+ const lines = content.split("\n");
188
+ for (let i = 0; i < lines.length; i++) {
189
+ if (regex.test(lines[i])) {
190
+ filenames.add(fullPath);
191
+ results.push(`${fullPath}:${i + 1}:${lines[i].slice(0, MAX_LINE_LENGTH)}`);
192
+ if (headLimit > 0 && results.length >= headLimit) return;
193
+ }
194
+ regex.lastIndex = 0;
195
+ }
196
+ } catch { /* skip unreadable files */ }
197
+ }
198
+ }
199
+ }
200
+
201
+ walk(searchPath);
202
+
203
+ return {
204
+ mode: outputMode,
205
+ numFiles: filenames.size,
206
+ filenames: [...filenames],
207
+ content: outputMode === "content" ? results.join("\n") : "",
208
+ numMatches: results.length,
209
+ appliedLimit: headLimit,
210
+ fallback: true,
211
+ };
212
+ }
213
+
214
+ function matchGlob(filename, glob) {
215
+ // Simple extension glob matching (e.g., "*.tsx", "*.{ts,tsx}")
216
+ if (glob.startsWith("*.")) {
217
+ const exts = glob.slice(1).replace(/[{}]/g, "").split(",");
218
+ return exts.some((ext) => filename.endsWith(ext));
219
+ }
220
+ return true;
221
+ }
222
+
223
+ export class GrepError extends Error {
224
+ constructor(message) {
225
+ super(message);
226
+ this.name = "GrepError";
227
+ }
228
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Shared Tools — Common tool implementations for all personas.
3
+ *
4
+ * Generic file/search/edit tools live here. Domain-specific tools
5
+ * (FrontendAnalyze, BackendAnalyze, DataAnalyze) stay in persona folders.
6
+ * Each persona's dispatch.js imports SHARED_TOOLS and merges with its own.
7
+ */
8
+
9
+ import { fileRead } from "./file-read.js";
10
+ import { grep } from "./grep.js";
11
+ import { glob } from "./glob.js";
12
+ import { shell } from "./shell.js";
13
+ import { fileEdit } from "./file-edit.js";
14
+
15
+ /**
16
+ * Tool map for generic tools shared across all personas.
17
+ * Merge with persona-specific tools in each persona's dispatch.js.
18
+ */
19
+ export const SHARED_TOOLS = {
20
+ FileRead: fileRead,
21
+ Grep: grep,
22
+ Glob: glob,
23
+ Shell: shell,
24
+ FileEdit: fileEdit,
25
+ };
26
+
27
+ /**
28
+ * Read-only tool names (safe for concurrent execution in swarm sub-agents).
29
+ */
30
+ export const SHARED_READ_ONLY_TOOLS = new Set(["FileRead", "Grep", "Glob"]);
31
+
32
+ // Re-export individual tools for direct import
33
+ export { fileRead, FileReadError } from "./file-read.js";
34
+ export { grep, GrepError } from "./grep.js";
35
+ export { glob, GlobError } from "./glob.js";
36
+ export { shell, analyzeCommand, buildScrubbedEnv, ShellError, ShellBlockedError } from "./shell.js";
37
+ export { fileEdit, FileEditError } from "./file-edit.js";
38
+ export { PathGuardError, resolveGuardedPath } from "./path-guards.js";
39
+
40
+ // Re-export dispatch infrastructure
41
+ export {
42
+ createToolDispatcher,
43
+ createAgentContext,
44
+ ToolDispatchError,
45
+ BudgetExhaustedError,
46
+ } from "./dispatch-core.js";
@@ -0,0 +1,161 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ const POSIX_BLOCKED_PREFIXES = ["/dev", "/proc", "/sys"];
5
+ const WINDOWS_DEVICE_SEGMENT = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\..*)?$/i;
6
+ const WINDOWS_DEVICE_NAMESPACE_PATTERN = /^\\\\[?.]\\.+/;
7
+ const WINDOWS_UNC_PATTERN = /^\\\\(?![?.]\\)/;
8
+ const POSIX_UNC_PATTERN = /^\/\/[^/]/;
9
+
10
+ /**
11
+ * Resolve a user-provided file path and enforce sandbox-style guardrails.
12
+ * Returns the resolved path and realpath so callers can safely read/write.
13
+ */
14
+ export function resolveGuardedPath({ filePath, allowedRoot }) {
15
+ const rawFilePath = normalizeInputPath(filePath);
16
+ assertPathNotNetwork(rawFilePath);
17
+ assertPathNotDeviceNamespace(rawFilePath);
18
+
19
+ const resolvedPath = path.resolve(rawFilePath);
20
+ const realPath = resolveRealPathOrFallback(resolvedPath);
21
+
22
+ assertPathNotNetwork(resolvedPath);
23
+ assertPathNotNetwork(realPath);
24
+ assertPathNotDeviceNamespace(resolvedPath);
25
+ assertPathNotDeviceNamespace(realPath);
26
+ assertPathNotBlockedPosixSystemPath(resolvedPath);
27
+ assertPathNotBlockedPosixSystemPath(realPath);
28
+ assertPathNotWindowsDeviceSegment(resolvedPath);
29
+ assertPathNotWindowsDeviceSegment(realPath);
30
+
31
+ if (allowedRoot !== undefined && allowedRoot !== null && String(allowedRoot).trim()) {
32
+ const resolvedAllowedRoot = path.resolve(String(allowedRoot));
33
+ const allowedRootRealPath = resolveRealPathOrFallback(resolvedAllowedRoot);
34
+ assertPathWithinAllowedRoot(resolvedPath, resolvedAllowedRoot);
35
+ assertPathWithinAllowedRoot(realPath, allowedRootRealPath);
36
+ }
37
+
38
+ return {
39
+ resolvedPath,
40
+ realPath,
41
+ };
42
+ }
43
+
44
+ function normalizeInputPath(filePath) {
45
+ if (!filePath || typeof filePath !== "string") {
46
+ throw new PathGuardError(
47
+ "PATH_INVALID",
48
+ "file_path is required and must be a non-empty string.",
49
+ );
50
+ }
51
+
52
+ const trimmed = filePath.trim();
53
+ if (!trimmed) {
54
+ throw new PathGuardError(
55
+ "PATH_INVALID",
56
+ "file_path is required and must be a non-empty string.",
57
+ );
58
+ }
59
+ return trimmed;
60
+ }
61
+
62
+ function resolveRealPathOrFallback(candidatePath) {
63
+ try {
64
+ if (typeof fs.realpathSync.native === "function") {
65
+ return fs.realpathSync.native(candidatePath);
66
+ }
67
+ return fs.realpathSync(candidatePath);
68
+ } catch {
69
+ return candidatePath;
70
+ }
71
+ }
72
+
73
+ function assertPathNotNetwork(candidatePath) {
74
+ const normalized = String(candidatePath || "");
75
+ if (WINDOWS_UNC_PATTERN.test(normalized) || POSIX_UNC_PATTERN.test(normalized)) {
76
+ throw new PathGuardError(
77
+ "PATH_UNC_BLOCKED",
78
+ `Network paths are not allowed: ${candidatePath}`,
79
+ );
80
+ }
81
+ }
82
+
83
+ function assertPathNotDeviceNamespace(candidatePath) {
84
+ const normalized = String(candidatePath || "");
85
+ if (WINDOWS_DEVICE_NAMESPACE_PATTERN.test(normalized)) {
86
+ throw new PathGuardError(
87
+ "PATH_DEVICE_NAMESPACE_BLOCKED",
88
+ `Device namespace paths are not allowed: ${candidatePath}`,
89
+ );
90
+ }
91
+ }
92
+
93
+ function assertPathNotBlockedPosixSystemPath(candidatePath) {
94
+ const normalized = String(candidatePath || "").replace(/\\/g, "/");
95
+ for (const prefix of POSIX_BLOCKED_PREFIXES) {
96
+ if (normalized === prefix || normalized.startsWith(`${prefix}/`)) {
97
+ throw new PathGuardError(
98
+ "PATH_SYSTEM_BLOCKED",
99
+ `Blocked system path: ${candidatePath}`,
100
+ );
101
+ }
102
+ }
103
+ }
104
+
105
+ function assertPathNotWindowsDeviceSegment(candidatePath) {
106
+ if (process.platform !== "win32") {
107
+ return;
108
+ }
109
+
110
+ const normalized = String(candidatePath || "").replace(/\//g, "\\");
111
+ const segments = normalized.split("\\").filter(Boolean);
112
+ for (const segment of segments) {
113
+ if (/^[a-z]:$/i.test(segment)) {
114
+ continue;
115
+ }
116
+ if (WINDOWS_DEVICE_SEGMENT.test(segment)) {
117
+ throw new PathGuardError(
118
+ "PATH_WINDOWS_DEVICE_BLOCKED",
119
+ `Blocked device path segment: ${candidatePath}`,
120
+ );
121
+ }
122
+ }
123
+ }
124
+
125
+ function assertPathWithinAllowedRoot(candidatePath, allowedRoot) {
126
+ if (isPathInsideRoot(candidatePath, allowedRoot)) {
127
+ return;
128
+ }
129
+ throw new PathGuardError(
130
+ "PATH_OUTSIDE_ALLOWED_ROOT",
131
+ `Path escapes allowed root: ${candidatePath} (root: ${allowedRoot})`,
132
+ );
133
+ }
134
+
135
+ function isPathInsideRoot(candidatePath, rootPath) {
136
+ const normalizedCandidate = normalizeForComparison(candidatePath);
137
+ const normalizedRoot = normalizeForComparison(rootPath);
138
+ const relative = path.relative(normalizedRoot, normalizedCandidate);
139
+
140
+ if (!relative) {
141
+ return true;
142
+ }
143
+
144
+ return !relative.startsWith("..") && !path.isAbsolute(relative);
145
+ }
146
+
147
+ function normalizeForComparison(candidatePath) {
148
+ const resolved = path.resolve(candidatePath);
149
+ if (process.platform === "win32") {
150
+ return resolved.toLowerCase();
151
+ }
152
+ return resolved;
153
+ }
154
+
155
+ export class PathGuardError extends Error {
156
+ constructor(code, message) {
157
+ super(`[${code}] ${message}`);
158
+ this.name = "PathGuardError";
159
+ this.code = code;
160
+ }
161
+ }