@hegemonart/get-design-done 1.60.0 → 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.60.0"
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.60.0",
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.60.0",
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,23 @@ 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
+
7
24
  ## [1.60.0] - 2026-06-10
8
25
 
9
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.
@@ -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.60.0",
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",
@@ -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 ?? {});