@hegemonart/get-design-done 1.59.9 → 1.60.1

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.
@@ -5,14 +5,14 @@
5
5
  },
6
6
  "metadata": {
7
7
  "description": "Get Design Done — 5-stage agent-orchestrated design pipeline (Brief → Explore → Plan → Design → Verify) for AI coding agents. 64 agents, 96 skills, 39 connection integrations, two MCP servers, opt-in SQLite state backbone, bidirectional Figma write-back, and a reflector-driven self-improvement loop. Cross-runtime install for Claude Code, Codex, Cursor, OpenCode, Gemini, and more.",
8
- "version": "1.59.9"
8
+ "version": "1.60.1"
9
9
  },
10
10
  "plugins": [
11
11
  {
12
12
  "name": "get-design-done",
13
13
  "source": "./",
14
14
  "description": "Agent-orchestrated 5-stage design pipeline (Brief → Explore → Plan → Design → Verify) for AI coding agents. 64 specialized agents, 96 skills, 39 connection integrations (Figma, Refero, Preview, Storybook, Chromatic, Graphify, Linear, Jira, Notion, …), bidirectional Figma write-back, queryable intel store, opt-in SQLite state backbone, and a reflector-driven self-improvement loop. Two MCP servers (gdd-state for typed STATE mutators, gdd-mcp for 13 read-only project-priming tools), tier-aware routing with cost telemetry, and defense-in-depth hooks (protected paths, MCP circuit breaker, injection scanner, budget enforcer). Cross-runtime install for Claude Code, Codex, Cursor, OpenCode, Gemini, Copilot, and more.",
15
- "version": "1.59.9",
15
+ "version": "1.60.1",
16
16
  "author": {
17
17
  "name": "hegemonart"
18
18
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "get-design-done",
3
3
  "short_name": "gdd",
4
- "version": "1.59.9",
4
+ "version": "1.60.1",
5
5
  "description": "Agent-orchestrated 5-stage design pipeline (Brief → Explore → Plan → Design → Verify) for AI coding agents. 64 specialized agents, 96 skills, 39 connection integrations (Figma, Refero, Preview, Storybook, Chromatic, Graphify, Linear, Jira, Notion, …), bidirectional Figma write-back, queryable intel store for O(1) design-surface lookups, opt-in SQLite state backbone, and a reflector-driven self-improvement loop. Two MCP servers (`gdd-state` for typed STATE mutators, `gdd-mcp` for 13 read-only project-priming tools), tier-aware agent routing with cost telemetry, defense-in-depth hooks (protected paths, MCP circuit breaker, injection scanner, budget enforcer), and a cross-runtime install layer for Claude Code, Codex, Cursor, OpenCode, Gemini, Copilot, and more.",
6
6
  "author": {
7
7
  "name": "hegemonart",
package/CHANGELOG.md CHANGED
@@ -4,6 +4,48 @@ All notable changes to get-design-done are documented here. Versions follow [sem
4
4
 
5
5
  ---
6
6
 
7
+ ## [1.60.1] - 2026-06-10
8
+
9
+ **Security hardening** - two HIGH-severity vulnerabilities closed before the upcoming rebrand copies the foundation layer across every runtime. Both were reachable by a prompt-injected agent, undercutting the trust boundary the plugin's own scanners exist to defend. Each fix ships with failing-then-passing regression tests; an independent adversarial audit confirmed both vectors are dead with no surviving bypass.
10
+
11
+ ### Security
12
+
13
+ - **Path traversal in the `gdd_intel_get` MCP tool (HARDEN-01).** A `slice_id` carrying `../`, an absolute path, or a path separator flowed unsanitized into a file read and could escape `.design/intel/` to return any `.json` file's contents (cloud credentials, config) to the caller. The intel store now rejects any non-basename `slice_id` and enforces a `resolve` + `startsWith(root + sep)` containment check before touching disk. Separately, the `gdd-mcp` server **now validates every `tools/call` argument against the advertised per-tool JSON schema** at the dispatcher - previously raw arguments were passed straight to handlers with no validation.
14
+ - **Protected-paths guard canonicalization bypass (HARDEN-02).** The hook that blocks edits to `hooks/**`, `skills/**`, `.git/**`, and `.claude/settings.json` matched a non-canonical path string, so a Windows forward-slash absolute path (`C:/…`) or a `../<cwd-basename>/…` relative re-entry slipped past the block. The guard now canonicalizes candidates with `path.resolve` + `path.relative` (forward-slash drive letters included), mandatorily resolves the nearest existing ancestor with `realpathSync` to catch symlinked-ancestor-of-a-new-file escapes, and folds case on Windows/macOS. The two bypass vectors (previously untested) now have explicit regression coverage.
15
+
16
+ ### Breaking changes
17
+
18
+ None.
19
+
20
+ 5,139/5,139 tests pass.
21
+
22
+ ---
23
+
24
+ ## [1.60.0] - 2026-06-10
25
+
26
+ **Foundation & Honesty** - the subtract-first base the v2.0 work and the upcoming rebrand depend on. Make the catalog enumerable and the capability claims machine-checked *before* building or renaming anything. A pre-flight audit found the catalog already clean (0 content-duplicate skills, 0 orphan skills or agents, a perfect manifest to template to generated bijection, and every count and capability claim already tracing to source), so this release does not manufacture cuts - it removes genuinely dead code and adds the guards that lock the clean state in.
27
+
28
+ ### Removed
29
+
30
+ - **Dead jsdom/puppeteer detection scaffolding** from `gdd-detect`. The CLI carried soft `try-require` probes for a `jsdom` "DOM-aware" engine and a `puppeteer` URL engine, plus `--fast`/`--puppeteer` flags - none of which were ever wired (the engine is and was pure regex over local files). 1.59.8 stopped the misleading mode label; this release deletes the dead probes and flags outright and rewrites the comments/help to the regex-only reality. `gdd-detect --help` now matches what it does. (A real DOM/URL engine is planned separately; this removal is reversed then.)
31
+
32
+ ### Added
33
+
34
+ - **Catalog-integrity validator** (`validate:catalog`, wired into CI): exact content-hash duplicate detection across skill templates and agents, near-duplicate description detection (calibrated above the existing distinct-but-parallel writer family, so a future copy-paste clone is caught), a manifest to template to generated three-way bijection check (no orphans in any direction), and description-length sanity. Passes on the current catalog; fails if bloat, dupes, or orphans are introduced later.
35
+ - **Capability-honesty assertion** (in the same CI gate): every MCP server named in the plugin/marketplace manifests resolves to a real `sdk/mcp/<server>/server.ts`, and the advertised read-only MCP-tool count is derived from the actual `gdd_*.ts` tool files rather than hand-typed. Combined with the existing source-derived feature counts, the 64 agents / 96 skills / 39 integrations / 13 MCP-tools / 2 MCP-servers claims are now machine-verified.
36
+
37
+ ### Notes
38
+
39
+ - Catalog audited clean - no skills or agents were merged or deleted. The integrity guard exists to keep it that way.
40
+
41
+ ### Breaking changes
42
+
43
+ None.
44
+
45
+ 5,102/5,102 tests pass.
46
+
47
+ ---
48
+
7
49
  ## [1.59.9] - 2026-06-10
8
50
 
9
51
  New-model-family readiness and cost truth (audit `.planning/audits/SELF-AUDIT-v1.59.7.md` §4). A new or unknown Anthropic model previously degraded cost accounting silently - billed at $0 or the sonnet rate and mis-attributed to the sonnet tier. This release makes unknown models loud and conservative, handles the 1M-context `[1m]` variant, and records context-window size in the model registry.
@@ -38,7 +38,75 @@ function findPackageRoot(startDir) {
38
38
 
39
39
  const REPO_ROOT = findPackageRoot(__dirname) || path.resolve(__dirname, '..');
40
40
 
41
- const { matches } = require(path.join(REPO_ROOT, 'scripts', 'lib', 'glob-match.cjs'));
41
+ const { matches, defaultNocase } = require(path.join(REPO_ROOT, 'scripts', 'lib', 'glob-match.cjs'));
42
+
43
+ /**
44
+ * HARDEN-02: Canonicalize a candidate path to a cwd-relative form before glob
45
+ * matching, defeating equivalent spellings of a protected file:
46
+ * - POSIX absolute `/abs/cwd/hooks/x.js`
47
+ * - backslash drive `C:\cwd\hooks\x.js`
48
+ * - forward-slash drive `C:/cwd/hooks/x.js` (was the bypass — backslash-only detector)
49
+ * - `../<cwd-basename>/hooks/x.js` reentry (was the bypass — raw string never matched)
50
+ * - symlink / symlinked ANCESTOR redirection into a protected dir (incl. NEW files)
51
+ *
52
+ * Returns a forward-slash cwd-relative string for IN-cwd targets, or the
53
+ * sentinel `null` for targets that resolve OUTSIDE cwd (out-of-repo edits are
54
+ * not this guard's concern and must not be false-blocked).
55
+ */
56
+ function canonicalizeCandidate(cand, cwd) {
57
+ // 1. Recognize absolute paths robustly across platforms. `path.isAbsolute`
58
+ // on a backslash-normalized copy catches POSIX `/…` and native drive
59
+ // paths; the drive-letter regex is the Windows-on-POSIX fallback so a
60
+ // `C:/…` / `C:\…` spelling is treated as absolute even when the test
61
+ // process runs on Linux.
62
+ const normalized = cand.replace(/\\/g, '/');
63
+ const isAbs = path.isAbsolute(cand)
64
+ || path.isAbsolute(normalized)
65
+ || /^[A-Za-z]:[\\/]/.test(cand);
66
+
67
+ const abs = isAbs ? normalized : path.resolve(cwd, cand);
68
+
69
+ // 2. Canonicalize through symlinks — MANDATORY, for existing AND new targets.
70
+ // Full-path realpath throws (ENOENT) on a not-yet-existing write target,
71
+ // so walk UP to the nearest existing ancestor, realpath THAT, then re-join
72
+ // the non-existent tail. This resolves a symlinked ancestor dir of a new
73
+ // file (the write-new-file symlink bypass). Any unexpected I/O error falls
74
+ // back to the plain resolved path — the hook must never hard-fail.
75
+ let canonicalAbs = abs;
76
+ try {
77
+ canonicalAbs = fs.realpathSync(abs);
78
+ } catch (e) {
79
+ if (e && e.code === 'ENOENT') {
80
+ try {
81
+ let ancestor = path.dirname(abs);
82
+ const tail = [path.basename(abs)];
83
+ // Walk up until an existing ancestor is found (or filesystem root).
84
+ // Guard the loop against an unbounded climb.
85
+ for (let i = 0; i < 64; i++) {
86
+ if (fs.existsSync(ancestor)) break;
87
+ const parent = path.dirname(ancestor);
88
+ if (parent === ancestor) break;
89
+ tail.unshift(path.basename(ancestor));
90
+ ancestor = parent;
91
+ }
92
+ const realAncestor = fs.realpathSync(ancestor);
93
+ canonicalAbs = path.join(realAncestor, ...tail);
94
+ } catch {
95
+ canonicalAbs = abs;
96
+ }
97
+ } else {
98
+ canonicalAbs = abs;
99
+ }
100
+ }
101
+
102
+ // 3. cwd-relative canonical form.
103
+ const rel = path.relative(cwd, canonicalAbs).replace(/\\/g, '/');
104
+
105
+ // 4. Out-of-cwd sentinel — these are NOT matched against repo-internal globs.
106
+ if (rel === '..' || rel.startsWith('../') || path.isAbsolute(rel)) return null;
107
+
108
+ return rel;
109
+ }
42
110
 
43
111
  function loadProtectedPaths(cwd) {
44
112
  const defaultFile = path.join(REPO_ROOT, 'reference', 'protected-paths.default.json');
@@ -248,10 +316,10 @@ async function main() {
248
316
 
249
317
  for (const cand of candidates) {
250
318
  if (!cand) continue;
251
- const rel = cand.startsWith('/') || /^[A-Z]:\\/i.test(cand)
252
- ? path.relative(cwd, cand).replace(/\\/g, '/')
253
- : cand.replace(/\\/g, '/');
254
- const r = matches(rel, protectedPaths);
319
+ const rel = canonicalizeCandidate(cand, cwd);
320
+ // Out-of-cwd targets (sentinel null) are not this guard's concern.
321
+ if (rel === null) continue;
322
+ const r = matches(rel, protectedPaths, { nocase: defaultNocase() });
255
323
  if (r.matched) {
256
324
  try {
257
325
  require('./_hook-emit.js').emitHookFired('gdd-protected-paths', 'block', {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hegemonart/get-design-done",
3
- "version": "1.59.9",
3
+ "version": "1.60.1",
4
4
  "description": "A design-quality pipeline for AI coding agents: brief, explore, plan, design, and verify UI work against your design system.",
5
5
  "author": "Hegemon",
6
6
  "homepage": "https://github.com/hegemonart/get-design-done",
@@ -84,6 +84,7 @@
84
84
  "validate:composition-graph": "node scripts/validate-composition-graph.cjs",
85
85
  "validate:design-context": "node scripts/validate-design-context.cjs",
86
86
  "validate:skill-frontmatter": "node scripts/validate-skill-frontmatter.cjs",
87
+ "validate:catalog": "node scripts/validate-catalog-integrity.cjs",
87
88
  "build:skill-graph": "node scripts/generate-skill-graph.cjs",
88
89
  "build:skill-graph:check": "node scripts/generate-skill-graph.cjs --check",
89
90
  "sync:rule-catalogue": "node scripts/sync-rule-catalogue.cjs --check",
@@ -1,38 +1,35 @@
1
1
  'use strict';
2
- // Phase 41 — gdd-detect CLI. Dep-free by default (regex-fast). The DOM-aware (jsdom) and URL
3
- // (puppeteer) paths are SOFT optionals loaded via try-require never a package.json dependency, so
4
- // the SC#10 network-isolation scan stays clean and the plugin keeps its zero-runtime-dep guarantee.
2
+ // Phase 41 — gdd-detect CLI. A regex anti-pattern scanner over LOCAL files: it walks a file or
3
+ // directory and runs each BAN-NN rule's matcher against the file text. There is exactly one engine
4
+ // (regex over file content) and it never touches the network or any optional dependency, so the
5
+ // SC#10 network-isolation scan stays clean and the plugin keeps its zero-runtime-dep guarantee.
5
6
  //
6
- // gdd-detect <path> [--json] [--fast] [--rule BAN-NN] [--puppeteer]
7
+ // gdd-detect <path> [--json] [--rule BAN-NN]
7
8
  //
8
9
  // Exit codes: 0 = clean · 2 = findings · 1 = invocation error.
9
10
 
10
11
  const engine = require('./engine.cjs');
11
12
 
12
- const HELP = `gdd-detect — scan HTML/CSS/JSX for GDD anti-patterns (BAN-NN).
13
+ const HELP = `gdd-detect — scan local HTML/CSS/JSX for GDD anti-patterns (BAN-NN).
13
14
 
14
15
  Usage:
15
16
  gdd-detect <path> [options]
16
17
 
17
18
  Arguments:
18
- <path> A file or directory (scanned recursively), or a http(s):// URL (needs --puppeteer).
19
+ <path> A file or directory (scanned recursively). Regex anti-pattern scan over local files.
19
20
 
20
21
  Options:
21
22
  --json Machine-readable JSON output.
22
- --fast Regex-only; do not load jsdom even if present.
23
23
  --rule <BAN-NN> Run a single rule (e.g. --rule BAN-08).
24
- --puppeteer Allow scanning a URL via Puppeteer (an optional, separately-installed dependency).
25
24
  -h, --help This help.
26
25
 
27
26
  Exit codes: 0 clean · 2 findings · 1 invocation error.`;
28
27
 
29
28
  function parseArgs(argv) {
30
- const opts = { path: null, json: false, fast: false, rule: null, puppeteer: false, help: false };
29
+ const opts = { path: null, json: false, rule: null, help: false };
31
30
  for (let i = 0; i < argv.length; i++) {
32
31
  const a = argv[i];
33
32
  if (a === '--json') opts.json = true;
34
- else if (a === '--fast') opts.fast = true;
35
- else if (a === '--puppeteer') opts.puppeteer = true;
36
33
  else if (a === '-h' || a === '--help') opts.help = true;
37
34
  else if (a === '--rule') opts.rule = argv[++i] || null;
38
35
  else if (a.startsWith('--rule=')) opts.rule = a.slice('--rule='.length);
@@ -46,22 +43,11 @@ function isUrl(p) {
46
43
  }
47
44
 
48
45
  /**
49
- * Select the detection engine. Returns { mode, warning }.
50
- *
51
- * There is exactly one engine path: regex over file text (see engine.cjs#run, which takes no
52
- * jsdom/DOM parameter and is byte-identical whether or not jsdom is installed). So the truthful
53
- * mode is always 'regex-fast'. We still probe jsdom (unless --fast) to surface a one-line hint
54
- * that a DOM-aware path is not wired in this build — but we no longer claim a 'dom-aware' mode the
55
- * engine does not have.
46
+ * Report the active engine. There is exactly one path: regex over file text (see engine.cjs#run).
47
+ * Returns { mode } so callers and the --json report can label output truthfully.
56
48
  */
57
- function selectEngine(opts, requireFn) {
58
- if (opts.fast) return { mode: 'regex-fast', warning: null };
59
- let hasJsdom = false;
60
- try { requireFn('jsdom'); hasJsdom = true; } catch { hasJsdom = false; }
61
- // jsdom presence does not change the engine — only emit a hint when it's absent, and never
62
- // promise a mode we can't deliver.
63
- const warning = hasJsdom ? null : 'jsdom not installed — running regex-fast (the only wired mode; a DOM-aware path is not implemented). Pass --fast to silence this.';
64
- return { mode: 'regex-fast', warning };
49
+ function selectEngine() {
50
+ return { mode: 'regex-fast' };
65
51
  }
66
52
 
67
53
  function renderHuman(result, mode) {
@@ -78,14 +64,13 @@ function renderHuman(result, mode) {
78
64
 
79
65
  /**
80
66
  * @param {string[]} argv process.argv.slice(2)
81
- * @param {{ cwd?: string, log?: fn, err?: fn, requireFn?: fn }} [io] injectable for tests
67
+ * @param {{ cwd?: string, log?: fn, err?: fn }} [io] injectable for tests
82
68
  * @returns {number} exit code
83
69
  */
84
70
  function main(argv, io) {
85
71
  const o = io || {};
86
72
  const log = o.log || ((s) => process.stdout.write(s + '\n'));
87
73
  const err = o.err || ((s) => process.stderr.write(s + '\n'));
88
- const requireFn = o.requireFn || require;
89
74
  const cwd = o.cwd || process.cwd();
90
75
  const opts = parseArgs(argv);
91
76
 
@@ -93,18 +78,13 @@ function main(argv, io) {
93
78
  if (!opts.path) { err('gdd-detect: missing <path>. See --help.'); return 1; }
94
79
  if (opts.rule && !/^BAN-\d{2}$/i.test(opts.rule)) { err(`gdd-detect: --rule expects a BAN-NN id (got "${opts.rule}").`); return 1; }
95
80
 
96
- // URL path Puppeteer (optional, separately installed). Never a stack trace.
81
+ // URL path is not wired: this is a regex scanner over local files. Never a stack trace.
97
82
  if (isUrl(opts.path)) {
98
- if (!opts.puppeteer) { err('gdd-detect: scanning a URL requires --puppeteer. Pass --puppeteer (and `npm i -D puppeteer`) to enable URL scans.'); return 1; }
99
- let hasPuppeteer = false;
100
- try { requireFn('puppeteer'); hasPuppeteer = true; } catch { hasPuppeteer = false; }
101
- if (!hasPuppeteer) { err('gdd-detect: --puppeteer given but puppeteer is not installed. Install it with `npm i -D puppeteer` (it stays an optional dependency).'); return 1; }
102
83
  err('gdd-detect: URL scanning is not wired in this build; clone the page locally and scan the files instead.');
103
84
  return 1;
104
85
  }
105
86
 
106
- const { mode, warning } = selectEngine(opts, requireFn);
107
- if (warning && !opts.json) err('gdd-detect: ' + warning);
87
+ const { mode } = selectEngine();
108
88
 
109
89
  let result;
110
90
  try { result = engine.run(opts.path, { ruleId: opts.rule, cwd }); }
@@ -1,8 +1,8 @@
1
1
  'use strict';
2
- // Phase 41 — gdd-detect engine. Pure, dep-free (regex-fast path). Walks a path, runs each rule's
3
- // matcher against file content, returns structured findings. The DOM-aware (jsdom) + URL (puppeteer)
4
- // paths are layered on in cli.cjs via soft try-require; the engine itself never touches the network
5
- // or any optional dependency — so the SC#10 network-isolation scan stays clean.
2
+ // Phase 41 — gdd-detect engine. Pure, dep-free regex engine over file content. Walks a path, runs
3
+ // each rule's matcher against the text of each scannable file, and returns structured findings. The
4
+ // engine never touches the network or any optional dependency so the SC#10 network-isolation scan
5
+ // stays clean.
6
6
 
7
7
  const fs = require('node:fs');
8
8
  const path = require('node:path');
@@ -3,9 +3,24 @@
3
3
  * scripts/lib/glob-match.cjs — tiny dependency-free glob matcher.
4
4
  * Supports: **, *, ?, and literal segments. Not a full minimatch implementation,
5
5
  * but covers the patterns used in reference/protected-paths.default.json.
6
+ *
7
+ * Case-sensitivity tracks the host filesystem by default: case-INsensitive on
8
+ * win32/darwin (so `HOOKS/x` matches `hooks/**`), case-sensitive elsewhere.
9
+ * Callers can override via `opts.nocase` (used by the protected-paths suite to
10
+ * exercise BOTH branches explicitly on a case-sensitive Linux CI runner).
6
11
  */
7
12
 
8
- function globToRegex(glob) {
13
+ /**
14
+ * The platform-derived default for case-insensitive matching. Exposed so the
15
+ * protected-paths hook and the tests reference the SAME decision rather than
16
+ * duplicating the win32||darwin check.
17
+ */
18
+ function defaultNocase() {
19
+ return process.platform === 'win32' || process.platform === 'darwin';
20
+ }
21
+
22
+ function globToRegex(glob, opts = {}) {
23
+ const nocase = opts.nocase !== undefined ? opts.nocase : defaultNocase();
9
24
  // Normalize separators
10
25
  const g = glob.replace(/\\/g, '/');
11
26
  let re = '^';
@@ -42,16 +57,18 @@ function globToRegex(glob) {
42
57
  i++;
43
58
  }
44
59
  re += '$';
45
- return new RegExp(re);
60
+ // Use the `i` flag for case-insensitivity rather than lowercasing inputs,
61
+ // which would corrupt the returned `pattern` string callers rely on.
62
+ return new RegExp(re, nocase ? 'i' : '');
46
63
  }
47
64
 
48
- function matches(filepath, globList) {
65
+ function matches(filepath, globList, opts = {}) {
49
66
  const norm = String(filepath).replace(/\\/g, '/').replace(/^\.\//, '');
50
67
  for (const g of globList) {
51
- const re = globToRegex(g);
68
+ const re = globToRegex(g, opts);
52
69
  if (re.test(norm)) return { matched: true, pattern: g };
53
70
  }
54
71
  return { matched: false };
55
72
  }
56
73
 
57
- module.exports = { matches, globToRegex };
74
+ module.exports = { matches, globToRegex, defaultNocase };
Binary file
@@ -1860,12 +1860,45 @@ var require_intel_store = __commonJS({
1860
1860
  this.dir = dir;
1861
1861
  }
1862
1862
  };
1863
+ var IntelInvalidSliceIdError = class extends Error {
1864
+ constructor(sliceId) {
1865
+ const shown = typeof sliceId === "string" ? sliceId : String(sliceId);
1866
+ super("invalid slice_id: " + JSON.stringify(shown));
1867
+ this.name = "IntelInvalidSliceIdError";
1868
+ this.code = "invalid_slice_id";
1869
+ this.sliceId = sliceId;
1870
+ }
1871
+ };
1872
+ function validateSliceId(sliceId) {
1873
+ if (typeof sliceId !== "string" || sliceId.length === 0) {
1874
+ throw new IntelInvalidSliceIdError(sliceId);
1875
+ }
1876
+ if (sliceId.indexOf("\0") !== -1) {
1877
+ throw new IntelInvalidSliceIdError(sliceId);
1878
+ }
1879
+ if (sliceId.indexOf("/") !== -1 || sliceId.indexOf("\\") !== -1) {
1880
+ throw new IntelInvalidSliceIdError(sliceId);
1881
+ }
1882
+ if (sliceId === "." || sliceId === ".." || sliceId.includes("..")) {
1883
+ throw new IntelInvalidSliceIdError(sliceId);
1884
+ }
1885
+ if (path.isAbsolute(sliceId)) {
1886
+ throw new IntelInvalidSliceIdError(sliceId);
1887
+ }
1888
+ }
1863
1889
  async function readSlice2(rootDir, sliceId) {
1890
+ validateSliceId(sliceId);
1864
1891
  const dir = path.join(rootDir, ".design", "intel");
1865
1892
  if (!fs.existsSync(dir)) {
1866
1893
  throw new IntelNotFoundError(dir);
1867
1894
  }
1868
1895
  const file = path.join(dir, sliceId + ".json");
1896
+ const resolvedDir = path.resolve(dir);
1897
+ const resolvedFile = path.resolve(file);
1898
+ const withSep = resolvedDir.endsWith(path.sep) ? resolvedDir : resolvedDir + path.sep;
1899
+ if (!resolvedFile.startsWith(withSep)) {
1900
+ throw new IntelInvalidSliceIdError(sliceId);
1901
+ }
1869
1902
  if (!fs.existsSync(file)) return null;
1870
1903
  const body = await fs.promises.readFile(file, "utf8");
1871
1904
  return JSON.parse(body);
@@ -1883,7 +1916,13 @@ var require_intel_store = __commonJS({
1883
1916
  }
1884
1917
  return ids;
1885
1918
  }
1886
- module2.exports = { readSlice: readSlice2, listSlices, IntelNotFoundError };
1919
+ module2.exports = {
1920
+ readSlice: readSlice2,
1921
+ listSlices,
1922
+ validateSliceId,
1923
+ IntelNotFoundError,
1924
+ IntelInvalidSliceIdError
1925
+ };
1887
1926
  }
1888
1927
  });
1889
1928
 
@@ -2035,6 +2074,7 @@ __export(server_exports, {
2035
2074
  module.exports = __toCommonJS(server_exports);
2036
2075
  var import_node_fs5 = require("node:fs");
2037
2076
  var import_node_path5 = require("node:path");
2077
+ var import_ajv = __toESM(require("ajv"));
2038
2078
  var import_server = require("@modelcontextprotocol/sdk/server/index.js");
2039
2079
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
2040
2080
  var import_types6 = require("@modelcontextprotocol/sdk/types.js");
@@ -3427,6 +3467,24 @@ function buildServer() {
3427
3467
  const tools = loadTools();
3428
3468
  const byName = /* @__PURE__ */ new Map();
3429
3469
  for (const t of tools) byName.set(t.name, t);
3470
+ const ajv = new import_ajv.default({ allErrors: true, strict: false });
3471
+ const PASS_THROUGH = (() => {
3472
+ const v = (() => true);
3473
+ v.errors = null;
3474
+ return v;
3475
+ })();
3476
+ const validators = /* @__PURE__ */ new Map();
3477
+ for (const t of tools) {
3478
+ try {
3479
+ validators.set(t.name, ajv.compile(t.inputSchema));
3480
+ } catch (err) {
3481
+ const msg = err instanceof Error ? err.message : String(err);
3482
+ console.error(
3483
+ `[gdd-mcp] schema compile failed for ${t.name}; tool degraded to permissive validation: ${msg}`
3484
+ );
3485
+ validators.set(t.name, PASS_THROUGH);
3486
+ }
3487
+ }
3430
3488
  const server = new import_server.Server(
3431
3489
  { name: SERVER_NAME, version: SERVER_VERSION },
3432
3490
  {
@@ -3469,6 +3527,26 @@ function buildServer() {
3469
3527
  structuredContent: { success: false, error: payload.error }
3470
3528
  };
3471
3529
  }
3530
+ const validate = validators.get(toolName);
3531
+ if (validate !== void 0) {
3532
+ const argsObj = args ?? {};
3533
+ if (!validate(argsObj)) {
3534
+ const detail = ajv.errorsText(validate.errors, { dataVar: "input" });
3535
+ const payload = toToolError(
3536
+ new Error(`input validation failed: ${detail}`)
3537
+ );
3538
+ return {
3539
+ isError: true,
3540
+ content: [
3541
+ {
3542
+ type: "text",
3543
+ text: JSON.stringify({ success: false, error: payload.error })
3544
+ }
3545
+ ],
3546
+ structuredContent: { success: false, error: payload.error }
3547
+ };
3548
+ }
3549
+ }
3472
3550
  let response;
3473
3551
  try {
3474
3552
  response = await tool.handle(args ?? {});
@@ -38,6 +38,8 @@
38
38
  import { readFileSync, existsSync } from 'node:fs';
39
39
  import { dirname, join, resolve } from 'node:path';
40
40
 
41
+ import Ajv, { type ValidateFunction } from 'ajv';
42
+
41
43
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
42
44
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
43
45
  import {
@@ -186,6 +188,33 @@ export function buildServer(): Server {
186
188
  const byName: Map<string, LoadedTool> = new Map();
187
189
  for (const t of tools) byName.set(t.name, t);
188
190
 
191
+ // HARDEN-01 (Task 2): compile an ajv validator per tool from its advertised
192
+ // input JSON Schema, so every tools/call argument is validated against the
193
+ // tool's contract BEFORE the handler runs. `strict:false` tolerates the
194
+ // Draft-07 keywords our schemas use; `allErrors` yields complete messages.
195
+ const ajv = new Ajv({ allErrors: true, strict: false });
196
+ const PASS_THROUGH: ValidateFunction = (() => {
197
+ const v = (() => true) as ValidateFunction;
198
+ v.errors = null;
199
+ return v;
200
+ })();
201
+ const validators: Map<string, ValidateFunction> = new Map();
202
+ for (const t of tools) {
203
+ try {
204
+ validators.set(t.name, ajv.compile(t.inputSchema));
205
+ } catch (err) {
206
+ // A single malformed schema file must not brick the whole server: fall
207
+ // back to a permissive validator for THAT tool only (today's no-
208
+ // validation behavior). The other tools still enforce (T-60.1-04).
209
+ const msg = err instanceof Error ? err.message : String(err);
210
+ // eslint-disable-next-line no-console
211
+ console.error(
212
+ `[gdd-mcp] schema compile failed for ${t.name}; tool degraded to permissive validation: ${msg}`,
213
+ );
214
+ validators.set(t.name, PASS_THROUGH);
215
+ }
216
+ }
217
+
189
218
  const server = new Server(
190
219
  { name: SERVER_NAME, version: SERVER_VERSION },
191
220
  {
@@ -237,6 +266,30 @@ export function buildServer(): Server {
237
266
  };
238
267
  }
239
268
 
269
+ // HARDEN-01 (Task 2): validate arguments against the tool's advertised
270
+ // input schema BEFORE the handler runs. A schema-invalid call returns a
271
+ // structured isError result and the handler is NEVER reached.
272
+ const validate = validators.get(toolName);
273
+ if (validate !== undefined) {
274
+ const argsObj = args ?? {};
275
+ if (!validate(argsObj)) {
276
+ const detail = ajv.errorsText(validate.errors, { dataVar: 'input' });
277
+ const payload = toToolError(
278
+ new Error(`input validation failed: ${detail}`),
279
+ );
280
+ return {
281
+ isError: true,
282
+ content: [
283
+ {
284
+ type: 'text' as const,
285
+ text: JSON.stringify({ success: false, error: payload.error }),
286
+ },
287
+ ],
288
+ structuredContent: { success: false, error: payload.error },
289
+ };
290
+ }
291
+ }
292
+
240
293
  let response;
241
294
  try {
242
295
  response = await tool.handle(args ?? {});