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.
package/dist/core/utils.d.ts
CHANGED
|
@@ -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;
|
package/dist/core/utils.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|