@tekyzinc/gsd-t 3.29.10 → 3.29.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/CHANGELOG.md CHANGED
@@ -2,6 +2,36 @@
2
2
 
3
3
  All notable changes to GSD-T are documented here. Updated with each release.
4
4
 
5
+ ## [3.29.11] - 2026-05-29 09:57 PDT
6
+
7
+ ### Fixed — CRITICAL test-data adapter data-destruction bug (M60)
8
+
9
+ Found by a native-Workflow Red Team bake-off (Opus 4.8, perspective-diverse
10
+ adversarial review) while evaluating GSD-T against native Claude Code 4.8
11
+ capability. M58's own in-session Red Team had passed this as "6/6 defended"
12
+ — the claim was wrong.
13
+
14
+ - **Bug**: `bin/gsd-t-test-data-adapters/file-json-array.cjs` and
15
+ `localstorage-key-prefix.cjs` guarded with
16
+ `typeof taggedPrefix === 'string' && taggedPrefix.length > 0 && !id.startsWith(...)`.
17
+ An empty/undefined `taggedPrefix` short-circuits the whole condition, so
18
+ **no guard runs** — `gsd-t test-data --purge` would delete untagged
19
+ production records and report `errors:0` (gate GREEN). No adapter enforced
20
+ `projectDir` containment on the `store` path, making it a write-anywhere
21
+ delete primitive. Reproduced live before fixing.
22
+ - **Fix** (additive refusal only — removes capability, never adds destructive
23
+ behavior): both adapters now hard-refuse empty/undefined `taggedPrefix`
24
+ (matching the already-correct `sqlite-table-where` adapter); `file-json-array`
25
+ and `sqlite-table-where` enforce the containment predicate
26
+ `resolved.startsWith(root + sep) && resolved !== root` when `projectDir` is
27
+ supplied; `purgeRunInserts` threads `projectDir` into every adapter call.
28
+ - **Tests**: new `test/m60-redteam-regressions.test.js` (10 tests, one per
29
+ finding + happy-path + back-compat). M58 suite 44/44 unchanged.
30
+ - **Contract**: `test-data-tagging-contract.md` → v1.1.0 STABLE (empty-prefix
31
+ refusal + path containment now normative).
32
+ - Backward-compatible: the happy path (non-empty prefix, in-project path, or
33
+ no `projectDir`) is unchanged.
34
+
5
35
  ## [3.29.10] - 2026-05-27 10:09 PDT
6
36
 
7
37
  ### Changed — Timestamp precision in progress.md (forward-only)
@@ -5,23 +5,47 @@
5
5
  * `store` is the file path; `id` is the value of the `id` field on the matching row.
6
6
  *
7
7
  * Refuses to delete a record whose `id` does not start with `taggedPrefix`.
8
- * Atomic rewrite (write-temp + rename).
8
+ * `taggedPrefix` is REQUIRED and non-empty an empty/omitted prefix would
9
+ * disable the guard entirely (Red Team CRITICAL, M60). Atomic rewrite
10
+ * (write-temp + rename).
11
+ *
12
+ * When `projectDir` is supplied, `store` MUST resolve inside it — the adapter
13
+ * refuses paths outside-AND-equal-to projectDir (containment predicate from
14
+ * feedback_destructive_path_ops_containment). Prevents a tampered ledger from
15
+ * becoming a write-anywhere delete primitive.
9
16
  */
10
17
  const fs = require('node:fs');
11
18
  const path = require('node:path');
12
19
 
13
20
  const KIND = 'file-json-array';
14
21
 
15
- function purge({ store, id, taggedPrefix }) {
22
+ function assertContained(store, projectDir) {
23
+ if (typeof projectDir !== 'string' || projectDir.length === 0) {
24
+ return; // no projectDir supplied — containment not enforceable, caller's choice
25
+ }
26
+ const root = path.resolve(projectDir);
27
+ const resolved = path.resolve(store);
28
+ if (!(resolved.startsWith(root + path.sep) && resolved !== root)) {
29
+ throw new Error(
30
+ `file-json-array: store path "${store}" resolves outside projectDir "${projectDir}" — refused (containment guard)`
31
+ );
32
+ }
33
+ }
34
+
35
+ function purge({ store, id, taggedPrefix, projectDir }) {
16
36
  if (typeof store !== 'string' || store.length === 0) {
17
37
  throw new Error('file-json-array: store must be a non-empty file path');
18
38
  }
19
39
  if (typeof id !== 'string' || id.length === 0) {
20
40
  throw new Error('file-json-array: id must be a non-empty string');
21
41
  }
22
- if (typeof taggedPrefix === 'string' && taggedPrefix.length > 0 && !id.startsWith(taggedPrefix)) {
42
+ if (typeof taggedPrefix !== 'string' || taggedPrefix.length === 0) {
43
+ throw new Error('file-json-array: taggedPrefix is required and must be non-empty (guard cannot be disabled)');
44
+ }
45
+ if (!id.startsWith(taggedPrefix)) {
23
46
  throw new Error(`file-json-array: tag prefix mismatch (id="${id}", taggedPrefix="${taggedPrefix}")`);
24
47
  }
48
+ assertContained(store, projectDir);
25
49
 
26
50
  if (!fs.existsSync(store)) {
27
51
  return 'absent';
@@ -22,7 +22,12 @@ async function purge({ page, store, id, taggedPrefix }) {
22
22
  if (typeof id !== 'string' || id.length === 0) {
23
23
  throw new Error('localStorage-key-prefix: id must be a non-empty string');
24
24
  }
25
- if (typeof taggedPrefix === 'string' && taggedPrefix.length > 0 && !id.startsWith(taggedPrefix)) {
25
+ // taggedPrefix is REQUIRED and non-empty an empty/omitted prefix would
26
+ // disable the guard entirely (Red Team CRITICAL, M60).
27
+ if (typeof taggedPrefix !== 'string' || taggedPrefix.length === 0) {
28
+ throw new Error('localStorage-key-prefix: taggedPrefix is required and must be non-empty (guard cannot be disabled)');
29
+ }
30
+ if (!id.startsWith(taggedPrefix)) {
26
31
  throw new Error(`localStorage-key-prefix: tag prefix mismatch (id="${id}", taggedPrefix="${taggedPrefix}")`);
27
32
  }
28
33
 
@@ -8,11 +8,25 @@
8
8
  * still loads when the module isn't installed. Tests self-skip in that case.
9
9
  */
10
10
  const fs = require('node:fs');
11
+ const path = require('node:path');
11
12
 
12
13
  const KIND = 'sqlite-table-where';
13
14
 
14
15
  const IDENT_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
15
16
 
17
+ function assertContained(dbPath, projectDir) {
18
+ if (typeof projectDir !== 'string' || projectDir.length === 0) {
19
+ return; // no projectDir supplied — containment not enforceable, caller's choice
20
+ }
21
+ const root = path.resolve(projectDir);
22
+ const resolved = path.resolve(dbPath);
23
+ if (!(resolved.startsWith(root + path.sep) && resolved !== root)) {
24
+ throw new Error(
25
+ `sqlite-table-where: dbPath "${dbPath}" resolves outside projectDir "${projectDir}" — refused (containment guard)`
26
+ );
27
+ }
28
+ }
29
+
16
30
  function parseStore(store) {
17
31
  if (typeof store !== 'string') {
18
32
  throw new Error('sqlite-table-where: store must be "dbPath|table|idColumn"');
@@ -34,7 +48,7 @@ function parseStore(store) {
34
48
  return { dbPath, table, idColumn };
35
49
  }
36
50
 
37
- function purge({ store, id, taggedPrefix }) {
51
+ function purge({ store, id, taggedPrefix, projectDir }) {
38
52
  const { dbPath, table, idColumn } = parseStore(store);
39
53
  if (typeof id !== 'string' || id.length === 0) {
40
54
  throw new Error('sqlite-table-where: id must be a non-empty string');
@@ -45,6 +59,7 @@ function purge({ store, id, taggedPrefix }) {
45
59
  if (!id.startsWith(taggedPrefix)) {
46
60
  throw new Error(`sqlite-table-where: tag prefix mismatch (id="${id}", taggedPrefix="${taggedPrefix}")`);
47
61
  }
62
+ assertContained(dbPath, projectDir);
48
63
  if (!fs.existsSync(dbPath)) {
49
64
  return 'absent';
50
65
  }
@@ -129,6 +129,7 @@ async function purgeRunInserts({ projectDir, runId, dryRun }) {
129
129
  store: row.store,
130
130
  id: row.id,
131
131
  taggedPrefix: row.taggedPrefix,
132
+ projectDir, // thread through so path adapters can enforce containment (M60)
132
133
  });
133
134
  if (result === 'purged') {
134
135
  purged.push(row);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekyzinc/gsd-t",
3
- "version": "3.29.10",
3
+ "version": "3.29.11",
4
4
  "description": "GSD-T: Contract-Driven Development for Claude Code — 54 slash commands with headless-by-default workflow spawning, unattended supervisor relay with event stream, graph-powered code analysis, real-time agent dashboard, task telemetry, doc-ripple enforcement, backlog management, impact analysis, test sync, milestone archival, and PRD generation",
5
5
  "author": "Tekyz, Inc.",
6
6
  "license": "MIT",