[object Object]

← back to Norma

TK-11383: enforce the enrollment hold at the PUBLISHER too, not just the builder

3bdc7a564c58a8dfc6b103add85654eccea74091 · 2026-09-13 14:54:29 -0700 · Steve Abrams

Follow-up to 123570c after a second-model review (Kimi) flagged that a hold
enforced only in build-registry.js is defeated by the very things that bypass
the builder: a hand-edited accounts.json, an `--include-held` build that gets
committed, or an older checkout. The irreversible action — a public post —
happens in daily-cadence.js, which posts to every key of accounts.json. So the
publisher now enforces the SAME enrollment-hold.json independently, right where
the harm would occur: any held handle that somehow reached the registry is
dropped from HANDLES before selection and cannot post.

Failure posture mirrors the builder for consistency: a corrupt hold file fails
CLOSED (aborts the whole run, exit 1 — an untrustworthy denylist must not read
as "no holds"); an absent file warns loudly and continues (documented fresh
state; the written registry already excludes held handles). Absent is not
fleet-halting because halting all 35 accounts over one missing config file is
disproportionate, and the file is committed.

Tested read-only (--status, never posts): normal run unchanged and emits no
false HOLD line; a corrupted enrollment-hold.json makes the real cadence abort
exit 1; and a synthetic registry containing @designerschat has it dropped with
a loud HOLD ENFORCED line before any posting logic.

Undo: git revert HEAD (1 file, no runtime state touched).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M85r8WQP2nf5jssn8RiuDA

Files touched

Diff

commit 3bdc7a564c58a8dfc6b103add85654eccea74091
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sun Sep 13 14:54:29 2026 -0700

    TK-11383: enforce the enrollment hold at the PUBLISHER too, not just the builder
    
    Follow-up to 123570c after a second-model review (Kimi) flagged that a hold
    enforced only in build-registry.js is defeated by the very things that bypass
    the builder: a hand-edited accounts.json, an `--include-held` build that gets
    committed, or an older checkout. The irreversible action — a public post —
    happens in daily-cadence.js, which posts to every key of accounts.json. So the
    publisher now enforces the SAME enrollment-hold.json independently, right where
    the harm would occur: any held handle that somehow reached the registry is
    dropped from HANDLES before selection and cannot post.
    
    Failure posture mirrors the builder for consistency: a corrupt hold file fails
    CLOSED (aborts the whole run, exit 1 — an untrustworthy denylist must not read
    as "no holds"); an absent file warns loudly and continues (documented fresh
    state; the written registry already excludes held handles). Absent is not
    fleet-halting because halting all 35 accounts over one missing config file is
    disproportionate, and the file is committed.
    
    Tested read-only (--status, never posts): normal run unchanged and emits no
    false HOLD line; a corrupted enrollment-hold.json makes the real cadence abort
    exit 1; and a synthetic registry containing @designerschat has it dropped with
    a loud HOLD ENFORCED line before any posting logic.
    
    Undo: git revert HEAD (1 file, no runtime state touched).
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01M85r8WQP2nf5jssn8RiuDA
---
 agents/instagram-agent/daily-cadence.js | 30 ++++++++++++++++++++++++++++++
 1 file changed, 30 insertions(+)

diff --git a/agents/instagram-agent/daily-cadence.js b/agents/instagram-agent/daily-cadence.js
index f7b4db5..960126b 100644
--- a/agents/instagram-agent/daily-cadence.js
+++ b/agents/instagram-agent/daily-cadence.js
@@ -31,6 +31,36 @@ const themes = JSON.parse(fs.readFileSync(path.join(HERE, 'account-themes.json')
 const content = require('./content');
 
 const HANDLES = Object.keys(reg);
+
+// ---- Publisher-side enrollment HOLD enforcement (TK-11383) ------------------
+// This is the point of the IRREVERSIBLE action: whatever is in HANDLES gets
+// posted PUBLICLY. build-registry.js honours enrollment-hold.json when it WRITES
+// accounts.json, but that guard is defeated by a hand-edit, an --include-held
+// build that gets committed, or an older checkout. So the publisher enforces the
+// SAME hold list independently, right where the harm would happen. A held handle
+// that somehow reached accounts.json is dropped here and cannot post.
+// Corrupt hold file => fail CLOSED (abort the whole run): an untrustworthy
+// denylist must not be treated as empty. Absent file => warn loudly + continue
+// (documented fresh state; the written registry already excludes held handles).
+(() => {
+  let hold = {};
+  try {
+    hold = JSON.parse(fs.readFileSync(path.join(HERE, 'enrollment-hold.json'), 'utf8')).hold || {};
+  } catch (e) {
+    if (e.code !== 'ENOENT') {
+      console.error(`ABORT: enrollment-hold.json is unreadable (${e.message}). Refusing to post — an untrustworthy hold list must not be read as "no holds".`);
+      process.exit(1);
+    }
+    console.error('WARNING: enrollment-hold.json not present; proceeding with no enrollment holds.');
+  }
+  const heldPresent = Object.keys(hold).filter((h) => Object.prototype.hasOwnProperty.call(reg, h));
+  for (const h of heldPresent) {
+    delete reg[h];
+    const i = HANDLES.indexOf(h);
+    if (i >= 0) HANDLES.splice(i, 1);
+    console.error(`HOLD ENFORCED: @${h} is on the enrollment hold list (${hold[h].ticket || 'no ticket'}) but was present in accounts.json — dropped from this run so it will NOT post publicly. ${hold[h].reason || ''}`);
+  }
+})();
 const now = new Date();
 const isRestDay = now.getDay() === 0; // Sunday
 const today = now.toISOString().slice(0, 10);

← 72f0976 TK-11383: enforce the designerschat enrollment hold at the P  ·  back to Norma  ·  TK-11383: close the allowlist-expansion path to unapproved p 36f4ede →