@probelabs/probe 0.6.0-rc225 → 0.6.0-rc226

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.
@@ -3288,7 +3288,26 @@ var init_error_types = __esm({
3288
3288
 
3289
3289
  // src/utils/path-validation.js
3290
3290
  import path4 from "path";
3291
- import { promises as fs5 } from "fs";
3291
+ import { promises as fs5, realpathSync } from "fs";
3292
+ function safeRealpath(inputPath) {
3293
+ try {
3294
+ return realpathSync(inputPath);
3295
+ } catch (error) {
3296
+ const normalized = path4.normalize(inputPath);
3297
+ const parts = normalized.split(path4.sep);
3298
+ for (let i = parts.length - 1; i >= 0; i--) {
3299
+ const ancestorPath = parts.slice(0, i).join(path4.sep) || path4.sep;
3300
+ try {
3301
+ const resolvedAncestor = realpathSync(ancestorPath);
3302
+ const remainingParts = parts.slice(i);
3303
+ return path4.join(resolvedAncestor, ...remainingParts);
3304
+ } catch (ancestorError) {
3305
+ continue;
3306
+ }
3307
+ }
3308
+ return normalized;
3309
+ }
3310
+ }
3292
3311
  async function validateCwdPath(inputPath, defaultPath = process.cwd()) {
3293
3312
  const targetPath = inputPath || defaultPath;
3294
3313
  const normalizedPath = path4.normalize(path4.resolve(targetPath));
@@ -3321,6 +3340,53 @@ async function validateCwdPath(inputPath, defaultPath = process.cwd()) {
3321
3340
  }
3322
3341
  return normalizedPath;
3323
3342
  }
3343
+ function getCommonPrefix(folders) {
3344
+ if (!folders || folders.length === 0) {
3345
+ return process.cwd();
3346
+ }
3347
+ if (folders.length === 1) {
3348
+ return safeRealpath(folders[0]);
3349
+ }
3350
+ const normalized = folders.map((f) => safeRealpath(f));
3351
+ const segments = normalized.map((f) => f.split(path4.sep));
3352
+ const minLen = Math.min(...segments.map((s) => s.length));
3353
+ const commonSegments = [];
3354
+ for (let i = 0; i < minLen; i++) {
3355
+ const segment = segments[0][i];
3356
+ if (segments.every((s) => s[i] === segment)) {
3357
+ commonSegments.push(segment);
3358
+ } else {
3359
+ break;
3360
+ }
3361
+ }
3362
+ if (commonSegments.length === 0) {
3363
+ return normalized[0];
3364
+ }
3365
+ if (commonSegments.length === 1 && /^[a-zA-Z]:$/.test(commonSegments[0])) {
3366
+ return normalized[0];
3367
+ }
3368
+ if (commonSegments.length === 1 && commonSegments[0] === "") {
3369
+ return normalized[0];
3370
+ }
3371
+ return commonSegments.join(path4.sep);
3372
+ }
3373
+ function toRelativePath(absolutePath, workspaceRoot) {
3374
+ if (!absolutePath || !workspaceRoot) {
3375
+ return absolutePath;
3376
+ }
3377
+ let normalized = safeRealpath(absolutePath);
3378
+ let normalizedRoot = safeRealpath(workspaceRoot);
3379
+ while (normalizedRoot.length > 1 && normalizedRoot.endsWith(path4.sep)) {
3380
+ normalizedRoot = normalizedRoot.slice(0, -1);
3381
+ }
3382
+ if (normalized === normalizedRoot) {
3383
+ return ".";
3384
+ }
3385
+ if (normalized.startsWith(normalizedRoot + path4.sep)) {
3386
+ return path4.relative(normalizedRoot, normalized);
3387
+ }
3388
+ return absolutePath;
3389
+ }
3324
3390
  var init_path_validation = __esm({
3325
3391
  "src/utils/path-validation.js"() {
3326
3392
  "use strict";
@@ -4515,6 +4581,7 @@ async function analyzeAll(options) {
4515
4581
  sessionId,
4516
4582
  debug = false,
4517
4583
  cwd,
4584
+ workspaceRoot,
4518
4585
  allowedFolders,
4519
4586
  provider,
4520
4587
  model,
@@ -4527,10 +4594,11 @@ async function analyzeAll(options) {
4527
4594
  if (!question) {
4528
4595
  throw new Error('The "question" parameter is required.');
4529
4596
  }
4597
+ const effectiveWorkspaceRoot = workspaceRoot || cwd || allowedFolders?.[0] || path9;
4530
4598
  const delegateOptions = {
4531
4599
  debug,
4532
4600
  sessionId,
4533
- path: allowedFolders?.[0] || cwd || path9,
4601
+ path: effectiveWorkspaceRoot,
4534
4602
  allowedFolders,
4535
4603
  provider,
4536
4604
  model,
@@ -8747,29 +8815,33 @@ import { dirname, resolve, isAbsolute, sep } from "path";
8747
8815
  import { existsSync } from "fs";
8748
8816
  function isPathAllowed(filePath, allowedFolders) {
8749
8817
  if (!allowedFolders || allowedFolders.length === 0) {
8750
- const resolvedPath2 = resolve(filePath);
8751
- const cwd = resolve(process.cwd());
8818
+ const resolvedPath2 = safeRealpath(filePath);
8819
+ const cwd = safeRealpath(process.cwd());
8752
8820
  return resolvedPath2 === cwd || resolvedPath2.startsWith(cwd + sep);
8753
8821
  }
8754
- const resolvedPath = resolve(filePath);
8822
+ const resolvedPath = safeRealpath(filePath);
8755
8823
  return allowedFolders.some((folder) => {
8756
- const allowedPath = resolve(folder);
8824
+ const allowedPath = safeRealpath(folder);
8757
8825
  return resolvedPath === allowedPath || resolvedPath.startsWith(allowedPath + sep);
8758
8826
  });
8759
8827
  }
8760
8828
  function parseFileToolOptions(options = {}) {
8829
+ const allowedFolders = options.allowedFolders || [];
8761
8830
  return {
8762
8831
  debug: options.debug || false,
8763
- allowedFolders: options.allowedFolders || [],
8764
- cwd: options.cwd
8832
+ allowedFolders,
8833
+ cwd: options.cwd,
8834
+ // Consistent fallback chain: workspaceRoot > cwd > allowedFolders[0] > process.cwd()
8835
+ workspaceRoot: options.workspaceRoot || options.cwd || allowedFolders.length > 0 && allowedFolders[0] || process.cwd()
8765
8836
  };
8766
8837
  }
8767
8838
  var editTool, createTool, editSchema, createSchema, editDescription, createDescription, editToolDefinition, createToolDefinition;
8768
8839
  var init_edit = __esm({
8769
8840
  "src/tools/edit.js"() {
8770
8841
  "use strict";
8842
+ init_path_validation();
8771
8843
  editTool = (options = {}) => {
8772
- const { debug, allowedFolders, cwd } = parseFileToolOptions(options);
8844
+ const { debug, allowedFolders, cwd, workspaceRoot } = parseFileToolOptions(options);
8773
8845
  return tool({
8774
8846
  name: "edit",
8775
8847
  description: `Edit files using exact string replacement (Claude Code style).
@@ -8825,7 +8897,8 @@ Important:
8825
8897
  console.error(`[Edit] Attempting to edit file: ${resolvedPath}`);
8826
8898
  }
8827
8899
  if (!isPathAllowed(resolvedPath, allowedFolders)) {
8828
- return `Error editing file: Permission denied - ${file_path} is outside allowed directories`;
8900
+ const relativePath = toRelativePath(resolvedPath, workspaceRoot);
8901
+ return `Error editing file: Permission denied - ${relativePath} is outside allowed directories`;
8829
8902
  }
8830
8903
  if (!existsSync(resolvedPath)) {
8831
8904
  return `Error editing file: File not found - ${file_path}`;
@@ -8861,7 +8934,7 @@ Important:
8861
8934
  });
8862
8935
  };
8863
8936
  createTool = (options = {}) => {
8864
- const { debug, allowedFolders, cwd } = parseFileToolOptions(options);
8937
+ const { debug, allowedFolders, cwd, workspaceRoot } = parseFileToolOptions(options);
8865
8938
  return tool({
8866
8939
  name: "create",
8867
8940
  description: `Create new files with specified content.
@@ -8909,7 +8982,8 @@ Important:
8909
8982
  console.error(`[Create] Attempting to create file: ${resolvedPath}`);
8910
8983
  }
8911
8984
  if (!isPathAllowed(resolvedPath, allowedFolders)) {
8912
- return `Error creating file: Permission denied - ${file_path} is outside allowed directories`;
8985
+ const relativePath = toRelativePath(resolvedPath, workspaceRoot);
8986
+ return `Error creating file: Permission denied - ${relativePath} is outside allowed directories`;
8913
8987
  }
8914
8988
  if (existsSync(resolvedPath) && !overwrite) {
8915
8989
  return `Error creating file: File already exists - ${file_path}. Use overwrite: true to replace it.`;
@@ -10471,7 +10545,7 @@ var init_vercel = __esm({
10471
10545
  });
10472
10546
  };
10473
10547
  delegateTool = (options = {}) => {
10474
- const { debug = false, timeout = 300, cwd, allowedFolders, enableBash = false, bashConfig, architectureFileName, enableMcp = false, mcpConfig = null, mcpConfigPath = null, delegationManager = null } = options;
10548
+ const { debug = false, timeout = 300, cwd, allowedFolders, workspaceRoot, enableBash = false, bashConfig, architectureFileName, enableMcp = false, mcpConfig = null, mcpConfigPath = null, delegationManager = null } = options;
10475
10549
  return tool2({
10476
10550
  name: "delegate",
10477
10551
  description: delegateDescription,
@@ -10504,8 +10578,8 @@ var init_vercel = __esm({
10504
10578
  if (searchDelegate !== void 0 && typeof searchDelegate !== "boolean") {
10505
10579
  throw new TypeError("searchDelegate must be a boolean if provided");
10506
10580
  }
10507
- const workspaceRoot = allowedFolders && allowedFolders[0];
10508
- const effectivePath = path9 || workspaceRoot || cwd;
10581
+ const effectiveWorkspaceRoot = workspaceRoot || allowedFolders && allowedFolders[0];
10582
+ const effectivePath = path9 || effectiveWorkspaceRoot || cwd;
10509
10583
  if (debug) {
10510
10584
  console.error(`Executing delegate with task: "${task.substring(0, 100)}${task.length > 100 ? "..." : ""}"`);
10511
10585
  if (parentSessionId) {
@@ -10542,7 +10616,7 @@ var init_vercel = __esm({
10542
10616
  });
10543
10617
  };
10544
10618
  analyzeAllTool = (options = {}) => {
10545
- const { sessionId, debug = false, delegationManager = null } = options;
10619
+ const { sessionId, debug = false, delegationManager = null, workspaceRoot } = options;
10546
10620
  return tool2({
10547
10621
  name: "analyze_all",
10548
10622
  description: analyzeAllDescription,
@@ -10561,12 +10635,14 @@ var init_vercel = __esm({
10561
10635
  console.error(`[analyze_all] Question: ${question}`);
10562
10636
  console.error(`[analyze_all] Path: ${searchPath}`);
10563
10637
  }
10638
+ const effectiveWorkspaceRoot = workspaceRoot || options.cwd || options.allowedFolders && options.allowedFolders[0];
10564
10639
  const result = await analyzeAll({
10565
10640
  question,
10566
10641
  path: searchPath,
10567
10642
  sessionId,
10568
10643
  debug,
10569
10644
  cwd: options.cwd,
10645
+ workspaceRoot: effectiveWorkspaceRoot,
10570
10646
  allowedFolders: options.allowedFolders,
10571
10647
  provider: options.provider,
10572
10648
  model: options.model,
@@ -12042,14 +12118,17 @@ var init_bash = __esm({
12042
12118
  "use strict";
12043
12119
  init_bashPermissions();
12044
12120
  init_bashExecutor();
12121
+ init_path_validation();
12045
12122
  bashTool = (options = {}) => {
12046
12123
  const {
12047
12124
  bashConfig = {},
12048
12125
  debug = false,
12049
12126
  cwd,
12050
12127
  allowedFolders = [],
12128
+ workspaceRoot: providedWorkspaceRoot,
12051
12129
  tracer = null
12052
12130
  } = options;
12131
+ const workspaceRoot = providedWorkspaceRoot || cwd || allowedFolders.length > 0 && allowedFolders[0] || process.cwd();
12053
12132
  const permissionChecker = new BashPermissionChecker({
12054
12133
  allow: bashConfig.allow,
12055
12134
  deny: bashConfig.deny,
@@ -12065,6 +12144,9 @@ var init_bash = __esm({
12065
12144
  if (cwd) {
12066
12145
  return cwd;
12067
12146
  }
12147
+ if (workspaceRoot) {
12148
+ return workspaceRoot;
12149
+ }
12068
12150
  if (allowedFolders && allowedFolders.length > 0) {
12069
12151
  return allowedFolders[0];
12070
12152
  }
@@ -12152,13 +12234,15 @@ For code exploration, try these safe alternatives:
12152
12234
  const defaultDir = getDefaultWorkingDirectory();
12153
12235
  const workingDir = workingDirectory ? isAbsolute3(workingDirectory) ? resolve4(workingDirectory) : resolve4(defaultDir, workingDirectory) : defaultDir;
12154
12236
  if (allowedFolders && allowedFolders.length > 0) {
12155
- const resolvedWorkingDir = resolve4(workingDir);
12237
+ const resolvedWorkingDir = safeRealpath(workingDir);
12156
12238
  const isAllowed = allowedFolders.some((folder) => {
12157
- const resolvedFolder = resolve4(folder);
12239
+ const resolvedFolder = safeRealpath(folder);
12158
12240
  return resolvedWorkingDir === resolvedFolder || resolvedWorkingDir.startsWith(resolvedFolder + sep2);
12159
12241
  });
12160
12242
  if (!isAllowed) {
12161
- return `Error: Working directory "${workingDir}" is not within allowed folders: ${allowedFolders.join(", ")}`;
12243
+ const relativeDir = toRelativePath(workingDir, workspaceRoot);
12244
+ const relativeAllowed = allowedFolders.map((f) => toRelativePath(f, workspaceRoot));
12245
+ return `Error: Working directory "${relativeDir}" is not within allowed folders: ${relativeAllowed.join(", ")}`;
12162
12246
  }
12163
12247
  }
12164
12248
  const executionOptions = {
@@ -16765,18 +16849,18 @@ import { fileURLToPath as fileURLToPath4 } from "node:url";
16765
16849
  import { lstatSync, readdir as readdirCB, readdirSync, readlinkSync, realpathSync as rps } from "fs";
16766
16850
  import * as actualFS from "node:fs";
16767
16851
  import { lstat, readdir, readlink, realpath } from "node:fs/promises";
16768
- var realpathSync, defaultFS, fsFromOption, uncDriveRegexp, uncToDrive, eitherSep, UNKNOWN, IFIFO, IFCHR, IFDIR, IFBLK, IFREG, IFLNK, IFSOCK, IFMT, IFMT_UNKNOWN, READDIR_CALLED, LSTAT_CALLED, ENOTDIR, ENOENT, ENOREADLINK, ENOREALPATH, ENOCHILD, TYPEMASK, entToType, normalizeCache, normalize, normalizeNocaseCache, normalizeNocase, ResolveCache, ChildrenCache, setAsCwd, PathBase, PathWin32, PathPosix, PathScurryBase, PathScurryWin32, PathScurryPosix, PathScurryDarwin, Path, PathScurry;
16852
+ var realpathSync2, defaultFS, fsFromOption, uncDriveRegexp, uncToDrive, eitherSep, UNKNOWN, IFIFO, IFCHR, IFDIR, IFBLK, IFREG, IFLNK, IFSOCK, IFMT, IFMT_UNKNOWN, READDIR_CALLED, LSTAT_CALLED, ENOTDIR, ENOENT, ENOREADLINK, ENOREALPATH, ENOCHILD, TYPEMASK, entToType, normalizeCache, normalize, normalizeNocaseCache, normalizeNocase, ResolveCache, ChildrenCache, setAsCwd, PathBase, PathWin32, PathPosix, PathScurryBase, PathScurryWin32, PathScurryPosix, PathScurryDarwin, Path, PathScurry;
16769
16853
  var init_esm4 = __esm({
16770
16854
  "node_modules/path-scurry/dist/esm/index.js"() {
16771
16855
  init_esm2();
16772
16856
  init_esm3();
16773
- realpathSync = rps.native;
16857
+ realpathSync2 = rps.native;
16774
16858
  defaultFS = {
16775
16859
  lstatSync,
16776
16860
  readdir: readdirCB,
16777
16861
  readdirSync,
16778
16862
  readlinkSync,
16779
- realpathSync,
16863
+ realpathSync: realpathSync2,
16780
16864
  promises: {
16781
16865
  lstat,
16782
16866
  readdir,
@@ -70171,6 +70255,7 @@ var init_ProbeAgent = __esm({
70171
70255
  init_FallbackManager();
70172
70256
  init_contextCompactor();
70173
70257
  init_error_types();
70258
+ init_path_validation();
70174
70259
  init_outputTruncator();
70175
70260
  init_delegate();
70176
70261
  init_tasks();
@@ -70288,7 +70373,8 @@ var init_ProbeAgent = __esm({
70288
70373
  } else {
70289
70374
  this.allowedFolders = [process.cwd()];
70290
70375
  }
70291
- this.cwd = options.cwd || null;
70376
+ this.workspaceRoot = getCommonPrefix(this.allowedFolders);
70377
+ this.cwd = options.cwd || this.workspaceRoot;
70292
70378
  this.clientApiProvider = options.provider || null;
70293
70379
  this.clientApiModel = options.model || null;
70294
70380
  this.clientApiKey = null;
@@ -70300,6 +70386,8 @@ var init_ProbeAgent = __esm({
70300
70386
  console.log(`[DEBUG] Maximum tool iterations configured: ${MAX_TOOL_ITERATIONS}`);
70301
70387
  console.log(`[DEBUG] Allow Edit (implement tool): ${this.allowEdit}`);
70302
70388
  console.log(`[DEBUG] Search delegation enabled: ${this.searchDelegate}`);
70389
+ console.log(`[DEBUG] Workspace root: ${this.workspaceRoot}`);
70390
+ console.log(`[DEBUG] Working directory (cwd): ${this.cwd}`);
70303
70391
  }
70304
70392
  this.initializeTools();
70305
70393
  this.history = [];
@@ -70647,8 +70735,9 @@ var init_ProbeAgent = __esm({
70647
70735
  const configOptions = {
70648
70736
  sessionId: this.sessionId,
70649
70737
  debug: this.debug,
70650
- // Use explicit cwd if set, otherwise fall back to first allowed folder
70651
- cwd: this.cwd || (this.allowedFolders.length > 0 ? this.allowedFolders[0] : process.cwd()),
70738
+ // Use cwd (which defaults to workspaceRoot in constructor)
70739
+ cwd: this.cwd,
70740
+ workspaceRoot: this.workspaceRoot,
70652
70741
  allowedFolders: this.allowedFolders,
70653
70742
  outline: this.outline,
70654
70743
  searchDelegate: this.searchDelegate,
@@ -71335,16 +71424,16 @@ var init_ProbeAgent = __esm({
71335
71424
  let absolutePath;
71336
71425
  let isPathAllowed2 = false;
71337
71426
  if (isAbsolute5(imagePath)) {
71338
- absolutePath = normalize2(resolve6(imagePath));
71427
+ absolutePath = safeRealpath(resolve6(imagePath));
71339
71428
  isPathAllowed2 = allowedDirs.some((dir) => {
71340
- const normalizedDir = normalize2(resolve6(dir));
71341
- return absolutePath === normalizedDir || absolutePath.startsWith(normalizedDir + sep5);
71429
+ const resolvedDir = safeRealpath(dir);
71430
+ return absolutePath === resolvedDir || absolutePath.startsWith(resolvedDir + sep5);
71342
71431
  });
71343
71432
  } else {
71344
71433
  for (const dir of allowedDirs) {
71345
- const normalizedDir = normalize2(resolve6(dir));
71346
- const resolvedPath = normalize2(resolve6(dir, imagePath));
71347
- if (resolvedPath === normalizedDir || resolvedPath.startsWith(normalizedDir + sep5)) {
71434
+ const resolvedDir = safeRealpath(dir);
71435
+ const resolvedPath = safeRealpath(resolve6(dir, imagePath));
71436
+ if (resolvedPath === resolvedDir || resolvedPath.startsWith(resolvedDir + sep5)) {
71348
71437
  absolutePath = resolvedPath;
71349
71438
  isPathAllowed2 = true;
71350
71439
  break;
@@ -71527,7 +71616,7 @@ var init_ProbeAgent = __esm({
71527
71616
  if (this._architectureContextLoaded) {
71528
71617
  return this.architectureContext;
71529
71618
  }
71530
- const rootDirectory = this.allowedFolders.length > 0 ? this.allowedFolders[0] : process.cwd();
71619
+ const rootDirectory = this.workspaceRoot || (this.allowedFolders.length > 0 ? this.allowedFolders[0] : process.cwd());
71531
71620
  const configuredName = typeof this.architectureFileName === "string" ? this.architectureFileName.trim() : "";
71532
71621
  const hasConfiguredName = !!configuredName;
71533
71622
  let guidanceCandidates = [];
@@ -71664,6 +71753,9 @@ ${this.architectureContext.content}
71664
71753
  `;
71665
71754
  }
71666
71755
  _getSkillsRepoRoot() {
71756
+ if (this.workspaceRoot) {
71757
+ return resolve6(this.workspaceRoot);
71758
+ }
71667
71759
  if (this.allowedFolders && this.allowedFolders.length > 0) {
71668
71760
  return resolve6(this.allowedFolders[0]);
71669
71761
  }
@@ -71731,7 +71823,7 @@ Workspace: ${this.allowedFolders.join(", ")}`;
71731
71823
 
71732
71824
  # Repository Structure
71733
71825
  `;
71734
- systemPrompt += `You are working with a repository located at: ${this.allowedFolders[0]}
71826
+ systemPrompt += `You are working with a repository located at: ${this.workspaceRoot}
71735
71827
 
71736
71828
  `;
71737
71829
  systemPrompt += `Here's an overview of the repository structure (showing up to 100 most relevant files):
@@ -71784,7 +71876,7 @@ Workspace: ${this.allowedFolders.join(", ")}`;
71784
71876
 
71785
71877
  # Repository Structure
71786
71878
  `;
71787
- systemPrompt += `You are working with a repository located at: ${this.allowedFolders[0]}
71879
+ systemPrompt += `You are working with a repository located at: ${this.workspaceRoot}
71788
71880
 
71789
71881
  `;
71790
71882
  systemPrompt += `Here's an overview of the repository structure (showing up to 100 most relevant files):
@@ -72083,21 +72175,34 @@ For MCP tools, use JSON format within the params tag, e.g.:
72083
72175
  `;
72084
72176
  }
72085
72177
  }
72086
- const searchDirectory = this.allowedFolders.length > 0 ? this.allowedFolders[0] : process.cwd();
72178
+ const searchDirectory = this.workspaceRoot;
72087
72179
  if (this.debug) {
72088
- console.log(`[DEBUG] Generating file list for base directory: ${searchDirectory}...`);
72180
+ console.log(`[DEBUG] Generating file list for workspace root: ${searchDirectory}...`);
72181
+ }
72182
+ const relativeWorkspaces = this.allowedFolders.map((f) => {
72183
+ const rel = toRelativePath(f, this.workspaceRoot);
72184
+ if (rel && rel !== "." && !rel.startsWith(".") && !rel.startsWith("/")) {
72185
+ return "./" + rel;
72186
+ }
72187
+ return rel;
72188
+ }).filter((f) => f && f !== ".");
72189
+ let workspaceDesc;
72190
+ if (relativeWorkspaces.length === 0) {
72191
+ workspaceDesc = ". (current directory)";
72192
+ } else {
72193
+ workspaceDesc = relativeWorkspaces.join(", ");
72089
72194
  }
72090
72195
  try {
72091
72196
  const files = await listFilesByLevel({
72092
72197
  directory: searchDirectory,
72093
72198
  maxFiles: 100,
72094
72199
  respectGitignore: !process.env.PROBE_NO_GITIGNORE || process.env.PROBE_NO_GITIGNORE === "",
72095
- cwd: process.cwd()
72200
+ cwd: this.workspaceRoot
72096
72201
  });
72097
72202
  systemMessage += `
72098
72203
  # Repository Structure
72099
72204
 
72100
- You are working with a repository located at: ${searchDirectory}
72205
+ You are working with a workspace. Available paths: ${workspaceDesc}
72101
72206
 
72102
72207
  Here's an overview of the repository structure (showing up to 100 most relevant files):
72103
72208
 
@@ -72113,15 +72218,22 @@ ${files}
72113
72218
  systemMessage += `
72114
72219
  # Repository Structure
72115
72220
 
72116
- You are working with a repository located at: ${searchDirectory}
72221
+ You are working with a workspace. Available paths: ${workspaceDesc}
72117
72222
 
72118
72223
  `;
72119
72224
  }
72120
72225
  await this.loadArchitectureContext();
72121
72226
  systemMessage += this.getArchitectureSection();
72122
72227
  if (this.allowedFolders.length > 0) {
72228
+ const relativeAllowed = this.allowedFolders.map((f) => {
72229
+ const rel = toRelativePath(f, this.workspaceRoot);
72230
+ if (rel && rel !== "." && !rel.startsWith(".") && !rel.startsWith("/")) {
72231
+ return "./" + rel;
72232
+ }
72233
+ return rel;
72234
+ });
72123
72235
  systemMessage += `
72124
- **Important**: For security reasons, you can only search within these allowed folders: ${this.allowedFolders.join(", ")}
72236
+ **Important**: For security reasons, you can only access these paths: ${relativeAllowed.join(", ")}
72125
72237
 
72126
72238
  `;
72127
72239
  }
@@ -72635,6 +72747,7 @@ You are working with a repository located at: ${searchDirectory}
72635
72747
  console.error(`[DEBUG] ========================================
72636
72748
  `);
72637
72749
  }
72750
+ currentMessages.push({ role: "assistant", content: assistantResponseContent });
72638
72751
  currentMessages.push({ role: "user", content: `<tool_result>
72639
72752
  ${toolResultContent}
72640
72753
  </tool_result>` });
@@ -72655,17 +72768,18 @@ ${toolResultContent}
72655
72768
  `);
72656
72769
  }
72657
72770
  const errorXml = formatErrorForAI(error);
72771
+ currentMessages.push({ role: "assistant", content: assistantResponseContent });
72658
72772
  currentMessages.push({ role: "user", content: `<tool_result>
72659
72773
  ${errorXml}
72660
72774
  </tool_result>` });
72661
72775
  }
72662
72776
  } else if (this.toolImplementations[toolName]) {
72663
72777
  try {
72664
- let resolvedWorkingDirectory = this.cwd || this.allowedFolders && this.allowedFolders[0] || process.cwd();
72778
+ let resolvedWorkingDirectory = this.workspaceRoot || this.cwd || this.allowedFolders && this.allowedFolders[0] || process.cwd();
72665
72779
  if (params.workingDirectory) {
72666
- const requestedDir = isAbsolute5(params.workingDirectory) ? resolve6(params.workingDirectory) : resolve6(resolvedWorkingDirectory, params.workingDirectory);
72780
+ const requestedDir = safeRealpath(isAbsolute5(params.workingDirectory) ? resolve6(params.workingDirectory) : resolve6(resolvedWorkingDirectory, params.workingDirectory));
72667
72781
  const isWithinAllowed = !this.allowedFolders || this.allowedFolders.length === 0 || this.allowedFolders.some((folder) => {
72668
- const resolvedFolder = resolve6(folder);
72782
+ const resolvedFolder = safeRealpath(folder);
72669
72783
  return requestedDir === resolvedFolder || requestedDir.startsWith(resolvedFolder + sep5);
72670
72784
  });
72671
72785
  if (isWithinAllowed) {
@@ -73134,7 +73248,7 @@ Convert your previous response content into actual JSON data that follows this s
73134
73248
  }
73135
73249
  const mermaidValidation = await validateAndFixMermaidResponse(finalResult, {
73136
73250
  debug: this.debug,
73137
- path: this.allowedFolders[0],
73251
+ path: this.workspaceRoot || this.allowedFolders[0],
73138
73252
  provider: this.clientApiProvider,
73139
73253
  model: this.model,
73140
73254
  tracer: this.tracer
@@ -73207,7 +73321,7 @@ Convert your previous response content into actual JSON data that follows this s
73207
73321
  }
73208
73322
  const { JsonFixingAgent: JsonFixingAgent2 } = await Promise.resolve().then(() => (init_schemaUtils(), schemaUtils_exports));
73209
73323
  const jsonFixer = new JsonFixingAgent2({
73210
- path: this.allowedFolders[0],
73324
+ path: this.workspaceRoot || this.allowedFolders[0],
73211
73325
  provider: this.clientApiProvider,
73212
73326
  model: this.model,
73213
73327
  debug: this.debug,
@@ -73276,7 +73390,7 @@ Convert your previous response content into actual JSON data that follows this s
73276
73390
  }
73277
73391
  const mermaidValidation = await validateAndFixMermaidResponse(finalResult, {
73278
73392
  debug: this.debug,
73279
- path: this.allowedFolders[0],
73393
+ path: this.workspaceRoot || this.allowedFolders[0],
73280
73394
  provider: this.clientApiProvider,
73281
73395
  model: this.model,
73282
73396
  tracer: this.tracer
@@ -73408,7 +73522,7 @@ Convert your previous response content into actual JSON data that follows this s
73408
73522
  }
73409
73523
  const finalMermaidValidation = await validateAndFixMermaidResponse(finalResult, {
73410
73524
  debug: this.debug,
73411
- path: this.allowedFolders[0],
73525
+ path: this.workspaceRoot || this.allowedFolders[0],
73412
73526
  provider: this.clientApiProvider,
73413
73527
  model: this.model,
73414
73528
  tracer: this.tracer
@@ -73558,8 +73672,7 @@ Convert your previous response content into actual JSON data that follows this s
73558
73672
  allowEdit: this.allowEdit,
73559
73673
  enableDelegate: this.enableDelegate,
73560
73674
  architectureFileName: this.architectureFileName,
73561
- path: this.allowedFolders[0],
73562
- // Use first allowed folder as primary path
73675
+ // Pass allowedFolders which will recompute workspaceRoot correctly
73563
73676
  allowedFolders: [...this.allowedFolders],
73564
73677
  cwd: this.cwd,
73565
73678
  // Preserve explicit working directory
@@ -514,6 +514,7 @@ export async function analyzeAll(options) {
514
514
  sessionId,
515
515
  debug = false,
516
516
  cwd,
517
+ workspaceRoot,
517
518
  allowedFolders,
518
519
  provider,
519
520
  model,
@@ -527,10 +528,14 @@ export async function analyzeAll(options) {
527
528
  throw new Error('The "question" parameter is required.');
528
529
  }
529
530
 
531
+ // Use workspaceRoot (computed common prefix) for consistent path handling
532
+ // Consistent fallback chain: workspaceRoot > cwd > allowedFolders[0] > path
533
+ const effectiveWorkspaceRoot = workspaceRoot || cwd || allowedFolders?.[0] || path;
534
+
530
535
  const delegateOptions = {
531
536
  debug,
532
537
  sessionId,
533
- path: allowedFolders?.[0] || cwd || path,
538
+ path: effectiveWorkspaceRoot,
534
539
  allowedFolders,
535
540
  provider,
536
541
  model,
@@ -7,6 +7,7 @@ import { tool } from 'ai';
7
7
  import { resolve, isAbsolute, sep } from 'path';
8
8
  import { BashPermissionChecker } from '../agent/bashPermissions.js';
9
9
  import { executeBashCommand, formatExecutionResult, validateExecutionOptions } from '../agent/bashExecutor.js';
10
+ import { toRelativePath, safeRealpath } from '../utils/path-validation.js';
10
11
 
11
12
  /**
12
13
  * Bash tool generator
@@ -32,9 +33,14 @@ export const bashTool = (options = {}) => {
32
33
  debug = false,
33
34
  cwd,
34
35
  allowedFolders = [],
36
+ workspaceRoot: providedWorkspaceRoot,
35
37
  tracer = null
36
38
  } = options;
37
39
 
40
+ // Compute workspaceRoot with proper fallback chain
41
+ // Priority: explicit workspaceRoot > cwd > allowedFolders[0] > process.cwd()
42
+ const workspaceRoot = providedWorkspaceRoot || cwd || (allowedFolders.length > 0 && allowedFolders[0]) || process.cwd();
43
+
38
44
  // Create permission checker with tracer for telemetry
39
45
  const permissionChecker = new BashPermissionChecker({
40
46
  allow: bashConfig.allow,
@@ -46,6 +52,7 @@ export const bashTool = (options = {}) => {
46
52
  });
47
53
 
48
54
  // Determine default working directory
55
+ // Priority: explicit bashConfig.workingDirectory > cwd (which defaults to workspaceRoot) > fallback
49
56
  const getDefaultWorkingDirectory = () => {
50
57
  if (bashConfig.workingDirectory) {
51
58
  return bashConfig.workingDirectory;
@@ -53,6 +60,10 @@ export const bashTool = (options = {}) => {
53
60
  if (cwd) {
54
61
  return cwd;
55
62
  }
63
+ // Use workspaceRoot (computed common prefix) for consistency with other tools
64
+ if (workspaceRoot) {
65
+ return workspaceRoot;
66
+ }
56
67
  if (allowedFolders && allowedFolders.length > 0) {
57
68
  return allowedFolders[0];
58
69
  }
@@ -154,16 +165,20 @@ For code exploration, try these safe alternatives:
154
165
 
155
166
  // Validate working directory is within allowed folders if specified
156
167
  if (allowedFolders && allowedFolders.length > 0) {
157
- const resolvedWorkingDir = resolve(workingDir);
168
+ // Use safeRealpath to resolve symlinks for security
169
+ // This prevents symlink bypass attacks (e.g., /tmp -> /private/tmp on macOS)
170
+ const resolvedWorkingDir = safeRealpath(workingDir);
158
171
  const isAllowed = allowedFolders.some(folder => {
159
- const resolvedFolder = resolve(folder);
172
+ const resolvedFolder = safeRealpath(folder);
160
173
  // Use exact match OR startsWith with separator to prevent bypass attacks
161
174
  // e.g., '/tmp-malicious' should NOT match allowed folder '/tmp'
162
175
  return resolvedWorkingDir === resolvedFolder || resolvedWorkingDir.startsWith(resolvedFolder + sep);
163
176
  });
164
177
 
165
178
  if (!isAllowed) {
166
- return `Error: Working directory "${workingDir}" is not within allowed folders: ${allowedFolders.join(', ')}`;
179
+ const relativeDir = toRelativePath(workingDir, workspaceRoot);
180
+ const relativeAllowed = allowedFolders.map(f => toRelativePath(f, workspaceRoot));
181
+ return `Error: Working directory "${relativeDir}" is not within allowed folders: ${relativeAllowed.join(', ')}`;
167
182
  }
168
183
  }
169
184