@agent-pattern-labs/iso-context
Advanced tools
+36
-14
@@ -1,3 +0,3 @@ | ||
| import { existsSync, readFileSync, statSync } from "node:fs"; | ||
| import { resolve } from "node:path"; | ||
| import { existsSync, readFileSync, realpathSync, statSync } from "node:fs"; | ||
| import { isAbsolute, relative, resolve, sep } from "node:path"; | ||
| import { isJsonObject } from "./json.js"; | ||
@@ -116,2 +116,16 @@ const DEFAULT_CHARS_PER_TOKEN = 4; | ||
| } | ||
| const realRoot = realpathSync(root); | ||
| const realFile = realpathSync(absolutePath); | ||
| const relativeRealPath = relative(realRoot, realFile); | ||
| if (relativeRealPath === ".." | ||
| || relativeRealPath.startsWith(`..${sep}`) | ||
| || isAbsolute(relativeRealPath)) { | ||
| issues.push({ | ||
| kind: "path-outside-root", | ||
| severity: "error", | ||
| path: file.path, | ||
| message: `context path "${file.path}" resolves outside the context root`, | ||
| }); | ||
| return { ...base, exists: true }; | ||
| } | ||
| const stat = statSync(absolutePath); | ||
@@ -188,7 +202,7 @@ if (!stat.isFile()) { | ||
| ...base, | ||
| description: base.description ?? parent.description, | ||
| files: mergeFiles(parent.files, base.files), | ||
| tokenBudget: parent.tokenBudget, | ||
| charsPerToken: parent.charsPerToken, | ||
| notes: unique([...parent.notes, ...base.notes]), | ||
| description: parent.description ?? base.description, | ||
| files: mergeFiles(base.files, parent.files), | ||
| tokenBudget: parent.tokenBudget ?? base.tokenBudget, | ||
| charsPerToken: parent.charsPerToken ?? base.charsPerToken, | ||
| notes: unique([...base.notes, ...parent.notes]), | ||
| }; | ||
@@ -259,5 +273,3 @@ } | ||
| if (typeof value === "string") { | ||
| if (!value.trim()) | ||
| throw new Error(`${path} must not be empty`); | ||
| return value.trim(); | ||
| return safeRelativePath(value, path); | ||
| } | ||
@@ -268,3 +280,3 @@ return normalizeFileSpec(value, path); | ||
| if (typeof value === "string") { | ||
| return { path: value, required: true, notes: [] }; | ||
| return { path: safeRelativePath(value, path), required: true, notes: [] }; | ||
| } | ||
@@ -276,5 +288,3 @@ return normalizeFileSpec(value, path); | ||
| throw new Error(`${path} must be a string or JSON object`); | ||
| const rawPath = requireString(value.path, `${path}.path`); | ||
| if (!rawPath) | ||
| throw new Error(`${path}.path must not be empty`); | ||
| const rawPath = safeRelativePath(requireString(value.path, `${path}.path`), `${path}.path`); | ||
| const spec = { | ||
@@ -293,2 +303,14 @@ path: rawPath, | ||
| } | ||
| function safeRelativePath(value, label) { | ||
| const path = value.trim(); | ||
| const portable = path.replace(/\\/g, "/"); | ||
| if (!path | ||
| || isAbsolute(path) | ||
| || portable.startsWith("/") | ||
| || /^[A-Za-z]:\//.test(portable) | ||
| || portable.split("/").some((part) => part === "..")) { | ||
| throw new Error(`${label} must be a relative path inside the context root`); | ||
| } | ||
| return path; | ||
| } | ||
| function isContextPolicy(input) { | ||
@@ -295,0 +317,0 @@ return isJsonObject(input) && Array.isArray(input.bundles); |
+1
-1
@@ -70,3 +70,3 @@ export type JsonPrimitive = string | number | boolean | null; | ||
| } | ||
| export type ContextIssueKind = "missing-required-file" | "not-a-file" | "read-error" | "file-over-budget" | "bundle-over-budget"; | ||
| export type ContextIssueKind = "missing-required-file" | "path-outside-root" | "not-a-file" | "read-error" | "file-over-budget" | "bundle-over-budget"; | ||
| export interface ContextIssue { | ||
@@ -73,0 +73,0 @@ kind: ContextIssueKind; |
+1
-1
| { | ||
| "name": "@agent-pattern-labs/iso-context", | ||
| "version": "0.1.1", | ||
| "version": "0.1.2", | ||
| "description": "Deterministic context bundle planning for AI-agent workflows: select files, estimate tokens, check budgets, and render prompt context without model calls.", | ||
@@ -5,0 +5,0 @@ "license": "MIT", |
+7
-1
@@ -82,3 +82,7 @@ # @agent-pattern-labs/iso-context | ||
| - `extends` supports one parent or an array of parents. | ||
| - Parent files and notes are inherited before child values. | ||
| - Parents are applied in declared order: a later parent overrides an earlier | ||
| parent's description, budgets, and fields on duplicate file paths when it | ||
| explicitly defines them. Omitted values do not erase earlier values. | ||
| - Parent files and notes are inherited before child values, and the child has | ||
| final precedence over every parent. | ||
| - Re-declaring the same file path in a child overrides scalar file fields while | ||
@@ -91,2 +95,4 @@ preserving the original file order. | ||
| with `charsPerToken` defaulting to `4`. | ||
| - Context file paths must remain relative to the configured root, including | ||
| after resolving symlinks. | ||
@@ -93,0 +99,0 @@ ## Library API |
40626
3.46%882
2.56%130
4.84%