← 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
M scripts/tk10002-null-sample/apply.jsA shopify/scripts/lib/approval-gate.js
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 →