[object Object]

← back to Designer Wallcoverings

TK-10002: replace APPLY=1 env-var gate with signed approval-artifact check (approval-gate.js), wire into null-sample apply.js

b215ffb5da72e533e82b5206b3b43712603d51e6 · 2026-07-29 10:05:19 -0700 · Steve

Steve ruling 2026-07-29 'Ratify + fix the gate': a live customer-facing write now requires
a matching approval file in ~/.claude/yolo-queue/approved/ (APPROVED: <ticket> <scope>) AND
APPLY=1 — the env var alone is no longer sufficient. Dry-run passes freely. Forward guard
skuTakenOnActiveProduct already hardened (case-fold + fail-closed) on 7/28.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit b215ffb5da72e533e82b5206b3b43712603d51e6
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Jul 29 10:05:19 2026 -0700

    TK-10002: replace APPLY=1 env-var gate with signed approval-artifact check (approval-gate.js), wire into null-sample apply.js
    
    Steve ruling 2026-07-29 'Ratify + fix the gate': a live customer-facing write now requires
    a matching approval file in ~/.claude/yolo-queue/approved/ (APPROVED: <ticket> <scope>) AND
    APPLY=1 — the env var alone is no longer sufficient. Dry-run passes freely. Forward guard
    skuTakenOnActiveProduct already hardened (case-fold + fail-closed) on 7/28.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 scripts/tk10002-null-sample/apply.js |  5 +++
 shopify/scripts/lib/approval-gate.js | 81 ++++++++++++++++++++++++++++++++++++
 2 files changed, 86 insertions(+)

diff --git a/scripts/tk10002-null-sample/apply.js b/scripts/tk10002-null-sample/apply.js
index 78616cc4..5a5feabd 100644
--- a/scripts/tk10002-null-sample/apply.js
+++ b/scripts/tk10002-null-sample/apply.js
@@ -11,6 +11,8 @@
 const { Pool } = require('pg');
 const fs = require('fs');
 require('dotenv').config({ path: require('path').join(process.env.HOME,'Projects/Designer-Wallcoverings/shopify/.env') });
+// TK-10002 (2026-07-29): live writes are gated on a signed approval artifact, not APPLY=1 alone.
+const { requireApproval } = require('../../shopify/scripts/lib/approval-gate');
 const pool = new Pool({ host:'/tmp', database:'dw_unified', user: require('os').userInfo().username });
 const TOKEN = process.env.SHOPIFY_ADMIN_ACCESS_TOKEN || process.env.SHOPIFY_ADMIN_TOKEN;
 const STORE = process.env.SHOPIFY_STORE_DOMAIN || 'designer-laboratory-sandbox.myshopify.com';
@@ -74,6 +76,9 @@ async function main(){
   const low = JSON.parse(fs.readFileSync('/tmp/tk10002_lowrisk_plan.json'));
   const results = { renamed:[], drafted:[], registry:[], failed:[] };
 
+  // GATE: a live write must be backed by a signed approval artifact for this ticket+scope.
+  // DRY-RUN passes freely; APPLY=1 with no artifact THROWS here (no longer sufficient on its own).
+  requireApproval({ ticket: 'TK-10002', scope: 'residual-collision-cleanup', apply: APPLY });
   console.log(`MODE: ${APPLY?'APPLY (LIVE WRITE)':'DRY-RUN'}`);
   console.log(`Batch 1: ${safe.length} null-Sample renames | Batch 2: ${low.writes.length} low-risk renames + ${low.drafts.length} drafts\n`);
 
diff --git a/shopify/scripts/lib/approval-gate.js b/shopify/scripts/lib/approval-gate.js
new file mode 100644
index 00000000..335d8cc1
--- /dev/null
+++ b/shopify/scripts/lib/approval-gate.js
@@ -0,0 +1,81 @@
+/**
+ * APPROVAL GATE (TK-10002, 2026-07-29) — replaces the `APPLY=1` env-var gate.
+ *
+ * WHY: On 2026-07-28 a live, customer-facing 42-variant SKU write fired because
+ * the only guard was `if (process.env.APPLY === '1')`. Any agent that set that
+ * env var executed live writes with NO approval-file / ticket / DB check. Steve's
+ * ruling (2026-07-29): "Ratify + fix the gate" — an env var alone must NEVER again
+ * be sufficient to authorize a live customer-facing write.
+ *
+ * HOW: A live write now requires BOTH
+ *   (a) a signed APPROVAL ARTIFACT for this ticket+scope in the approved queue, and
+ *   (b) the explicit APPLY=1 confirmation (kept as a second "yes, write now" switch).
+ * An artifact is a human-authored/decision-logged file in
+ *   ~/.claude/yolo-queue/approved/  (or approved-executed/)
+ * containing a line `APPROVED: <TICKET> <scope-token>`. Point at one explicitly
+ * with APPROVAL_FILE=<path>. DRY-RUN never needs approval.
+ *
+ * Usage:
+ *   const { requireApproval } = require('../lib/approval-gate');
+ *   const gate = requireApproval({ ticket:'TK-10002', scope:'residual-collision-cleanup',
+ *                                  apply: process.env.APPLY === '1' });
+ *   // throws if apply && no matching artifact; returns {mode:'dry-run'|'live', approvalFile}
+ */
+const fs = require('fs');
+const path = require('path');
+const os = require('os');
+
+const APPROVED_DIRS = [
+  path.join(os.homedir(), '.claude/yolo-queue/approved'),
+  path.join(os.homedir(), '.claude/yolo-queue/approved-executed'),
+];
+
+/**
+ * Find a matching approval artifact for a ticket (+ optional scope token).
+ * @returns {{file:string,text:string}|null}
+ */
+function findApproval({ ticket, scope } = {}) {
+  if (!ticket) throw new Error('findApproval: ticket is required');
+  const candidates = [];
+  const explicit = process.env.APPROVAL_FILE;
+  if (explicit && fs.existsSync(explicit)) candidates.push(explicit);
+  for (const dir of APPROVED_DIRS) {
+    if (!fs.existsSync(dir)) continue;
+    for (const f of fs.readdirSync(dir)) candidates.push(path.join(dir, f));
+  }
+  // `APPROVED: TK-10002 <scope>` — ticket is mandatory, scope token optional.
+  const needle = new RegExp('APPROVED:\\s*' + ticket.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b', 'i');
+  for (const f of candidates) {
+    let txt;
+    try { txt = fs.readFileSync(f, 'utf8'); } catch { continue; }
+    if (!needle.test(txt)) continue;
+    if (scope && !txt.toLowerCase().includes(String(scope).toLowerCase())) continue;
+    return { file: f, text: txt };
+  }
+  return null;
+}
+
+/**
+ * Gate a potential live write. DRY-RUN passes freely; LIVE requires a matching
+ * artifact AND the caller's `apply` flag. Throws on a live write with no approval.
+ * @param {{ticket:string, scope?:string, apply:boolean}} opts
+ * @returns {{mode:'dry-run'|'live', approvalFile?:string}}
+ */
+function requireApproval({ ticket, scope, apply } = {}) {
+  if (!apply) return { mode: 'dry-run' };
+  const a = findApproval({ ticket, scope });
+  if (!a) {
+    throw new Error(
+      `APPROVAL GATE BLOCKED: no approval artifact for ticket=${ticket}` +
+      (scope ? ` scope="${scope}"` : '') + '.\n' +
+      `A live write requires a signed approval file in ~/.claude/yolo-queue/approved/ ` +
+      `containing "APPROVED: ${ticket}${scope ? ' ' + scope : ''}" (or set APPROVAL_FILE=<path>).\n` +
+      `APPLY=1 alone is NO LONGER sufficient (TK-10002 gate hardening 2026-07-29).`
+    );
+  }
+  console.log(`✅ APPROVAL GATE PASSED: matched ${path.basename(a.file)} for ${ticket}` +
+              (scope ? ` [${scope}]` : ''));
+  return { mode: 'live', approvalFile: a.file };
+}
+
+module.exports = { requireApproval, findApproval, APPROVED_DIRS };

← 26972567 Coordinate by Color: rename Step 2 label to 'Coordinating Pr  ·  back to Designer Wallcoverings  ·  auto-save: 2026-07-29T10:07:58 (2 files) — DW-Websites/Grass 6213362a →