codecartographer-pi 0.12.9 → 0.12.11

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.
@@ -3,6 +3,15 @@ export declare function pathExists(path: string): Promise<boolean>;
3
3
  export declare function canonicalPath(path: string): Promise<string>;
4
4
  export declare function normalizeForComparison(path: string): string;
5
5
  export declare function isWithinPath(path: string, root: string): boolean;
6
+ /**
7
+ * Symlink-aware version of isWithinPath. Resolves symlinks on both the path
8
+ * and root before comparing, preventing bypass via symlinks inside the
9
+ * allowed root that point outside it.
10
+ *
11
+ * Falls back to lexical isWithinPath if realpath fails (e.g., path does not
12
+ * exist yet), which is safe for write targets that haven't been created.
13
+ */
14
+ export declare function isWithinPathResolved(path: string, root: string): Promise<boolean>;
6
15
  export declare function isPlainObject(value: unknown): value is Record<string, unknown>;
7
16
  export declare function uniqueStrings(items: string[]): string[];
8
17
  export declare function dateOnly(timestamp: string): string;
@@ -36,6 +36,27 @@ export function isWithinPath(path, root) {
36
36
  return true;
37
37
  return normalizedPath.startsWith(`${normalizedRoot}${process.platform === "win32" ? "\\" : "/"}`);
38
38
  }
39
+ /**
40
+ * Symlink-aware version of isWithinPath. Resolves symlinks on both the path
41
+ * and root before comparing, preventing bypass via symlinks inside the
42
+ * allowed root that point outside it.
43
+ *
44
+ * Falls back to lexical isWithinPath if realpath fails (e.g., path does not
45
+ * exist yet), which is safe for write targets that haven't been created.
46
+ */
47
+ export async function isWithinPathResolved(path, root) {
48
+ try {
49
+ const resolvedPath = await realpath(path);
50
+ const resolvedRoot = await realpath(root);
51
+ return isWithinPath(resolvedPath, resolvedRoot);
52
+ }
53
+ catch {
54
+ // If realpath fails (path doesn't exist yet, broken symlink, etc.),
55
+ // fall back to lexical check. For write targets this is safe because
56
+ // the parent directory should already be within the root.
57
+ return isWithinPath(path, root);
58
+ }
59
+ }
39
60
  export function isPlainObject(value) {
40
61
  return typeof value === "object" && value !== null && !Array.isArray(value);
41
62
  }
@@ -8,7 +8,7 @@ import { narrateDashboard } from "./dashboard-narrator.js";
8
8
  import { writeDashboard } from "./dashboard-writer.js";
9
9
  import { parseNextFlags } from "./next-flags.js";
10
10
  import { phaseCompactionExtension } from "./phase-compaction.js";
11
- import { buildPhasePrompt, buildSkillPrompt, buildValidationSummary, canonicalPath, computePerPhaseTotals, computeTotals, createEmptyStatus, DEFAULT_PIPELINE_PATH, deriveSlug, discoverLibrary, getNextEligiblePhase, getPipelineLabel, getWorkspaceState, isWithinPath, listSkillNames, loadCodecartoConfig, loadUsage, loadYamlFile, normalizeForComparison, packagedWorkspaceDir, pathExists, PACKAGE_VERSION, PhasePreflightError, PIPELINE_ALIASES, publishEntry, resolvePhase, resolvePipelineChoice, runPhasePreflight, stringifySimpleYaml, switchPipeline, validatePhaseOutput, writeLibraryConfig, } from "../../core/index.js";
11
+ import { buildPhasePrompt, buildSkillPrompt, buildValidationSummary, canonicalPath, computePerPhaseTotals, computeTotals, createEmptyStatus, DEFAULT_PIPELINE_PATH, deriveSlug, discoverLibrary, getNextEligiblePhase, getPipelineLabel, getWorkspaceState, isWithinPathResolved, listSkillNames, loadCodecartoConfig, loadUsage, loadYamlFile, normalizeForComparison, packagedWorkspaceDir, pathExists, PACKAGE_VERSION, PhasePreflightError, PIPELINE_ALIASES, publishEntry, resolvePhase, resolvePipelineChoice, runPhasePreflight, stringifySimpleYaml, switchPipeline, validatePhaseOutput, writeLibraryConfig, } from "../../core/index.js";
12
12
  import { initLibrary } from "../../core/library.js";
13
13
  import { resolveUserConfigPath } from "../../core/orchestrator-config.js";
14
14
  const STATUS_WIDGET_ID = "codecarto-widget";
@@ -167,7 +167,8 @@ export default function codeCartographerExtension(pi) {
167
167
  if (config.library.path && await discoverLibrary(config.library.path)) {
168
168
  allowedRoots.push(await canonicalPath(config.library.path));
169
169
  }
170
- if (!allowedRoots.some((allowedRoot) => isWithinPath(targetPath, allowedRoot))) {
170
+ const withinAllowed = await Promise.all(allowedRoots.map((allowedRoot) => isWithinPathResolved(targetPath, allowedRoot)));
171
+ if (!withinAllowed.some((result) => result)) {
171
172
  if (ctx.hasUI) {
172
173
  ctx.ui.notify(`Blocked ${event.toolName} outside .codecarto/ or configured library: ${inputPath}`, "warning");
173
174
  }
@@ -15,7 +15,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
15
15
  import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from "@modelcontextprotocol/sdk/types.js";
16
16
  import { cp, mkdir, readFile, rename, writeFile } from "node:fs/promises";
17
17
  import { basename, isAbsolute, join } from "node:path";
18
- import { buildPhasePrompt, buildSkillPrompt, buildValidationSummary, canonicalPath, completeValidatedPhase, computePerPhaseTotals, computeTotals, createEmptyStatus, DEFAULT_PIPELINE_PATH, deriveSlug, discoverLibrary, getNextEligiblePhase, getPipelineLabel, getWorkspaceState, isValidSlug, listEntries, listSkillNames, loadCodecartoConfig, loadUsage, loadYamlFile, normalizeForComparison, PACKAGE_VERSION, packagedWorkspaceDir, pathExists, PhasePreflightError, publishEntry, reindex as libraryReindex, resolvePhase, resolvePipelineChoice, stringifySimpleYaml, switchPipeline, validatePhaseOutput, writeLibraryConfig, } from "../core/index.js";
18
+ import { buildPhasePrompt, buildSkillPrompt, buildValidationSummary, canonicalPath, completeValidatedPhase, computePerPhaseTotals, computeTotals, createEmptyStatus, DEFAULT_PIPELINE_PATH, deriveSlug, discoverLibrary, getNextEligiblePhase, getPipelineLabel, getWorkspaceState, isValidSlug, isWithinPathResolved, listEntries, listSkillNames, loadCodecartoConfig, loadUsage, loadYamlFile, normalizeForComparison, PACKAGE_VERSION, packagedWorkspaceDir, pathExists, PhasePreflightError, publishEntry, reindex as libraryReindex, resolvePhase, resolvePipelineChoice, stringifySimpleYaml, switchPipeline, validatePhaseOutput, writeLibraryConfig, } from "../core/index.js";
19
19
  import { initLibrary } from "../core/library.js";
20
20
  import { loadUserConfig, resolveUserConfigPath } from "../core/orchestrator-config.js";
21
21
  import { writeDashboard } from "../extensions/codecarto/dashboard-writer.js";
@@ -325,7 +325,7 @@ function buildGenerationFromArg(model_metadata) {
325
325
  out.notes = m.notes;
326
326
  return out;
327
327
  }
328
- async function readSpecArg(args) {
328
+ async function readSpecArg(args, allowedRoots = []) {
329
329
  if (typeof args.spec === "string" && args.spec.length > 0)
330
330
  return args.spec;
331
331
  if (typeof args.spec_path === "string" && args.spec_path.length > 0) {
@@ -335,6 +335,16 @@ async function readSpecArg(args) {
335
335
  if (!(await pathExists(args.spec_path))) {
336
336
  throw new McpError(ErrorCode.InvalidParams, `spec_path does not exist: ${args.spec_path}`);
337
337
  }
338
+ // Enforce path containment: spec_path must be within an allowed root
339
+ // (cwd's .codecarto/ or the configured library path) to prevent
340
+ // arbitrary file reads.
341
+ if (allowedRoots.length > 0) {
342
+ const resolvedSpecPath = await canonicalPath(args.spec_path);
343
+ const withinAllowed = await Promise.all(allowedRoots.map((root) => isWithinPathResolved(resolvedSpecPath, root)));
344
+ if (!withinAllowed.some((result) => result)) {
345
+ throw new McpError(ErrorCode.InvalidParams, `spec_path must be within the workspace (.codecarto/) or the configured library path. Got: ${args.spec_path}`);
346
+ }
347
+ }
338
348
  return readFile(args.spec_path, "utf8");
339
349
  }
340
350
  throw new McpError(ErrorCode.InvalidParams, "Either spec (inline content) or spec_path (absolute file path) is required");
@@ -375,7 +385,12 @@ export async function handlePublish(args) {
375
385
  if (!marker) {
376
386
  throw new McpError(ErrorCode.InvalidParams, `No CodeCartographer library at ${libraryPath} (missing .codecarto-library marker). Create one before publishing.`);
377
387
  }
378
- const spec = await readSpecArg(args);
388
+ // Build allowed roots for spec_path containment: workspace .codecarto/ and library path
389
+ const allowedRoots = [libraryPath];
390
+ if (typeof args.cwd === "string" && args.cwd.trim() !== "") {
391
+ allowedRoots.push(join(args.cwd.trim(), ".codecarto"));
392
+ }
393
+ const spec = await readSpecArg(args, allowedRoots);
379
394
  if (typeof args.source_repo !== "string" || args.source_repo.trim() === "") {
380
395
  throw new McpError(ErrorCode.InvalidParams, "source_repo is required");
381
396
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codecartographer-pi",
3
- "version": "0.12.9",
3
+ "version": "0.12.11",
4
4
  "mcpName": "io.github.HuginnIndustries/codecartographer",
5
5
  "description": "Evidence-backed reverse engineering and human-gated software planning for Pi and MCP coding agents.",
6
6
  "type": "module",