@beastmode-develeap/beastmode 0.1.360 → 0.1.362

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.
package/dist/index.js CHANGED
@@ -5282,33 +5282,51 @@ var init_chat_handler = __esm({
5282
5282
 
5283
5283
  // src/cli/ui/board-api-routes.ts
5284
5284
  import { readFileSync as readFileSync14, writeFileSync as writeFileSync13, existsSync as existsSync16, readdirSync as readdirSync8, unlinkSync as unlinkSync4, mkdirSync as mkdirSync12, statSync as statSync6, rmSync as rmSync3 } from "fs";
5285
- import { join as join15, basename as basename5, resolve as resolve4, dirname as dirname6, sep } from "path";
5285
+ import { join as join15, basename as basename5, resolve as resolve4, dirname as dirname6, sep, delimiter } from "path";
5286
5286
  import { homedir } from "os";
5287
5287
  import { randomUUID as randomUUID2 } from "crypto";
5288
5288
  import { execSync as execSync3, spawnSync } from "child_process";
5289
5289
  import http2 from "http";
5290
+ function _beastmodeRoot() {
5291
+ return process.env.BEASTMODE_ROOT && process.env.BEASTMODE_ROOT.length > 0 ? process.env.BEASTMODE_ROOT : "/app";
5292
+ }
5290
5293
  function _getAllowedRoots() {
5291
- const roots = [];
5294
+ const candidates = [];
5295
+ const allowlistEnv = process.env.BEASTMODE_FS_ALLOWLIST;
5296
+ if (allowlistEnv && allowlistEnv.length > 0) {
5297
+ for (const entry of allowlistEnv.split(delimiter)) {
5298
+ if (entry && entry.length > 0) candidates.push(entry);
5299
+ }
5300
+ }
5301
+ const root = _beastmodeRoot();
5302
+ candidates.push(join15(root, "project"));
5303
+ candidates.push(root);
5292
5304
  const homeRaw = process.env.HOME && process.env.HOME.length > 0 ? process.env.HOME : homedir();
5293
- roots.push(resolve4(homeRaw));
5305
+ candidates.push(homeRaw);
5294
5306
  const projectDir = process.env.PROJECT_DIR;
5295
- if (projectDir && projectDir.length > 0) {
5296
- roots.push(resolve4(projectDir));
5297
- }
5307
+ if (projectDir && projectDir.length > 0) candidates.push(projectDir);
5298
5308
  const cloneDir = process.env.BEASTMODE_CLONE_DIR;
5299
- if (cloneDir && cloneDir.length > 0) {
5300
- roots.push(resolve4(cloneDir));
5301
- }
5309
+ if (cloneDir && cloneDir.length > 0) candidates.push(cloneDir);
5302
5310
  const seen = /* @__PURE__ */ new Set();
5303
5311
  const deduped = [];
5304
- for (const r of roots) {
5305
- if (!seen.has(r)) {
5306
- seen.add(r);
5307
- deduped.push(r);
5308
- }
5312
+ for (const c of candidates) {
5313
+ const r = resolve4(c);
5314
+ if (!existsSync16(r)) continue;
5315
+ if (seen.has(r)) continue;
5316
+ seen.add(r);
5317
+ deduped.push(r);
5309
5318
  }
5310
5319
  return deduped;
5311
5320
  }
5321
+ function _pickDefaultRoot(roots) {
5322
+ const root = _beastmodeRoot();
5323
+ const preferred = [resolve4(join15(root, "project")), resolve4(root)];
5324
+ for (const p of preferred) {
5325
+ if (roots.includes(p)) return p;
5326
+ }
5327
+ if (roots.length > 0) return roots[0];
5328
+ return resolve4(homedir());
5329
+ }
5312
5330
  function _isPathAllowed(targetPath, roots) {
5313
5331
  if (roots.length === 0) return false;
5314
5332
  const target = resolve4(targetPath);
@@ -6354,10 +6372,15 @@ function getBoardRoutes(factoryDir) {
6354
6372
  {
6355
6373
  // Filesystem browser for the Add Project picker.
6356
6374
  //
6357
- // POST body: { path?: string } — absolute directory to list (default: $HOME)
6375
+ // POST body: { path?: string } — absolute directory to list. An omitted
6376
+ // or empty path opens at a known-good default root (the mounted project
6377
+ // root, e.g. /app/project), never an empty $HOME (#969 FR-3).
6358
6378
  //
6359
- // Returns { path, parent, entries: [{name, path, is_dir, has_git}] }.
6379
+ // Returns { path, parent, entries: [{name, path, is_dir, has_git}], roots }
6380
+ // where `roots` is the existence-gated allowlist for the picker chips.
6360
6381
  // Only directories are returned (the picker only selects dirs).
6382
+ // User-input errors (disallowed/missing/not-a-dir) return 4xx via
6383
+ // HttpError, never 500 (#969 FR-2).
6361
6384
  // This is behind the board's password auth, so it trusts the caller
6362
6385
  // with full filesystem read access — the daemon's trust boundary
6363
6386
  // already allows cloning arbitrary repos and reading arbitrary code.
@@ -6365,17 +6388,21 @@ function getBoardRoutes(factoryDir) {
6365
6388
  pattern: "/api/filesystem/browse",
6366
6389
  handler: (body) => {
6367
6390
  const { path: queryPath } = body || {};
6368
- const startPath = queryPath || homedir();
6369
- const target = resolve4(startPath);
6370
6391
  const allowedRoots = _getAllowedRoots();
6392
+ const startPath = queryPath && queryPath.length > 0 ? queryPath : _pickDefaultRoot(allowedRoots);
6393
+ const target = resolve4(startPath);
6371
6394
  if (!_isPathAllowed(target, allowedRoots)) {
6372
- throw new Error(
6373
- `Path not allowed: ${target} is outside the configured allowlist`
6374
- );
6395
+ throw new HttpError(400, {
6396
+ error: `Path not allowed: ${target} is outside the configured allowlist`
6397
+ });
6398
+ }
6399
+ if (!existsSync16(target)) {
6400
+ throw new HttpError(404, { error: `Path not found: ${target}` });
6375
6401
  }
6376
- if (!existsSync16(target)) throw new Error(`Path not found: ${target}`);
6377
6402
  const st = statSync6(target);
6378
- if (!st.isDirectory()) throw new Error(`Not a directory: ${target}`);
6403
+ if (!st.isDirectory()) {
6404
+ throw new HttpError(400, { error: `Not a directory: ${target}` });
6405
+ }
6379
6406
  let entries = [];
6380
6407
  try {
6381
6408
  entries = readdirSync8(target, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith(".")).map((d) => {
@@ -6394,7 +6421,10 @@ function getBoardRoutes(factoryDir) {
6394
6421
  return {
6395
6422
  path: target,
6396
6423
  parent: parent !== target ? parent : null,
6397
- entries
6424
+ entries,
6425
+ // FR-3: surface the allowed roots so the picker can render them as
6426
+ // clickable navigation chips.
6427
+ roots: allowedRoots
6398
6428
  };
6399
6429
  }
6400
6430
  },