← back to Tk 10965 Zero Price Analysis
Reject unsafe inventory CLI arguments before initialization
7fb358ca8948290915678dd2c34f83a02d5a0532 · 2026-09-08 04:48:13 -0700 · Steve
Files touched
M apply-fix.mjsA cli-args.mjsA test/cli-preflight.test.mjsA test/deny-side-effects.cjsA verification/cli-preflight-test-output.txtM verification/e2e-proof.json
Diff
commit 7fb358ca8948290915678dd2c34f83a02d5a0532
Author: Steve <steve@designerwallcoverings.com>
Date: Tue Sep 8 04:48:13 2026 -0700
Reject unsafe inventory CLI arguments before initialization
---
apply-fix.mjs | 12 ++++-
cli-args.mjs | 29 +++++++++++
test/cli-preflight.test.mjs | 69 ++++++++++++++++++++++++++
test/deny-side-effects.cjs | 49 +++++++++++++++++++
verification/cli-preflight-test-output.txt | 50 +++++++++++++++++++
verification/e2e-proof.json | 78 ++++++++++++++++++++----------
6 files changed, 259 insertions(+), 28 deletions(-)
diff --git a/apply-fix.mjs b/apply-fix.mjs
index d052e21..77c7b00 100644
--- a/apply-fix.mjs
+++ b/apply-fix.mjs
@@ -11,6 +11,15 @@
// node apply-fix.mjs --rollback FILE # restore on_hand from a restore-map file
import fs from 'node:fs';
import path from 'node:path';
+import { parseArgs, USAGE } from './cli-args.mjs';
+
+// Preflight must precede runtime directories, credentials and external calls.
+let cli;
+try { cli = parseArgs(process.argv.slice(2)); }
+catch (error) { console.error(`${error.message}\n\n${USAGE}`); process.exit(2); }
+if (cli.mode === '--help') { console.log(USAGE); process.exit(0); }
+const arg = cli.mode;
+const arg2 = cli.file;
const HERE = path.dirname(new URL(import.meta.url).pathname);
const RUNS = path.join(HERE, 'runs'); fs.mkdirSync(RUNS, { recursive: true });
@@ -81,7 +90,6 @@ async function setOnHand(inventoryItemId, locationId, compareQuantity, quantity)
}
const stamp = () => new Date().toISOString().replace(/[:.]/g, '-');
-const arg = process.argv[2], arg2 = process.argv[3];
if (arg === '--rollback') {
await requireInventoryWriteScope();
@@ -103,7 +111,7 @@ if (arg === '--enumerate') { console.log('Enumerate-only. No writes.'); process.
const scopes = await requireInventoryWriteScope();
console.log(`Inventory write scope verified (${scopes.length} total scopes).`);
-const limit = arg === '--canary' ? (Number(arg2) || 50) : bad.length;
+const limit = arg === '--canary' ? cli.count : bad.length;
const target = bad.slice(0, limit);
console.log(`Applying on_hand=0 to ${target.length} products (${arg})...`);
let done = 0, fail = 0;
diff --git a/cli-args.mjs b/cli-args.mjs
new file mode 100644
index 0000000..5088c2b
--- /dev/null
+++ b/cli-args.mjs
@@ -0,0 +1,29 @@
+export const USAGE = `Usage: node apply-fix.mjs <mode>
+ --help Show usage without initialization
+ --enumerate Scan and save restore map; no inventory writes
+ --canary [N] Apply to first N results (default: 50)
+ --all Apply to all freshly enumerated results
+ --rollback FILE Restore quantities from FILE`;
+
+export function parseArgs(args) {
+ const [mode, value] = args;
+ if (!['--help', '--enumerate', '--canary', '--all', '--rollback'].includes(mode)) {
+ throw new Error(mode ? `Unrecognized mode: ${mode}` : 'A mode is required.');
+ }
+ if (mode === '--canary') {
+ if (args.length > 2) throw new Error('Unexpected extra arguments for --canary.');
+ const count = value === undefined ? 50 : Number(value);
+ if ((value !== undefined && !/^[0-9]+$/.test(value)) || !Number.isSafeInteger(count) || count <= 0) {
+ throw new Error('--canary count must be a positive safe integer.');
+ }
+ return { mode, count };
+ }
+ if (mode === '--rollback') {
+ if (args.length !== 2 || !value.trim() || value.startsWith('-')) {
+ throw new Error('--rollback requires exactly one file path (use ./ for a path beginning with a dash).');
+ }
+ return { mode, file: value };
+ }
+ if (args.length !== 1) throw new Error(`Unexpected extra arguments for ${mode}.`);
+ return { mode };
+}
diff --git a/test/cli-preflight.test.mjs b/test/cli-preflight.test.mjs
new file mode 100644
index 0000000..b734bb9
--- /dev/null
+++ b/test/cli-preflight.test.mjs
@@ -0,0 +1,69 @@
+import test from 'node:test';
+import assert from 'node:assert/strict';
+import { spawnSync } from 'node:child_process';
+import { fileURLToPath } from 'node:url';
+import { parseArgs } from '../cli-args.mjs';
+const entry = fileURLToPath(new URL('../apply-fix.mjs', import.meta.url));
+const parser = fileURLToPath(new URL('../cli-args.mjs', import.meta.url));
+const preload = fileURLToPath(new URL('./deny-side-effects.cjs', import.meta.url));
+function run(args, target = entry) {
+ const result = spawnSync(process.execPath, ['--require', preload, target, ...args], {
+ encoding: 'utf8', timeout: 5000,
+ env: { GUARD_ALLOWED_READS: JSON.stringify([target, parser]) }
+ });
+ assert.ifError(result.error);
+ assert.equal(result.signal, null);
+ return { ...result, attempts: [...result.stderr.matchAll(/^GUARD_ATTEMPT:(.+)$/gm)].map(m => m[1]) };
+}
+const invalid = [[], ['--help', '--all'], ['--wat'], ['--canery'], ['--all', '--all'], ['--enumerate', '--all'], ['--all', 'extra'], ['--canary', '--all'], ['--canary', '50', '--all'], ['--rollback'], ['--rollback', ''], ['--rollback', ' '], ['--rollback', '--all'], ['--rollback', 'file.json', 'extra'], ...['0','-1','1.5','nope','9007199254740992','Infinity','NaN','',' ','1e2','0x10'].map(n => ['--canary', n])];
+for (const args of invalid) test('reject before initialization: ' + JSON.stringify(args), () => {
+ assert.throws(() => parseArgs(args));
+ const result = run(args);
+ assert.equal(result.status, 2, result.stderr);
+ assert.match(result.stderr, /Usage: node apply-fix.mjs/);
+ assert.deepEqual(result.attempts, [], result.stderr);
+});
+test('--help is safe at actual entry point', () => {
+ assert.deepEqual(parseArgs(['--help']), {mode:'--help'});
+ const result = run(['--help']);
+ assert.equal(result.status, 0, result.stderr);
+ assert.match(result.stdout, /Usage: node apply-fix.mjs/);
+ assert.deepEqual(result.attempts, []);
+});
+const recognized = [
+ [['--enumerate'], {mode:'--enumerate'}], [['--all'], {mode:'--all'}],
+ [['--canary'], {mode:'--canary',count:50}], [['--canary','1'], {mode:'--canary',count:1}],
+ [['--canary','9007199254740991'], {mode:'--canary',count:9007199254740991}],
+ [['--rollback','file.json'], {mode:'--rollback',file:'file.json'}],
+ [['--rollback','./--file.json'], {mode:'--rollback',file:'./--file.json'}]
+];
+for (const [args, expected] of recognized) test('recognized mode reaches blocked initialization only: ' + JSON.stringify(args), () => {
+ assert.deepEqual(parseArgs(args), expected);
+ const result = run(args);
+ assert.notEqual(result.status, 0);
+ assert.deepEqual(result.attempts, ['fs.mkdirSync'], result.stderr);
+});
+if (process.env.CLI_BASELINE_ENTRY) for (const args of [[], ['--help'], ['--typo']]) {
+ test('prepatch entry exposes missing guard: ' + JSON.stringify(args), () => {
+ const result = run(args, process.env.CLI_BASELINE_ENTRY);
+ assert.notEqual(result.status, 0);
+ assert.deepEqual(result.attempts, ['fs.mkdirSync'], result.stderr);
+ });
+}
+
+const deniedProbes = [
+ ["require('node:fs').readFileSync('/guard-denied-read')", 'fs.readFileSync'],
+ ["require('node:fs').writeFileSync('/guard-denied-write', 'x')", 'fs.writeFileSync'],
+ ["require('node:fs').promises.readFile('/guard-denied-read')", 'fs.promises.readFile'],
+ ["fetch('https://guard.invalid')", 'fetch'],
+ ["require('node:https').get('https://guard.invalid')", 'node:https.get'],
+ ["require('node:child_process').spawn('guard-must-not-launch')", 'node:child_process.spawn']
+];
+for (const [code, expected] of deniedProbes) test('boundary hook calibration: ' + expected, () => {
+ const result = spawnSync(process.execPath, ['--require', preload, '--eval', code], {
+ encoding:'utf8', timeout:5000, env:{GUARD_ALLOWED_READS:'[]'}
+ });
+ assert.ifError(result.error);
+ assert.notEqual(result.status, 0);
+ assert.deepEqual([...result.stderr.matchAll(/^GUARD_ATTEMPT:(.+)$/gm)].map(m => m[1]), [expected]);
+});
diff --git a/test/deny-side-effects.cjs b/test/deny-side-effects.cjs
new file mode 100644
index 0000000..46c91ca
--- /dev/null
+++ b/test/deny-side-effects.cjs
@@ -0,0 +1,49 @@
+const fs = require('node:fs');
+const { syncBuiltinESMExports } = require('node:module');
+const originalWrite = fs.writeSync.bind(fs);
+const allowed = new Set(JSON.parse(process.env.GUARD_ALLOWED_READS));
+function denied(name) {
+ return function () {
+ originalWrite(2, 'GUARD_ATTEMPT:' + name + '\n');
+ throw new Error('GUARD_BLOCKED:' + name);
+ };
+}
+// Only the actual entry point and its pure parser may be read by the loader.
+for (const name of ['readFileSync', 'readFile', 'createReadStream']) {
+ const original = fs[name].bind(fs);
+ fs[name] = function (file, ...rest) {
+ const path = file instanceof URL ? require('node:url').fileURLToPath(file) : String(file);
+ if (!allowed.has(path)) return denied('fs.' + name)();
+ return original(file, ...rest);
+ };
+}
+for (const name of ['readFile', 'open']) {
+ const original = fs.promises[name].bind(fs.promises);
+ fs.promises[name] = async function(file, ...rest) {
+ const path = file instanceof URL ? require('node:url').fileURLToPath(file) : String(file);
+ if (!allowed.has(path) || (name === 'open' && rest[0] !== 'r' && rest[0] !== 0)) return denied('fs.promises.' + name)();
+ return original(file, ...rest);
+ };
+}
+for (const name of ['mkdir', 'mkdtemp', 'writeFile', 'appendFile', 'rename', 'unlink', 'rm', 'rmdir', 'copyFile', 'cp', 'link', 'symlink', 'truncate', 'chmod', 'chown', 'utimes', 'write', 'writev', 'ftruncate']) {
+ for (const key of [name, name + 'Sync']) if (typeof fs[key] === 'function') fs[key] = denied('fs.' + key);
+ if (typeof fs.promises[name] === 'function') fs.promises[name] = denied('fs.promises.' + name);
+}
+for (const name of ['open', 'openSync']) {
+ const original = fs[name].bind(fs);
+ fs[name] = function(file, flags, ...rest) {
+ const path = file instanceof URL ? require('node:url').fileURLToPath(file) : String(file);
+ if (!allowed.has(path) || (flags !== 'r' && flags !== 0)) return denied('fs.' + name)();
+ return original(file, flags, ...rest);
+ };
+}
+fs.createWriteStream = denied('fs.createWriteStream');
+for (const [module, names] of Object.entries({
+ 'node:child_process': ['spawn', 'spawnSync', 'exec', 'execSync', 'execFile', 'execFileSync', 'fork'],
+ 'node:http': ['request', 'get'], 'node:https': ['request', 'get'],
+ 'node:net': ['connect', 'createConnection', 'createServer'], 'node:tls': ['connect', 'createServer'],
+ 'node:dgram': ['createSocket'], 'node:dns': ['lookup', 'resolve']
+})) { const api = require(module); for (const name of names) api[name] = denied(module + '.' + name); }
+require('node:net').Socket.prototype.connect = denied('net.Socket.connect');
+globalThis.fetch = denied('fetch');
+syncBuiltinESMExports();
diff --git a/verification/cli-preflight-test-output.txt b/verification/cli-preflight-test-output.txt
new file mode 100644
index 0000000..5dc2dbe
--- /dev/null
+++ b/verification/cli-preflight-test-output.txt
@@ -0,0 +1,50 @@
+✔ reject before initialization: [] (116.038083ms)
+✔ reject before initialization: ["--help","--all"] (100.679042ms)
+✔ reject before initialization: ["--wat"] (121.443125ms)
+✔ reject before initialization: ["--canery"] (133.2855ms)
+✔ reject before initialization: ["--all","--all"] (181.764583ms)
+✔ reject before initialization: ["--enumerate","--all"] (142.869834ms)
+✔ reject before initialization: ["--all","extra"] (148.684875ms)
+✔ reject before initialization: ["--canary","--all"] (100.886958ms)
+✔ reject before initialization: ["--canary","50","--all"] (106.357083ms)
+✔ reject before initialization: ["--rollback"] (103.613541ms)
+✔ reject before initialization: ["--rollback",""] (127.23925ms)
+✔ reject before initialization: ["--rollback"," "] (105.318666ms)
+✔ reject before initialization: ["--rollback","--all"] (106.160291ms)
+✔ reject before initialization: ["--rollback","file.json","extra"] (109.317292ms)
+✔ reject before initialization: ["--canary","0"] (133.799417ms)
+✔ reject before initialization: ["--canary","-1"] (144.4195ms)
+✔ reject before initialization: ["--canary","1.5"] (137.57925ms)
+✔ reject before initialization: ["--canary","nope"] (127.759042ms)
+✔ reject before initialization: ["--canary","9007199254740992"] (147.690416ms)
+✔ reject before initialization: ["--canary","Infinity"] (145.912084ms)
+✔ reject before initialization: ["--canary","NaN"] (142.053666ms)
+✔ reject before initialization: ["--canary",""] (117.936ms)
+✔ reject before initialization: ["--canary"," "] (109.753333ms)
+✔ reject before initialization: ["--canary","1e2"] (117.146ms)
+✔ reject before initialization: ["--canary","0x10"] (113.495ms)
+✔ --help is safe at actual entry point (114.693ms)
+✔ recognized mode reaches blocked initialization only: ["--enumerate"] (138.40225ms)
+✔ recognized mode reaches blocked initialization only: ["--all"] (117.33225ms)
+✔ recognized mode reaches blocked initialization only: ["--canary"] (125.193541ms)
+✔ recognized mode reaches blocked initialization only: ["--canary","1"] (137.309875ms)
+✔ recognized mode reaches blocked initialization only: ["--canary","9007199254740991"] (133.01375ms)
+✔ recognized mode reaches blocked initialization only: ["--rollback","file.json"] (121.832167ms)
+✔ recognized mode reaches blocked initialization only: ["--rollback","./--file.json"] (118.679084ms)
+✔ prepatch entry exposes missing guard: [] (143.616875ms)
+✔ prepatch entry exposes missing guard: ["--help"] (131.497584ms)
+✔ prepatch entry exposes missing guard: ["--typo"] (138.005209ms)
+✔ boundary hook calibration: fs.readFileSync (120.587458ms)
+✔ boundary hook calibration: fs.writeFileSync (107.745916ms)
+✔ boundary hook calibration: fs.promises.readFile (112.150167ms)
+✔ boundary hook calibration: fetch (105.367791ms)
+✔ boundary hook calibration: node:https.get (98.253833ms)
+✔ boundary hook calibration: node:child_process.spawn (99.3895ms)
+ℹ tests 42
+ℹ suites 0
+ℹ pass 42
+ℹ fail 0
+ℹ cancelled 0
+ℹ skipped 0
+ℹ todo 0
+ℹ duration_ms 5374.470917
diff --git a/verification/e2e-proof.json b/verification/e2e-proof.json
index 2cc7bbd..83dec89 100644
--- a/verification/e2e-proof.json
+++ b/verification/e2e-proof.json
@@ -1,44 +1,70 @@
{
- "intent": "Revalidate the live zero-price-orderable cohort and execute a restore-map-first CAS canary only when inventory credentials are authorized.",
- "risk_tier": "R4",
- "environment": "Designer Wallcoverings Shopify production, read-only preflight",
- "ticket": "TK-10963",
- "authorization_ticket": "TK-10979",
- "timestamp": "2026-08-31T00:39:04.097Z",
- "baseline": {
- "affected_total": 1743,
- "phillipe_romano": 1281,
- "fentucci_naturals": 462,
- "quantity_2026": 1743
+ "schema_version": 1,
+ "task_id": "cycle-20260908T1121Z.O0jFR4/vienna-cli-guard",
+ "ticket": "TK-11299-zero-price-bin-zsh-orderable-regression",
+ "intent": "Reject malformed invocations and safely show help before runtime initialization; preserve recognized parser modes",
+ "risk_tier": "R1 isolated CLI guard with actual subprocess integration; live inventory R4 remains blocked",
+ "environment": "/private/tmp/vienna-cli-guard-l81wngor/worktree",
+ "timestamp_utc": "2026-09-08T11:48:04.917315+00:00",
+ "baseline_commit": "55f8804fcd7c46032073893bf9724b1f99023739",
+ "node_version": "v26.4.0",
+ "code_sha256": {
+ "apply-fix.mjs": "d806221f4dc7751bf0e7fe35d4d88975ca0a0df67653e012677fb3080c09ee8a",
+ "cli-args.mjs": "cf52141b25bf74ff77cd1bd3b9e03bc7976b1df97aca4e2348c3bb1500c97888",
+ "test/cli-preflight.test.mjs": "1cedcc7da5904f16c5d66e05120e0addddc4cb6288ad9c0337d4f66375c119df",
+ "test/deny-side-effects.cjs": "a02860f2ae9043a3a12f64e394160a5bed24615dbfc5a352afcebb45a9b426fc"
},
+ "commands": [
+ "CLI_BASELINE_ENTRY=/Users/macstudio3/Projects/tk-10965-zero-price-analysis/apply-fix.mjs node --test test/cli-preflight.test.mjs",
+ "git diff --check"
+ ],
"checks": [
{
- "boundary": "Shopify authentication and scopes",
+ "name": "Original actual CLI with missing/help/typo arguments",
"verdict": "PASS",
- "assertion": "Token authenticated with HTTP 200 and scopes were enumerated without exposing the token."
+ "observed": "3 baseline invocations all attempt mkdirSync; preload throws before actual write or credentials"
},
{
- "boundary": "Inventory authorization",
- "verdict": "FAIL",
- "assertion": "Required write_inventory scope is absent; present scopes are read_products, read_publications, write_products, write_publications."
+ "name": "Patched actual CLI invalid arguments",
+ "verdict": "PASS",
+ "cases": 25,
+ "observed": "Exit2, clear usage, zero intercepted filesystem mutation, sensitive-read, network or subprocess attempts"
},
{
- "boundary": "Live defect cohort",
+ "name": "Patched actual CLI help",
"verdict": "PASS",
- "assertion": "Read-only verification found exactly 1,743 active zero-price orderable non-sample variants; all 1,743 have quantity 2026."
+ "observed": "Exit0, usage, zero intercepted attempts"
},
{
- "boundary": "Restore-map-first CAS canary",
- "verdict": "SKIP",
- "assertion": "Critical path intentionally not attempted because inventory write authorization failed."
+ "name": "Recognized modes parser and actual entry",
+ "verdict": "PASS",
+ "cases": 7,
+ "observed": "Expected parse outputs; actual CLI stops at intercepted mkdirSync only; no real initialization"
},
{
- "boundary": "Side effects",
+ "name": "Deny-hook calibration",
"verdict": "PASS",
- "assertion": "No Shopify mutation, email, paid API, database write, deploy, or schedule change occurred."
+ "cases": 6,
+ "observed": "Sensitive read/write, promise read, fetch, HTTPS and subprocess probes each intercepted and denied"
+ },
+ {
+ "name": "Whitespace diff check",
+ "verdict": "PASS"
+ },
+ {
+ "name": "Live inventory remediation and runtime integration",
+ "verdict": "SKIP",
+ "reason": "Outside scope and not approved; exact manifest, bounded executor, explicit write approval including canary still missing"
}
],
- "overall_verdict": "BLOCKED",
- "exact_blocker": "SHOPIFY_ADMIN_TOKEN lacks write_inventory.",
- "safest_next_action": "Provision or route an approved Shopify Admin token with read_products and write_inventory, rerun scope preflight, then run enumerate -> small CAS canary -> read-only verification before any scale-up."
+ "tests": {
+ "passed": 42,
+ "failed": 0,
+ "skipped": 0,
+ "output": "verification/cli-preflight-test-output.txt"
+ },
+ "boundary_harness": "Preload installed before actual entry imports. Only entry/parser source reads allowed; read-only opens of those source files permitted. Builtin ESM exports synced after fs/network/child-process overrides. First denied call throws. Child environment contains only allowlist; no inherited tokens or NODE_OPTIONS.",
+ "initial_harness_issue": "First test attempt blocked Node source-loader openSync too early; corrected by allowing read-only source opens only, then reran all tests. No real sensitive side effects occurred.",
+ "cleanup": "Isolated worktree and all artifacts retained; original checkout unmodified; no cleanup deletion",
+ "verdict": "PASS for bounded local CLI fix; operational ticket remains UNRESOLVED"
}
← 55f8804 chore: lint, refactor, v1.1.1 (session close)
·
back to Tk 10965 Zero Price Analysis
·
Freeze Vienna executor scope and journal offline mutations s 7645583 →