memnox 0.1.1 → 0.2.1

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.
Files changed (2) hide show
  1. package/dist/index.js +405 -285
  2. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -113,8 +113,60 @@ function explain(err) {
113
113
  // src/program.ts
114
114
  import { Command } from "commander";
115
115
 
116
+ // src/banner.ts
117
+ var LETTERS = [
118
+ ["## ##", "### ###", "## # ##", "## ##", "## ##"],
119
+ ["######", "##", "#####", "##", "######"],
120
+ ["## ##", "### ###", "## # ##", "## ##", "## ##"],
121
+ ["## ##", "### ##", "## # ##", "## ###", "## ##"],
122
+ [" #####", "## ##", "## ##", "## ##", " #####"],
123
+ ["## ##", " ## ##", " ###", " ## ##", "## ##"]
124
+ ];
125
+ var ROWS = 5;
126
+ var TRACKING = 1;
127
+ var FACE = "\u2588";
128
+ var SHADOW = "\u2584";
129
+ var DRAWN = "#";
130
+ function faceGrid() {
131
+ const grid = Array.from({ length: ROWS }, () => []);
132
+ for (const letter of LETTERS) {
133
+ const width = Math.max(...letter.map((row3) => row3.length));
134
+ for (let row3 = 0; row3 < ROWS; row3 += 1) {
135
+ const art = letter[row3] ?? "";
136
+ for (let column = 0; column < width + TRACKING; column += 1) {
137
+ grid[row3].push(art[column] === DRAWN);
138
+ }
139
+ }
140
+ }
141
+ return grid;
142
+ }
143
+ var at = (grid, row3, column) => row3 >= 0 && column >= 0 && (grid[row3]?.[column] ?? false);
144
+ var shadowed = (grid, row3, column) => row3 === ROWS && at(grid, ROWS - 1, column - 1);
145
+ function wordmark(style, plain) {
146
+ if (!style.decorated) return [plain];
147
+ const grid = faceGrid();
148
+ const width = grid[0]?.length ?? 0;
149
+ const lines = [];
150
+ for (let row3 = 0; row3 <= ROWS; row3 += 1) {
151
+ let line2 = "";
152
+ for (let column = 0; column < width; column += 1) {
153
+ if (at(grid, row3, column)) line2 += style.bold(FACE);
154
+ else if (shadowed(grid, row3, column)) line2 += style.dim(SHADOW);
155
+ else line2 += " ";
156
+ }
157
+ lines.push(line2);
158
+ }
159
+ return lines;
160
+ }
161
+ function masthead(out, style) {
162
+ if (!style.decorated) return;
163
+ out.note("");
164
+ for (const line2 of wordmark(style, "memnox")) out.note(line2);
165
+ out.note("");
166
+ }
167
+
116
168
  // src/defaults.ts
117
- var CLI_VERSION = "0.1.1";
169
+ var CLI_VERSION = "0.2.1";
118
170
 
119
171
  // src/commands/scan.command.ts
120
172
  import { homedir as homedir3 } from "os";
@@ -210,6 +262,8 @@ function reaches(policy, action) {
210
262
  // src/scan/machine-report.ts
211
263
  import {
212
264
  AGENT_APPROVAL,
265
+ agentRefOf,
266
+ agentsReachingPath,
213
267
  approvalOf,
214
268
  describeBrowser,
215
269
  describeCombined,
@@ -313,7 +367,7 @@ function renderMachine(context, report3, counts) {
313
367
  out.line(`${surfaces.length} execution surfaces.`);
314
368
  if (counts.approvedAgents.length === 0 && report3.agents.length > 0) {
315
369
  out.note(
316
- 'No agent has been approved or refused here. Decide with "memnox config set approvedAgents".'
370
+ `No agent has been approved or refused here. Decide with "memnox config set approvedAgents ${report3.agents.map((agent) => agent.kind).join(",")}".`
317
371
  );
318
372
  }
319
373
  out.line("");
@@ -342,7 +396,11 @@ function renderMachine(context, report3, counts) {
342
396
  function renderCredentials(context, report3) {
343
397
  const { out, style } = context;
344
398
  if (report3.credentials.length === 0) return;
345
- const agents = report3.agents.length;
399
+ const refs = report3.agents.map(agentRefOf);
400
+ const reach2 = (path) => {
401
+ const count = agentsReachingPath(path, refs, report3.surfaces).length;
402
+ return `${count} agent${count === 1 ? "" : "s"}`;
403
+ };
346
404
  out.line("");
347
405
  out.line(style.bold("CREDENTIALS THESE AGENTS CAN READ"));
348
406
  out.line("");
@@ -351,17 +409,16 @@ function renderCredentials(context, report3) {
351
409
  ...report3.envFiles.map((each) => each.path.length)
352
410
  ) + PATH_GUTTER;
353
411
  for (const credential of report3.credentials) {
354
- const reach2 = `${agents} agent${agents === 1 ? "" : "s"}`;
355
- out.line(` ${style.warn("!")} ${credential.path.padEnd(width)}${reach2}`);
412
+ out.line(
413
+ ` ${style.warn("!")} ${credential.path.padEnd(width)}${reach2(credential.path)}`
414
+ );
356
415
  if (credential.detail !== void 0) {
357
416
  out.line(` ${style.dim(credential.detail)}`);
358
417
  }
359
418
  }
360
419
  for (const env of report3.envFiles) {
361
420
  const keys = env.keyLike === 0 ? "" : env.keyLike === 1 ? ", 1 looks like a credential" : `, ${env.keyLike} look like credentials`;
362
- out.line(
363
- ` ${style.warn("!")} ${env.path.padEnd(width)}${agents} agent${agents === 1 ? "" : "s"}`
364
- );
421
+ out.line(` ${style.warn("!")} ${env.path.padEnd(width)}${reach2(env.path)}`);
365
422
  out.line(` ${style.dim(`${env.variables} variables${keys}`)}`);
366
423
  }
367
424
  }
@@ -878,22 +935,22 @@ function watchConfigPaths(paths, seams = {}) {
878
935
  async next(timeoutMs) {
879
936
  if (pending) {
880
937
  pending = false;
881
- await new Promise((resolve5) => setTimer(resolve5, SETTLE_MS));
938
+ await new Promise((resolve6) => setTimer(resolve6, SETTLE_MS));
882
939
  return true;
883
940
  }
884
- const changed = await new Promise((resolve5) => {
941
+ const changed = await new Promise((resolve6) => {
885
942
  const timer = setTimer(() => {
886
943
  wake = null;
887
- resolve5(false);
944
+ resolve6(false);
888
945
  }, timeoutMs);
889
946
  wake = () => {
890
947
  clearTimer(timer);
891
- resolve5(true);
948
+ resolve6(true);
892
949
  };
893
950
  });
894
951
  if (changed) {
895
952
  pending = false;
896
- await new Promise((resolve5) => setTimer(resolve5, SETTLE_MS));
953
+ await new Promise((resolve6) => setTimer(resolve6, SETTLE_MS));
897
954
  }
898
955
  return changed;
899
956
  },
@@ -1163,31 +1220,79 @@ async function resolve(milestones, id) {
1163
1220
  }
1164
1221
 
1165
1222
  // src/commands/check.command.ts
1166
- import { existsSync as existsSync2 } from "fs";
1167
1223
  import { homedir as homedir5 } from "os";
1168
- import { join as join3 } from "path";
1169
1224
  import {
1170
1225
  DECISION_EFFECT as DECISION_EFFECT2,
1171
1226
  INTENT_KIND,
1172
1227
  LocalGate,
1173
- loadPolicySet as loadPolicySet3,
1174
- MEMNOX_HOME as MEMNOX_HOME2,
1175
1228
  preflightFor,
1176
1229
  overlaysInForce,
1177
- readPolicyRegistry as readPolicyRegistry3,
1178
1230
  stateFactsInForce
1179
1231
  } from "@memnox/core";
1180
1232
 
1181
1233
  // src/policy-path.ts
1182
1234
  import { existsSync } from "fs";
1235
+ import { resolve as resolve3 } from "path";
1236
+ import { loadPolicySet as loadPolicySet3, readPolicyRegistry as readPolicyRegistry4 } from "@memnox/core";
1237
+
1238
+ // src/policy-registry.ts
1239
+ import { mkdir, writeFile } from "fs/promises";
1240
+ import { dirname as dirname2, join as join3, resolve as resolve2 } from "path";
1241
+ import { readPolicyRegistry as readPolicyRegistry3 } from "@memnox/core";
1242
+ var CONFIG_DIR2 = ".memnox";
1243
+ var REGISTRY_FILE3 = "policies.json";
1244
+ var DIR_MODE = 448;
1245
+ function policyRegistryPath(homeDir) {
1246
+ return join3(homeDir, CONFIG_DIR2, REGISTRY_FILE3);
1247
+ }
1248
+ async function registerPolicyFile(homeDir, filePath) {
1249
+ const absolute = resolve2(filePath);
1250
+ const existing = await readPolicyRegistry3(policyRegistryPath(homeDir));
1251
+ if (existing.includes(absolute)) return existing;
1252
+ const files = [...existing, absolute];
1253
+ const path = policyRegistryPath(homeDir);
1254
+ await mkdir(dirname2(path), { recursive: true, mode: DIR_MODE });
1255
+ await writeFile(path, `${JSON.stringify({ files }, null, 2)}
1256
+ `, "utf8");
1257
+ return files;
1258
+ }
1259
+ async function forgetPolicyFiles(homeDir, drop) {
1260
+ const gone = new Set(drop);
1261
+ const path = policyRegistryPath(homeDir);
1262
+ const files = (await readPolicyRegistry3(path)).filter((file) => !gone.has(file));
1263
+ await mkdir(dirname2(path), { recursive: true, mode: DIR_MODE });
1264
+ await writeFile(path, `${JSON.stringify({ files }, null, 2)}
1265
+ `, "utf8");
1266
+ return files;
1267
+ }
1268
+
1269
+ // src/policy-path.ts
1183
1270
  var POLICY_FILES = ["memnox.policies.toml", "memnox.policies.yaml"];
1184
1271
  function resolvePolicyFile(explicit, exists2 = existsSync) {
1185
1272
  if (explicit !== void 0) return explicit;
1186
1273
  return POLICY_FILES.find((candidate) => exists2(candidate)) ?? POLICY_FILES[0];
1187
1274
  }
1275
+ async function policyFilesInForce(homeDir, explicit, exists2 = existsSync) {
1276
+ const here = resolvePolicyFile(explicit, exists2);
1277
+ if (explicit !== void 0) return [here];
1278
+ const files = new Set(await readPolicyRegistry4(policyRegistryPath(homeDir)));
1279
+ if (exists2(here)) files.add(resolve3(here));
1280
+ return [...files];
1281
+ }
1282
+ async function policySetInForce(homeDir, explicit) {
1283
+ return loadPolicySet3(await policyFilesInForce(homeDir, explicit));
1284
+ }
1285
+ function sayWhatDidNotLoad(context, set) {
1286
+ for (const broken of set.unreadable) {
1287
+ const count = broken.issues.length;
1288
+ context.out.note(
1289
+ `${broken.file} would not load \u2014 ${count} problem${count === 1 ? "" : "s"}, so its rules are not in force.`
1290
+ );
1291
+ context.out.note(` fix them with "memnox policy check --fix ${broken.file}"`);
1292
+ }
1293
+ }
1188
1294
 
1189
1295
  // src/commands/check.command.ts
1190
- var REGISTRY_FILE3 = "policies.json";
1191
1296
  function registerCheckCommand(program, context, now = () => (/* @__PURE__ */ new Date()).toISOString()) {
1192
1297
  program.command("check <intent>").description(
1193
1298
  "Decide before the loop starts: what this would do, and what would stop it"
@@ -1203,7 +1308,8 @@ function registerCheckCommand(program, context, now = () => (/* @__PURE__ */ new
1203
1308
  out.line("Nothing on this machine does that, so there is nothing to decide.");
1204
1309
  return;
1205
1310
  }
1206
- const gate = await buildGate(options.agent, now());
1311
+ const { gate, rules: rules2 } = await buildGate(options.agent, now());
1312
+ sayWhatDidNotLoad(context, rules2);
1207
1313
  const rulings = preflight.actions.map((action) => ({
1208
1314
  action: action.action,
1209
1315
  verdict: gate.evaluate({
@@ -1219,16 +1325,15 @@ function registerCheckCommand(program, context, now = () => (/* @__PURE__ */ new
1219
1325
  }
1220
1326
  async function buildGate(agent, moment) {
1221
1327
  const home = homedir5();
1222
- const registered = await readPolicyRegistry3(join3(home, MEMNOX_HOME2, REGISTRY_FILE3));
1223
- const here = resolvePolicyFile();
1224
- const files = new Set(registered);
1225
- if (existsSync2(here)) files.add(here);
1226
- const set = await loadPolicySet3([...files]);
1328
+ const rules2 = await policySetInForce(home);
1227
1329
  const overlays = await overlaysInForce(home);
1228
- return new LocalGate(set.policies, {
1229
- agentName: agent,
1230
- stateFacts: stateFactsInForce(overlays, moment)
1231
- });
1330
+ return {
1331
+ rules: rules2,
1332
+ gate: new LocalGate(rules2.policies, {
1333
+ agentName: agent,
1334
+ stateFacts: stateFactsInForce(overlays, moment)
1335
+ })
1336
+ };
1232
1337
  }
1233
1338
  var ORDER = {
1234
1339
  [DECISION_EFFECT2.DENY]: 0,
@@ -1249,16 +1354,35 @@ function render2(context, preflight, rulings) {
1249
1354
  const shown = [...rulings].sort(
1250
1355
  (a, b) => (ORDER[a.verdict.effect] ?? 3) - (ORDER[b.verdict.effect] ?? 3)
1251
1356
  );
1357
+ const reasons = new Set(rulings.map((ruling) => ruling.verdict.reason));
1358
+ const shared = reasons.size === 1 ? [...reasons][0] : void 0;
1252
1359
  for (const ruling of shown) {
1253
1360
  const effect = ruling.verdict.effect;
1254
1361
  const label = effect === DECISION_EFFECT2.DENY ? style.warn("deny ") : effect === DECISION_EFFECT2.ASK ? style.warn("ask ") : style.dim("allow");
1255
- out.line(
1256
- ` ${label} ${ruling.action.padEnd(width)}${style.dim(ruling.verdict.reason)}`
1257
- );
1362
+ const reason = shared === void 0 ? style.dim(ruling.verdict.reason) : "";
1363
+ out.line(` ${label} ${ruling.action.padEnd(width)}${reason}`.trimEnd());
1364
+ }
1365
+ if (shared !== void 0) {
1366
+ out.line("");
1367
+ out.line(` ${style.dim(shared)}`);
1258
1368
  }
1259
1369
  out.line("");
1260
1370
  if (stopped.length === 0) {
1261
- out.line(`Nothing here would stop. ${rulings.length} action(s) checked.`);
1371
+ const unruled = rulings.filter(
1372
+ (ruling) => ruling.verdict.matchedPolicies.length === 0
1373
+ ).length;
1374
+ if (unruled === rulings.length) {
1375
+ out.line(
1376
+ `Nothing here would stop, and no rule covers any of it. ${rulings.length} action(s) checked.`
1377
+ );
1378
+ out.line(
1379
+ ` ${style.dim("memnox protect")} put the dangerous ones behind ask or deny`
1380
+ );
1381
+ return;
1382
+ }
1383
+ out.line(
1384
+ unruled === 0 ? `Nothing here would stop. ${rulings.length} action(s) checked.` : `Nothing here would stop. ${rulings.length} action(s) checked, ${unruled} of them covered by no rule.`
1385
+ );
1262
1386
  return;
1263
1387
  }
1264
1388
  out.line(
@@ -1278,22 +1402,22 @@ import { homedir as homedir6 } from "os";
1278
1402
 
1279
1403
  // src/memnox-paths.ts
1280
1404
  import { join as join4 } from "path";
1281
- import { MEMNOX_HOME as MEMNOX_HOME3 } from "@memnox/core";
1405
+ import { MEMNOX_HOME as MEMNOX_HOME2 } from "@memnox/core";
1282
1406
  var GUARD_DIR = "guard";
1283
1407
  var GUARD_PROFILE = "memnox.sb";
1284
1408
  var TRANSCRIPT_DIR = "transcripts";
1285
1409
  var BACKUP_DIR = "backup";
1286
1410
  function guardProfilePath(home) {
1287
- return join4(home, MEMNOX_HOME3, GUARD_DIR, GUARD_PROFILE);
1411
+ return join4(home, MEMNOX_HOME2, GUARD_DIR, GUARD_PROFILE);
1288
1412
  }
1289
1413
  function landlockRulesetPath(home) {
1290
- return join4(home, MEMNOX_HOME3, GUARD_DIR, "landlock.json");
1414
+ return join4(home, MEMNOX_HOME2, GUARD_DIR, "landlock.json");
1291
1415
  }
1292
1416
  function transcriptPathFor(home, sessionId) {
1293
- return join4(home, MEMNOX_HOME3, TRANSCRIPT_DIR, `${sessionId}.log`);
1417
+ return join4(home, MEMNOX_HOME2, TRANSCRIPT_DIR, `${sessionId}.log`);
1294
1418
  }
1295
1419
  function backupPathFor(home, path) {
1296
- return join4(home, MEMNOX_HOME3, BACKUP_DIR, path.replace(/[/\\ ]/g, "_"));
1420
+ return join4(home, MEMNOX_HOME2, BACKUP_DIR, path.replace(/[/\\ ]/g, "_"));
1297
1421
  }
1298
1422
 
1299
1423
  // src/commands/trace.command.ts
@@ -1371,9 +1495,9 @@ async function renderStreams(context, event, home) {
1371
1495
 
1372
1496
  // src/commands/explain.command.ts
1373
1497
  import { cwd } from "process";
1374
- import { existsSync as existsSync4 } from "fs";
1498
+ import { existsSync as existsSync3 } from "fs";
1375
1499
  import { homedir as homedir7 } from "os";
1376
- import { join as join8 } from "path";
1500
+ import { join as join7 } from "path";
1377
1501
  import {
1378
1502
  actionForVerb,
1379
1503
  hasTag,
@@ -1393,9 +1517,9 @@ import {
1393
1517
  } from "@memnox/core";
1394
1518
 
1395
1519
  // src/health-probe.ts
1396
- import { existsSync as existsSync3 } from "fs";
1520
+ import { existsSync as existsSync2 } from "fs";
1397
1521
  import { readdir, readFile as readFile2, stat } from "fs/promises";
1398
- import { delimiter, join as join6 } from "path";
1522
+ import { delimiter, join as join5 } from "path";
1399
1523
  import {
1400
1524
  CONFIG_FORMAT,
1401
1525
  configPathFor,
@@ -1422,39 +1546,6 @@ import {
1422
1546
  realPath,
1423
1547
  resolveReal
1424
1548
  } from "@memnox/interceptors";
1425
-
1426
- // src/policy-registry.ts
1427
- import { mkdir, writeFile } from "fs/promises";
1428
- import { dirname as dirname2, join as join5, resolve as resolve2 } from "path";
1429
- import { readPolicyRegistry as readPolicyRegistry4 } from "@memnox/core";
1430
- var CONFIG_DIR2 = ".memnox";
1431
- var REGISTRY_FILE4 = "policies.json";
1432
- var DIR_MODE = 448;
1433
- function policyRegistryPath(homeDir) {
1434
- return join5(homeDir, CONFIG_DIR2, REGISTRY_FILE4);
1435
- }
1436
- async function registerPolicyFile(homeDir, filePath) {
1437
- const absolute = resolve2(filePath);
1438
- const existing = await readPolicyRegistry4(policyRegistryPath(homeDir));
1439
- if (existing.includes(absolute)) return existing;
1440
- const files = [...existing, absolute];
1441
- const path = policyRegistryPath(homeDir);
1442
- await mkdir(dirname2(path), { recursive: true, mode: DIR_MODE });
1443
- await writeFile(path, `${JSON.stringify({ files }, null, 2)}
1444
- `, "utf8");
1445
- return files;
1446
- }
1447
- async function forgetPolicyFiles(homeDir, drop) {
1448
- const gone = new Set(drop);
1449
- const path = policyRegistryPath(homeDir);
1450
- const files = (await readPolicyRegistry4(path)).filter((file) => !gone.has(file));
1451
- await mkdir(dirname2(path), { recursive: true, mode: DIR_MODE });
1452
- await writeFile(path, `${JSON.stringify({ files }, null, 2)}
1453
- `, "utf8");
1454
- return files;
1455
- }
1456
-
1457
- // src/health-probe.ts
1458
1549
  var RULE_FILES = POLICY_FILES;
1459
1550
  async function exists(path) {
1460
1551
  try {
@@ -1466,7 +1557,7 @@ async function exists(path) {
1466
1557
  }
1467
1558
  async function rules(dir) {
1468
1559
  for (const name of RULE_FILES) {
1469
- const path = join6(dir, name);
1560
+ const path = join5(dir, name);
1470
1561
  if (!await exists(path)) continue;
1471
1562
  try {
1472
1563
  const policies = await loadPoliciesFromFile(path);
@@ -1490,7 +1581,7 @@ async function proxyWiring(home, dir) {
1490
1581
  let servers = 0;
1491
1582
  let wrapped = 0;
1492
1583
  for (const location of MCP_CONFIG_LOCATIONS) {
1493
- const path = join6(location.scope === "home" ? home : dir, location.relative);
1584
+ const path = join5(location.scope === "home" ? home : dir, location.relative);
1494
1585
  if (!await exists(path)) continue;
1495
1586
  let raw;
1496
1587
  try {
@@ -1587,13 +1678,13 @@ async function readLedger(home) {
1587
1678
  function presentBinaries(home, env) {
1588
1679
  const path = realPath(env["PATH"] ?? "", home);
1589
1680
  return interceptedBinaries().filter(
1590
- (binary) => resolveReal(binary, path, existsSync3) !== null
1681
+ (binary) => resolveReal(binary, path, existsSync2) !== null
1591
1682
  );
1592
1683
  }
1593
1684
 
1594
1685
  // src/protect/shell-profile.ts
1595
1686
  import { readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
1596
- import { basename, join as join7 } from "path";
1687
+ import { basename, join as join6 } from "path";
1597
1688
  import { interceptorDirFor as interceptorDirFor2 } from "@memnox/interceptors";
1598
1689
  var BEGIN = "# >>> memnox >>>";
1599
1690
  var END = "# <<< memnox <<<";
@@ -1606,7 +1697,7 @@ function profilesFor(shell, home) {
1606
1697
  const name = basename(shell || "zsh");
1607
1698
  const known = Object.entries(PROFILES).find(([key]) => name.includes(key));
1608
1699
  const relative2 = known === void 0 ? PROFILES["zsh"] : known[1];
1609
- return relative2.map((each) => join7(home, each));
1700
+ return relative2.map((each) => join6(home, each));
1610
1701
  }
1611
1702
  function pathLineFor(shell, home) {
1612
1703
  const directory = interceptorDirFor2(home);
@@ -1681,7 +1772,7 @@ function renderTrace(context, trace) {
1681
1772
  );
1682
1773
  out.line("");
1683
1774
  }
1684
- async function answerQuestion(question, inventory, policyFile) {
1775
+ async function answerQuestion(question, inventory, rules2, here) {
1685
1776
  const agent = inventory.agents.find(
1686
1777
  (each) => each.kind.toLowerCase().includes(question.agent.toLowerCase())
1687
1778
  );
@@ -1703,14 +1794,14 @@ async function answerQuestion(question, inventory, policyFile) {
1703
1794
  const technically = matchingPath.length > 0 ? `yes \u2014 ${matchingPath[0]?.path} is reachable by ${agent.kind}` : reachesShell ? `yes \u2014 ${agent.kind} holds a shell, which reaches anything you can` : tools.length > 0 ? `unclear \u2014 ${tools.length} tool(s) reachable, none named for ${question.resource}` : `no \u2014 nothing ${agent.kind} holds here reaches ${question.resource}`;
1704
1795
  const runtime = reachesShell ? "a shell is present, so the runtime restricts nothing by itself" : `${tools.length} tool(s) across ${inventory.mcpServers.length} server(s)`;
1705
1796
  const action = actionForVerb(question.verb);
1706
- if (!existsSync4(policyFile)) {
1797
+ if (rules2.policies.length === 0) {
1707
1798
  return {
1708
1799
  technically,
1709
1800
  runtime,
1710
- policy: `no rules at ${policyFile} \u2014 nothing here would stop it`
1801
+ policy: `no rules at ${here} \u2014 nothing here would stop it`
1711
1802
  };
1712
1803
  }
1713
- const gate = await LocalGate2.fromFiles([policyFile], { agentName: agent.kind });
1804
+ const gate = new LocalGate2(rules2.policies, { agentName: agent.kind });
1714
1805
  const verdict = gate.evaluate({ action, target: question.resource });
1715
1806
  const rule = verdict.matchedPolicies[0];
1716
1807
  const named = rule === void 0 ? "no rule matched" : `rule ${rule.name}`;
@@ -1760,11 +1851,14 @@ function registerExplainCommand(program, context, buildSeams = defaultScanSeams)
1760
1851
  if (subject.trim().includes(" ")) {
1761
1852
  const { question, error } = parseQuestion(subject);
1762
1853
  if (question === void 0) throw new Error(error);
1854
+ const rules2 = await policySetInForce(homedir7(), options.file);
1763
1855
  const answer = await answerQuestion(
1764
1856
  question,
1765
1857
  inventoryOf2(report3, snapshot.takenAt),
1858
+ rules2,
1766
1859
  resolvePolicyFile(options.file)
1767
1860
  );
1861
+ sayWhatDidNotLoad(context, rules2);
1768
1862
  if (options.json === true) {
1769
1863
  context.out.json({ question, ...answer });
1770
1864
  return;
@@ -1968,8 +2062,8 @@ async function coverageFacts(dir) {
1968
2062
  return {
1969
2063
  interceptorsInstalled: health.interceptorsInstalled.length > 0,
1970
2064
  interceptorsFirstOnPath: health.interceptorDirFirstOnPath,
1971
- gitHooksInstalled: existsSync4(join8(dir, ".git", "hooks", "pre-push")),
1972
- osGuardWritten: existsSync4(guardProfilePath(homedir7())),
2065
+ gitHooksInstalled: existsSync3(join7(dir, ".git", "hooks", "pre-push")),
2066
+ osGuardWritten: existsSync3(guardProfilePath(homedir7())),
1973
2067
  egressProxySet: PROXY_VARS.some((name) => (process.env[name] ?? "") !== ""),
1974
2068
  loginPathConfigured: await loginPathConfigured(
1975
2069
  process.env["SHELL"] ?? "zsh",
@@ -2086,14 +2180,14 @@ function registerConfigCommand(program, context, home = homedir8) {
2086
2180
 
2087
2181
  // src/commands/doctor.command.ts
2088
2182
  import { homedir as homedir9 } from "os";
2089
- import { join as join9 } from "path";
2183
+ import { join as join8 } from "path";
2090
2184
  import {
2091
2185
  chainsFor as chainsFor2,
2092
2186
  CHECK,
2093
2187
  checkInstallation,
2094
2188
  discover as discover2,
2095
2189
  lastProbed,
2096
- MEMNOX_HOME as MEMNOX_HOME4,
2190
+ MEMNOX_HOME as MEMNOX_HOME3,
2097
2191
  NodeMachineReader as NodeMachineReader2,
2098
2192
  NodeSnapshotStore as NodeSnapshotStore2,
2099
2193
  rankAgents,
@@ -2102,7 +2196,7 @@ import {
2102
2196
  withToolsFrom
2103
2197
  } from "@memnox/core";
2104
2198
  var SEVERITY_WIDTH = 10;
2105
- function registerDoctorCommand(program, context, buildReader = () => new NodeMachineReader2(homedir9()), cwd3 = () => process.cwd(), buildSnapshots = () => new NodeSnapshotStore2(join9(homedir9(), MEMNOX_HOME4))) {
2199
+ function registerDoctorCommand(program, context, buildReader = () => new NodeMachineReader2(homedir9()), cwd3 = () => process.cwd(), buildSnapshots = () => new NodeSnapshotStore2(join8(homedir9(), MEMNOX_HOME3))) {
2106
2200
  program.command("doctor").description(
2107
2201
  "What on this machine is risky, why, and the one change that closes each"
2108
2202
  ).option("--json", "emit the findings as JSON").option(
@@ -2158,7 +2252,9 @@ function registerDoctorCommand(program, context, buildReader = () => new NodeMac
2158
2252
  out.line("");
2159
2253
  for (const finding of report3.findings) {
2160
2254
  out.line(` ${severity(style, finding).padEnd(SEVERITY_WIDTH)}${finding.title}`);
2161
- out.line(` ${" ".repeat(SEVERITY_WIDTH)}${style.dim(finding.evidence)}`);
2255
+ if (!finding.title.includes(finding.evidence)) {
2256
+ out.line(` ${" ".repeat(SEVERITY_WIDTH)}${style.dim(finding.evidence)}`);
2257
+ }
2162
2258
  const remediation = finding.remediation;
2163
2259
  if (remediation !== void 0) {
2164
2260
  out.line(
@@ -2245,17 +2341,17 @@ import {
2245
2341
 
2246
2342
  // src/protect/harden-state.ts
2247
2343
  import { homedir as homedir10 } from "os";
2248
- import { join as join10 } from "path";
2344
+ import { join as join9 } from "path";
2249
2345
  import {
2250
2346
  HARDEN_TARGET,
2251
- MEMNOX_HOME as MEMNOX_HOME5,
2347
+ MEMNOX_HOME as MEMNOX_HOME4,
2252
2348
  NodeHardenWriter,
2253
2349
  NodeMachineReader as NodeMachineReader3,
2254
2350
  NodeSnapshotStore as NodeSnapshotStore3
2255
2351
  } from "@memnox/core";
2256
2352
  function defaultSeams() {
2257
2353
  const home = homedir10();
2258
- const root = join10(home, MEMNOX_HOME5);
2354
+ const root = join9(home, MEMNOX_HOME4);
2259
2355
  return {
2260
2356
  reader: new NodeMachineReader3(home),
2261
2357
  writer: new NodeHardenWriter(root),
@@ -2264,7 +2360,7 @@ function defaultSeams() {
2264
2360
  registerPolicy: async (path) => {
2265
2361
  await registerPolicyFile(home, path);
2266
2362
  },
2267
- absolute: (path) => join10(root, path)
2363
+ absolute: (path) => join9(root, path)
2268
2364
  };
2269
2365
  }
2270
2366
  async function registerApplied(seams, applied) {
@@ -2344,10 +2440,10 @@ async function runInteractive(context, takeRecommended, ask) {
2344
2440
  }
2345
2441
 
2346
2442
  // src/protect/native-permissions.ts
2347
- import { existsSync as existsSync5 } from "fs";
2443
+ import { existsSync as existsSync4 } from "fs";
2348
2444
  import { readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
2349
2445
  import { homedir as homedir12 } from "os";
2350
- import { join as join11 } from "path";
2446
+ import { join as join10 } from "path";
2351
2447
  import {
2352
2448
  applyNative,
2353
2449
  applyOpenClaw,
@@ -2365,7 +2461,7 @@ import {
2365
2461
  var TARGETS = [
2366
2462
  {
2367
2463
  product: "Claude Code",
2368
- path: join11(".claude", "settings.json"),
2464
+ path: join10(".claude", "settings.json"),
2369
2465
  apply: (raw, policies) => {
2370
2466
  const translation = toClaudeCodePermissions(policies);
2371
2467
  const { allow, ask, deny } = translation.permissions;
@@ -2379,7 +2475,7 @@ var TARGETS = [
2379
2475
  },
2380
2476
  {
2381
2477
  product: "OpenClaw",
2382
- path: join11(".openclaw", "openclaw.json"),
2478
+ path: join10(".openclaw", "openclaw.json"),
2383
2479
  apply: (raw, policies) => {
2384
2480
  const translation = toOpenClawTools(policies);
2385
2481
  const { allow, deny } = translation.tools;
@@ -2400,7 +2496,7 @@ var TARGETS = [
2400
2496
  },
2401
2497
  {
2402
2498
  product: "Hermes",
2403
- path: join11(".hermes", "config.yaml"),
2499
+ path: join10(".hermes", "config.yaml"),
2404
2500
  text: true,
2405
2501
  apply: (raw, policies) => {
2406
2502
  const translation = toHermesApprovals(
@@ -2427,11 +2523,11 @@ function json(value) {
2427
2523
  async function runNative(context, reverting, home = homedir12) {
2428
2524
  const present = TARGETS.map((target) => ({
2429
2525
  target,
2430
- path: join11(home(), target.path)
2431
- })).filter((each) => existsSync5(each.path));
2526
+ path: join10(home(), target.path)
2527
+ })).filter((each) => existsSync4(each.path));
2432
2528
  if (present.length === 0) {
2433
2529
  throw new Error(
2434
- "No agent here publishes a permission file Memnox can write. Looked for:\n" + TARGETS.map((each) => ` ${join11(home(), each.path)} (${each.product})`).join(
2530
+ "No agent here publishes a permission file Memnox can write. Looked for:\n" + TARGETS.map((each) => ` ${join10(home(), each.path)} (${each.product})`).join(
2435
2531
  "\n"
2436
2532
  )
2437
2533
  );
@@ -2475,17 +2571,17 @@ async function runNative(context, reverting, home = homedir12) {
2475
2571
  }
2476
2572
  async function rulesToWrite() {
2477
2573
  const rules2 = resolvePolicyFile();
2478
- if (!existsSync5(rules2)) {
2574
+ if (!existsSync4(rules2)) {
2479
2575
  throw new Error(`No rules at ${rules2} to write. Try "memnox protect --interactive".`);
2480
2576
  }
2481
2577
  return loadPoliciesFromFile2(rules2);
2482
2578
  }
2483
2579
 
2484
2580
  // src/protect/seam-install.ts
2485
- import { existsSync as existsSync6, realpathSync } from "fs";
2581
+ import { existsSync as existsSync5, realpathSync } from "fs";
2486
2582
  import { mkdir as mkdir2, writeFile as writeFile4 } from "fs/promises";
2487
2583
  import { homedir as homedir13, release } from "os";
2488
- import { basename as basename2, dirname as dirname3, join as join12 } from "path";
2584
+ import { basename as basename2, dirname as dirname3, join as join11 } from "path";
2489
2585
  import {
2490
2586
  guardFor,
2491
2587
  guardPlanFrom,
@@ -2544,7 +2640,7 @@ async function runOsGuard(context, repoDir, home = homedir13()) {
2544
2640
  const { out, style } = context;
2545
2641
  const support = guardFor(process.platform, release());
2546
2642
  const file = resolvePolicyFile();
2547
- if (!existsSync6(file)) {
2643
+ if (!existsSync5(file)) {
2548
2644
  throw new Error(`No rules at ${file}. Write some first: memnox protect --yes`);
2549
2645
  }
2550
2646
  const plan = guardPlanFrom(await loadPoliciesFromFile3(file), home, [repoDir]);
@@ -2618,7 +2714,7 @@ async function runPathLine(context, reverting, env = process.env) {
2618
2714
  );
2619
2715
  }
2620
2716
  async function firstExisting(paths) {
2621
- for (const path of paths) if (existsSync6(path)) return path;
2717
+ for (const path of paths) if (existsSync5(path)) return path;
2622
2718
  return null;
2623
2719
  }
2624
2720
  function throughSymlinks(policy) {
@@ -2636,7 +2732,7 @@ function resolveThrough(path) {
2636
2732
  const tail = [];
2637
2733
  while (head !== dirname3(head)) {
2638
2734
  try {
2639
- return join12(realpathSync(head), ...tail);
2735
+ return join11(realpathSync(head), ...tail);
2640
2736
  } catch {
2641
2737
  tail.unshift(basename2(head));
2642
2738
  head = dirname3(head);
@@ -2972,7 +3068,8 @@ function render4(context, event) {
2972
3068
  );
2973
3069
  out.line("");
2974
3070
  row(context.out, "when", event.at);
2975
- row(context.out, "agent", `${event.agent} (${event.actorType})`);
3071
+ const named = event.agent.toLowerCase().includes(event.actorType.toLowerCase());
3072
+ row(context.out, "agent", named ? event.agent : `${event.agent} (${event.actorType})`);
2976
3073
  row(context.out, "surface", event.surface);
2977
3074
  row(context.out, "class", event.class);
2978
3075
  row(context.out, "reason", event.reason);
@@ -3455,7 +3552,7 @@ function registerFreezeCommand(program, context, home = homedir20, now = () => /
3455
3552
 
3456
3553
  // src/commands/lock.command.ts
3457
3554
  import { homedir as homedir21 } from "os";
3458
- import { relative, resolve as resolve3 } from "path";
3555
+ import { relative, resolve as resolve4 } from "path";
3459
3556
  import {
3460
3557
  DEFAULT_LEASE_MINUTES,
3461
3558
  LEASE_OUTCOME,
@@ -3520,7 +3617,7 @@ function registerLockCommand(program, context, home = homedir21, now = () => /*
3520
3617
  "A lease is repository-relative, and this is not a repository."
3521
3618
  );
3522
3619
  }
3523
- const wanted = normalizeLeasePath(relative(root, resolve3(process.cwd(), path)));
3620
+ const wanted = normalizeLeasePath(relative(root, resolve4(process.cwd(), path)));
3524
3621
  if (wanted === null) {
3525
3622
  throw new Error(`${path} is outside this repository, so nothing can lease it.`);
3526
3623
  }
@@ -3743,7 +3840,6 @@ function asUnit(value) {
3743
3840
 
3744
3841
  // src/commands/next.command.ts
3745
3842
  import { homedir as homedir25 } from "os";
3746
- import { existsSync as existsSync7 } from "fs";
3747
3843
  import {
3748
3844
  delegations,
3749
3845
  describeLevel,
@@ -3773,8 +3869,8 @@ function registerNextCommand(program, context, home = homedir25, now = () => /*
3773
3869
  const ready = promotable(found);
3774
3870
  const asked = interruptions(events, week);
3775
3871
  const config = await loadOrCreateConfig6(home());
3776
- const file = resolvePolicyFile();
3777
- const gate = existsSync7(file) ? await LocalGate3.fromFiles([file], { agentName: "agent" }) : null;
3872
+ const rules2 = await policySetInForce(home());
3873
+ const gate = rules2.policies.length === 0 ? null : new LocalGate3(rules2.policies, { agentName: "agent" });
3778
3874
  const standing = standingOf({
3779
3875
  enforcing: config.mode === ENFORCEMENT_MODE2.ENFORCE,
3780
3876
  rules: gate?.rules().length ?? 0,
@@ -3952,6 +4048,7 @@ function merge(accepted, wanted, at2) {
3952
4048
  }
3953
4049
  return [...by.values()];
3954
4050
  }
4051
+ var NEW_SHOWN = 12;
3955
4052
  function render9(context, findings) {
3956
4053
  const { out, style } = context;
3957
4054
  if (findings.length === 0) {
@@ -3973,7 +4070,18 @@ function render9(context, findings) {
3973
4070
  out.line("");
3974
4071
  out.line(style.bold("NEW"));
3975
4072
  out.line("");
3976
- for (const skill of fresh) out.line(` ${style.dim("+")} ${describeSkill(skill)}`);
4073
+ const ordered = [...fresh].sort((a, b) => b.reaches.length - a.reaches.length);
4074
+ for (const skill of ordered.slice(0, NEW_SHOWN)) {
4075
+ out.line(` ${style.dim("+")} ${describeSkill(skill)}`);
4076
+ }
4077
+ const rest = ordered.length - NEW_SHOWN;
4078
+ if (rest > 0) {
4079
+ const quiet = ordered.slice(NEW_SHOWN).filter((each) => each.reaches.length === 0);
4080
+ out.line(
4081
+ ` ${style.dim(`\u2026 and ${rest} more, ${quiet.length} of which name no tool this knows`)}`
4082
+ );
4083
+ out.note('"memnox skills --json" lists every one.');
4084
+ }
3977
4085
  }
3978
4086
  const known = findings.length - held.length - fresh.length;
3979
4087
  out.line("");
@@ -3985,14 +4093,13 @@ function render9(context, findings) {
3985
4093
  }
3986
4094
 
3987
4095
  // src/commands/autopilot.command.ts
3988
- import { existsSync as existsSync8 } from "fs";
4096
+ import { homedir as homedir28 } from "os";
3989
4097
  import {
3990
4098
  BAND,
3991
4099
  LocalGate as LocalGate4,
3992
4100
  actionsForCli,
3993
4101
  blastRadiusOf,
3994
4102
  boundaryFor,
3995
- loadPoliciesFromFile as loadPoliciesFromFile4,
3996
4103
  rolesIn,
3997
4104
  standingFor,
3998
4105
  inBand,
@@ -4002,19 +4109,25 @@ import {
4002
4109
  function registerAutopilotCommand(program, context) {
4003
4110
  program.command("autopilot [agent]").description("What an agent would do on its own, and what would still be asked").option("-f, --file <path>", "policy file (default: whichever exists)").option("--role <name>", "show the boundary of a job rather than of a product").option("--roles", "every job the rules name, and what each may do").option("--json", "machine-readable output").action(
4004
4111
  async (agent, options) => {
4005
- const file = resolvePolicyFile(options.file);
4006
- if (!existsSync8(file)) {
4112
+ const rules2 = await policySetInForce(homedir28(), options.file);
4113
+ if (rules2.policies.length === 0) {
4007
4114
  throw new Error(
4008
- `No rules at ${file}, so there is no boundary to show. Write one: memnox protect --yes`
4115
+ `No rules at ${resolvePolicyFile(options.file)}, so there is no boundary to show. Write one: memnox protect --yes`
4009
4116
  );
4010
4117
  }
4118
+ sayWhatDidNotLoad(context, rules2);
4011
4119
  const candidatesFor = () => interceptedBinaries2().flatMap((binary) => actionsForCli(binary));
4012
4120
  if (options.roles === true) {
4013
- await renderWorkforce(context, file, candidatesFor(), options.json === true);
4121
+ await renderWorkforce(
4122
+ context,
4123
+ rules2.policies,
4124
+ candidatesFor(),
4125
+ options.json === true
4126
+ );
4014
4127
  return;
4015
4128
  }
4016
4129
  const name = agent ?? "claude-code";
4017
- const gate = await LocalGate4.fromFiles([file], {
4130
+ const gate = new LocalGate4(rules2.policies, {
4018
4131
  agentName: name,
4019
4132
  /* Evaluated as the job, so a `roles:` rule fires. Without this the screen
4020
4133
  would show what the product may do and call it what the role may do. */
@@ -4107,8 +4220,8 @@ function band(context, title, entries, mark) {
4107
4220
  context.out.line(` ${context.style.dim(`and ${entries.length - 12} more`)}`);
4108
4221
  }
4109
4222
  }
4110
- async function renderWorkforce(context, file, candidates, asJson) {
4111
- const roles = rolesIn(await loadPoliciesFromFile4(file));
4223
+ async function renderWorkforce(context, policies, candidates, asJson) {
4224
+ const roles = rolesIn([...policies]);
4112
4225
  const { out, style } = context;
4113
4226
  if (roles.length === 0) {
4114
4227
  out.line("No rule here names a job, so there is no workforce to show.");
@@ -4119,7 +4232,7 @@ async function renderWorkforce(context, file, candidates, asJson) {
4119
4232
  }
4120
4233
  const standings = [];
4121
4234
  for (const role of roles) {
4122
- const gate = await LocalGate4.fromFiles([file], { agentName: role, agentRole: role });
4235
+ const gate = new LocalGate4([...policies], { agentName: role, agentRole: role });
4123
4236
  const boundary = boundaryFor(role, candidates, (action) => {
4124
4237
  const verdict = gate.evaluate({ action });
4125
4238
  return {
@@ -4150,11 +4263,11 @@ async function renderWorkforce(context, file, candidates, asJson) {
4150
4263
 
4151
4264
  // src/commands/env.command.ts
4152
4265
  import { delimiter as delimiter2 } from "path";
4153
- import { homedir as homedir28 } from "os";
4266
+ import { homedir as homedir29 } from "os";
4154
4267
  import { SESSION_VAR as SESSION_VAR2 } from "@memnox/core";
4155
4268
  import { interceptorDirFor as interceptorDirFor4, REAL_SHELL_VAR } from "@memnox/interceptors";
4156
4269
  var FORMATS = ["sh", "systemd", "docker"];
4157
- function registerEnvCommand(program, context, home = homedir28) {
4270
+ function registerEnvCommand(program, context, home = homedir29) {
4158
4271
  program.command("env").description("The environment an agent needs when something else starts it").option("--format <format>", `one of: ${FORMATS.join(", ")}`, "sh").option("--shell <path>", "shell the agent should use", "memnox-shell").option("--session <id>", "group this run in the timeline under one session").action(async (options) => {
4159
4272
  if (!FORMATS.includes(options.format)) {
4160
4273
  throw new Error(`--format takes one of: ${FORMATS.join(", ")}`);
@@ -4202,7 +4315,7 @@ function expand(key, value) {
4202
4315
 
4203
4316
  // src/commands/verify.command.ts
4204
4317
  import { readFile as readFile7 } from "fs/promises";
4205
- import { homedir as homedir29 } from "os";
4318
+ import { homedir as homedir30 } from "os";
4206
4319
  import {
4207
4320
  describeProof,
4208
4321
  enforcementFailed,
@@ -4214,13 +4327,13 @@ import {
4214
4327
 
4215
4328
  // src/verify/enforcement.ts
4216
4329
  import { execFile as execFile3, spawn } from "child_process";
4217
- import { existsSync as existsSync9 } from "fs";
4330
+ import { existsSync as existsSync6 } from "fs";
4218
4331
  import { mkdir as mkdir3, mkdtemp, writeFile as writeFile6 } from "fs/promises";
4219
4332
  import { tmpdir } from "os";
4220
- import { join as join13 } from "path";
4333
+ import { join as join12 } from "path";
4221
4334
  import { promisify as promisify3 } from "util";
4222
4335
  import {
4223
- MEMNOX_HOME as MEMNOX_HOME6,
4336
+ MEMNOX_HOME as MEMNOX_HOME5,
4224
4337
  PROOF,
4225
4338
  PROXY_BINARY as PROXY_BINARY2,
4226
4339
  SURFACE_KIND as SURFACE_KIND2
@@ -4246,12 +4359,12 @@ else if(m.id!==undefined)send({jsonrpc:'2.0',id:m.id,result:{content:[{type:'tex
4246
4359
  }});
4247
4360
  `;
4248
4361
  async function proveEnforcement(context) {
4249
- const sandbox = await mkdtemp(join13(tmpdir(), "memnox-selftest-"));
4250
- const rules2 = join13(sandbox, "memnox.policies.toml");
4362
+ const sandbox = await mkdtemp(join12(tmpdir(), "memnox-selftest-"));
4363
+ const rules2 = join12(sandbox, "memnox.policies.toml");
4251
4364
  await writeFile6(rules2, PROBE_RULES, "utf8");
4252
- await mkdir3(join13(sandbox, MEMNOX_HOME6), { recursive: true, mode: 448 });
4365
+ await mkdir3(join12(sandbox, MEMNOX_HOME5), { recursive: true, mode: 448 });
4253
4366
  await writeFile6(
4254
- join13(sandbox, MEMNOX_HOME6, "policies.json"),
4367
+ join12(sandbox, MEMNOX_HOME5, "policies.json"),
4255
4368
  `${JSON.stringify({ files: [rules2] }, null, 2)}
4256
4369
  `,
4257
4370
  "utf8"
@@ -4266,7 +4379,7 @@ async function proveEnforcement(context) {
4266
4379
  }
4267
4380
  async function proveMcp(sandbox) {
4268
4381
  const seam = SURFACE_KIND2.MCP;
4269
- const stub = join13(sandbox, "stub.mjs");
4382
+ const stub = join12(sandbox, "stub.mjs");
4270
4383
  await writeFile6(stub, STUB_SERVER, "utf8");
4271
4384
  const call = `{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"memnox-self-test","version":"1"}}}
4272
4385
  {"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"memnox_selftest_delete","arguments":{}}}
@@ -4313,14 +4426,14 @@ function resolvedThrough(sandbox) {
4313
4426
  return env;
4314
4427
  }
4315
4428
  function speak(command, args, input, env) {
4316
- return new Promise((resolve5, reject) => {
4429
+ return new Promise((resolve6, reject) => {
4317
4430
  const child = spawn(command, [...args], { env, stdio: ["pipe", "pipe", "pipe"] });
4318
4431
  let out = "";
4319
4432
  child.stdout.on("data", (chunk) => {
4320
4433
  out += chunk.toString();
4321
4434
  });
4322
4435
  child.on("error", reject);
4323
- child.on("close", () => resolve5(out));
4436
+ child.on("close", () => resolve6(out));
4324
4437
  const timer = setTimeout(() => child.kill(), PROBE_MS);
4325
4438
  timer.unref();
4326
4439
  child.stdin.end(input);
@@ -4328,8 +4441,8 @@ function speak(command, args, input, env) {
4328
4441
  }
4329
4442
  async function proveShell(home, sandbox) {
4330
4443
  const seam = SURFACE_KIND2.SHELL;
4331
- const binary = join13(interceptorDirFor5(home), "git");
4332
- if (!existsSync9(binary)) {
4444
+ const binary = join12(interceptorDirFor5(home), "git");
4445
+ if (!existsSync6(binary)) {
4333
4446
  return {
4334
4447
  seam,
4335
4448
  state: PROOF.ABSENT,
@@ -4361,9 +4474,9 @@ function refusedIn(seam, output) {
4361
4474
  };
4362
4475
  }
4363
4476
  function proveGitHook(dir) {
4364
- const hook = join13(dir, ".git", "hooks", "pre-push");
4477
+ const hook = join12(dir, ".git", "hooks", "pre-push");
4365
4478
  const seam = SURFACE_KIND2.GIT;
4366
- if (!existsSync9(hook)) {
4479
+ if (!existsSync6(hook)) {
4367
4480
  return {
4368
4481
  seam,
4369
4482
  state: PROOF.ABSENT,
@@ -4379,7 +4492,7 @@ function proveGitHook(dir) {
4379
4492
  }
4380
4493
  function proveOsGuard(home) {
4381
4494
  const seam = SURFACE_KIND2.FILESYSTEM;
4382
- if (!existsSync9(guardProfilePath(home))) {
4495
+ if (!existsSync6(guardProfilePath(home))) {
4383
4496
  return {
4384
4497
  seam,
4385
4498
  state: PROOF.ABSENT,
@@ -4410,7 +4523,7 @@ function proveEgress() {
4410
4523
  }
4411
4524
 
4412
4525
  // src/commands/verify.command.ts
4413
- function registerVerifyCommand(program, context, probe = proveEnforcement, home = homedir29, dir = () => process.cwd()) {
4526
+ function registerVerifyCommand(program, context, probe = proveEnforcement, home = homedir30, dir = () => process.cwd()) {
4414
4527
  program.command("verify [bundle]").description("Check an exported bundle, or prove the seams actually refuse").option(
4415
4528
  "--enforcement",
4416
4529
  "ask every seam to refuse something, and report what came back"
@@ -4482,8 +4595,8 @@ async function renderEnforcement(context, proofs) {
4482
4595
  }
4483
4596
 
4484
4597
  // src/commands/daemon.command.ts
4485
- import { homedir as homedir30 } from "os";
4486
- import { existsSync as existsSync10 } from "fs";
4598
+ import { homedir as homedir31 } from "os";
4599
+ import { existsSync as existsSync7 } from "fs";
4487
4600
  import { LocalGate as LocalGate5 } from "@memnox/core";
4488
4601
  import { readBudgets as readBudgets4, readFleetSpend, SessionPauses as SessionPauses4 } from "@memnox/core";
4489
4602
 
@@ -4541,9 +4654,9 @@ Use a shorter home directory. The interceptors still evaluate in process, which
4541
4654
  await rm2(path, { force: true });
4542
4655
  const server = createServer((socket) => this.serve(socket));
4543
4656
  this.server = server;
4544
- await new Promise((resolve5, reject) => {
4657
+ await new Promise((resolve6, reject) => {
4545
4658
  server.once("error", reject);
4546
- server.listen(path, () => resolve5());
4659
+ server.listen(path, () => resolve6());
4547
4660
  });
4548
4661
  await chmod(path, 384);
4549
4662
  return path;
@@ -4551,7 +4664,7 @@ Use a shorter home directory. The interceptors still evaluate in process, which
4551
4664
  async close() {
4552
4665
  const server = this.server;
4553
4666
  if (server === null) return;
4554
- await new Promise((resolve5) => server.close(() => resolve5()));
4667
+ await new Promise((resolve6) => server.close(() => resolve6()));
4555
4668
  this.server = null;
4556
4669
  }
4557
4670
  serve(socket) {
@@ -4742,11 +4855,11 @@ import { readAccount as readAccount2 } from "@memnox/core";
4742
4855
 
4743
4856
  // src/sync/heartbeat.ts
4744
4857
  import { readFile as readFile10 } from "fs/promises";
4745
- import { join as join16 } from "path";
4858
+ import { join as join15 } from "path";
4746
4859
  import {
4747
4860
  fleetBudgets,
4748
4861
  HOLD_ANSWER as HOLD_ANSWER2,
4749
- MEMNOX_HOME as MEMNOX_HOME9,
4862
+ MEMNOX_HOME as MEMNOX_HOME8,
4750
4863
  NodeSnapshotStore as NodeSnapshotStore4,
4751
4864
  PendingApprovals as PendingApprovals3,
4752
4865
  readBudgets as readBudgets3,
@@ -4758,10 +4871,10 @@ import { readAccount } from "@memnox/core";
4758
4871
 
4759
4872
  // src/sync/bundle.ts
4760
4873
  import { mkdir as mkdir5, readFile as readFile8, rename, rm as rm3, writeFile as writeFile7 } from "fs/promises";
4761
- import { dirname as dirname5, join as join14 } from "path";
4874
+ import { dirname as dirname5, join as join13 } from "path";
4762
4875
  import {
4763
- loadPoliciesFromFile as loadPoliciesFromFile5,
4764
- MEMNOX_HOME as MEMNOX_HOME7,
4876
+ loadPoliciesFromFile as loadPoliciesFromFile4,
4877
+ MEMNOX_HOME as MEMNOX_HOME6,
4765
4878
  orgConditionsPathFor,
4766
4879
  orgOverlaysFrom,
4767
4880
  writeOrgConditions
@@ -4848,7 +4961,7 @@ var PULL_OUTCOME = {
4848
4961
  LAPSED: "lapsed"
4849
4962
  };
4850
4963
  function orgPolicyPath(home) {
4851
- return join14(home, MEMNOX_HOME7, ORG_POLICY_FILE);
4964
+ return join13(home, MEMNOX_HOME6, ORG_POLICY_FILE);
4852
4965
  }
4853
4966
  async function pullBundle(home, account, held, now = Date.now) {
4854
4967
  const answer = await callCloud({
@@ -4882,7 +4995,7 @@ async function applyBundle(home, bundle, now = Date.now) {
4882
4995
  mode: OWNER_ONLY
4883
4996
  });
4884
4997
  try {
4885
- await loadPoliciesFromFile5(staging);
4998
+ await loadPoliciesFromFile4(staging);
4886
4999
  } catch (err) {
4887
5000
  await rm3(staging, { force: true });
4888
5001
  return {
@@ -4933,11 +5046,11 @@ async function touchOrgConditions(home, at2) {
4933
5046
  // src/sync/push.ts
4934
5047
  import { createSign, sign as signBytes } from "crypto";
4935
5048
  import { readFile as readFile9, writeFile as writeFile8, mkdir as mkdir6 } from "fs/promises";
4936
- import { dirname as dirname6, join as join15 } from "path";
5049
+ import { dirname as dirname6, join as join14 } from "path";
4937
5050
  import {
4938
5051
  DECISION_EFFECT as DECISION_EFFECT8,
4939
5052
  EXECUTION,
4940
- MEMNOX_HOME as MEMNOX_HOME8
5053
+ MEMNOX_HOME as MEMNOX_HOME7
4941
5054
  } from "@memnox/core";
4942
5055
 
4943
5056
  // src/sync/census.ts
@@ -5020,7 +5133,7 @@ var PUSH_OUTCOME = {
5020
5133
  REFUSED: "refused"
5021
5134
  };
5022
5135
  function cursorPathFor(home) {
5023
- return join15(home, MEMNOX_HOME8, CURSOR_FILE);
5136
+ return join14(home, MEMNOX_HOME7, CURSOR_FILE);
5024
5137
  }
5025
5138
  async function readCursor(home) {
5026
5139
  try {
@@ -5256,7 +5369,7 @@ async function onePass(home) {
5256
5369
  const census = await pushCensus(
5257
5370
  home,
5258
5371
  account,
5259
- await new NodeSnapshotStore4(join16(home, MEMNOX_HOME9)).latest()
5372
+ await new NodeSnapshotStore4(join15(home, MEMNOX_HOME8)).latest()
5260
5373
  );
5261
5374
  if (census.outcome === PUSH_OUTCOME.REVOKED) {
5262
5375
  return { pull, push, census, revoked: true };
@@ -5356,10 +5469,10 @@ async function syncLoop(home, running, seams = {}) {
5356
5469
  }
5357
5470
 
5358
5471
  // src/commands/daemon.command.ts
5359
- function registerDaemonCommand(program, context, home = homedir30) {
5472
+ function registerDaemonCommand(program, context, home = homedir31) {
5360
5473
  program.command("daemon").description("Hold the rules in one process, so an interceptor pays a connect").option("-f, --file <path>", "policy file (default: whichever exists)").option("--no-sync", "hold the rules but talk to nothing").action(async (options) => {
5361
5474
  const file = resolvePolicyFile(options.file);
5362
- const gate = existsSync10(file) ? await LocalGate5.fromFiles([file], { agentName: "agent" }) : void 0;
5475
+ const gate = existsSync7(file) ? await LocalGate5.fromFiles([file], { agentName: "agent" }) : void 0;
5363
5476
  const budgets = await readBudgets4(home());
5364
5477
  const spentAlready = budgets.length === 0 ? [] : await withEvents(home(), (store) => store.query({ limit: 2e4 }));
5365
5478
  const daemon = new MemnoxDaemon({
@@ -5391,10 +5504,10 @@ function registerDaemonCommand(program, context, home = homedir30) {
5391
5504
  log: (message) => context.out.note(message)
5392
5505
  });
5393
5506
  }
5394
- await new Promise((resolve5) => {
5507
+ await new Promise((resolve6) => {
5395
5508
  const stop = () => {
5396
5509
  running = false;
5397
- void daemon.close().then(resolve5);
5510
+ void daemon.close().then(resolve6);
5398
5511
  };
5399
5512
  process.once("SIGINT", stop);
5400
5513
  process.once("SIGTERM", stop);
@@ -5403,17 +5516,17 @@ function registerDaemonCommand(program, context, home = homedir30) {
5403
5516
  }
5404
5517
 
5405
5518
  // src/commands/uninstall.command.ts
5406
- import { homedir as homedir31 } from "os";
5519
+ import { homedir as homedir32 } from "os";
5407
5520
  import { cwd as cwd2 } from "process";
5408
- import { existsSync as existsSync11 } from "fs";
5521
+ import { existsSync as existsSync8 } from "fs";
5409
5522
  import { rm as rm4 } from "fs/promises";
5410
- import { join as join17 } from "path";
5523
+ import { join as join16 } from "path";
5411
5524
  import {
5412
5525
  fleetSpendPathFor,
5413
5526
  LEASE_OUTCOME as LEASE_OUTCOME2,
5414
5527
  LeaseRegistry as LeaseRegistry3,
5415
5528
  leaseDirFor,
5416
- MEMNOX_HOME as MEMNOX_HOME10,
5529
+ MEMNOX_HOME as MEMNOX_HOME9,
5417
5530
  pauseDirFor,
5418
5531
  pendingDirFor,
5419
5532
  taskDirFor
@@ -5433,7 +5546,7 @@ async function clearOperationalState(home) {
5433
5546
  ];
5434
5547
  let cleared = 0;
5435
5548
  for (const path of paths) {
5436
- if (!existsSync11(path)) continue;
5549
+ if (!existsSync8(path)) continue;
5437
5550
  await rm4(path, { recursive: true, force: true });
5438
5551
  cleared += 1;
5439
5552
  }
@@ -5455,7 +5568,7 @@ async function releaseEveryLease(home, now) {
5455
5568
  }
5456
5569
  function registerUninstallCommand(program, context, deps = {}) {
5457
5570
  program.command("uninstall").description("Remove the interceptors, the hooks and the wrapping from this machine").option("--purge", "also delete ~/.memnox, including the history and your rules").action(async (options) => {
5458
- const home = (deps.home ?? homedir31)();
5571
+ const home = (deps.home ?? homedir32)();
5459
5572
  const dir = (deps.dir ?? cwd2)();
5460
5573
  const { out } = context;
5461
5574
  const interceptors = await removeInterceptors(home);
@@ -5488,14 +5601,14 @@ function registerUninstallCommand(program, context, deps = {}) {
5488
5601
  }
5489
5602
  if (options.purge !== true) {
5490
5603
  out.line("");
5491
- out.line(`Your rules and history are still in ${join17(home, MEMNOX_HOME10)}.`);
5604
+ out.line(`Your rules and history are still in ${join16(home, MEMNOX_HOME9)}.`);
5492
5605
  out.line("Add --purge to delete those too.");
5493
5606
  return;
5494
5607
  }
5495
- await rm4(join17(home, MEMNOX_HOME10), { recursive: true, force: true });
5608
+ await rm4(join16(home, MEMNOX_HOME9), { recursive: true, force: true });
5496
5609
  out.line("");
5497
- out.line(`Deleted ${join17(home, MEMNOX_HOME10)}.`);
5498
- const rules2 = POLICY_FILES.map((name) => join17(dir, name)).filter(existsSync11);
5610
+ out.line(`Deleted ${join16(home, MEMNOX_HOME9)}.`);
5611
+ const rules2 = POLICY_FILES.map((name) => join16(dir, name)).filter(existsSync8);
5499
5612
  if (rules2.length === 0) {
5500
5613
  out.line("Nothing of Memnox is left on this machine.");
5501
5614
  } else {
@@ -5513,11 +5626,11 @@ function registerUninstallCommand(program, context, deps = {}) {
5513
5626
 
5514
5627
  // src/commands/run.command.ts
5515
5628
  import { spawn as spawn2 } from "child_process";
5516
- import { createWriteStream, existsSync as existsSync12, mkdirSync } from "fs";
5629
+ import { createWriteStream, existsSync as existsSync9, mkdirSync } from "fs";
5517
5630
  import { join as join18 } from "path";
5518
- import { homedir as homedir32, release as release2 } from "os";
5631
+ import { homedir as homedir33, release as release2 } from "os";
5519
5632
  import { randomUUID } from "crypto";
5520
- import { delimiter as delimiter3 } from "path";
5633
+ import { delimiter as delimiter4 } from "path";
5521
5634
  import {
5522
5635
  guardFor as guardFor2,
5523
5636
  isEmptyScope,
@@ -5536,12 +5649,40 @@ import {
5536
5649
  interceptorDirFor as interceptorDirFor7,
5537
5650
  REAL_SHELL_VAR as REAL_SHELL_VAR2
5538
5651
  } from "@memnox/interceptors";
5652
+
5653
+ // src/on-path.ts
5654
+ import { accessSync, constants } from "fs";
5655
+ import { delimiter as delimiter3, join as join17 } from "path";
5656
+ import { DISCOVERED_AGENT_KIND } from "@memnox/core";
5657
+ function onPath(binary, path = process.env["PATH"] ?? "") {
5658
+ if (binary.includes("/")) return executable(binary);
5659
+ return path.split(delimiter3).some((entry) => entry !== "" && executable(join17(entry, binary)));
5660
+ }
5661
+ function executable(candidate) {
5662
+ try {
5663
+ accessSync(candidate, constants.X_OK);
5664
+ return true;
5665
+ } catch {
5666
+ return false;
5667
+ }
5668
+ }
5669
+ var BINARY_FOR = {
5670
+ [DISCOVERED_AGENT_KIND.CLAUDE_CODE]: "claude",
5671
+ [DISCOVERED_AGENT_KIND.CODEX_CLI]: "codex",
5672
+ [DISCOVERED_AGENT_KIND.CLAUDE_DESKTOP]: "claude"
5673
+ };
5674
+ function binaryMeantBy(name) {
5675
+ const binary = BINARY_FOR[name];
5676
+ return binary === void 0 || !onPath(binary) ? null : binary;
5677
+ }
5678
+
5679
+ // src/commands/run.command.ts
5539
5680
  function environmentFor(base, home, sessionId, shellBinary) {
5540
5681
  const interceptors = interceptorDirFor7(home);
5541
5682
  const path = base["PATH"] ?? "";
5542
5683
  return {
5543
5684
  ...base,
5544
- PATH: path.startsWith(interceptors) ? path : `${interceptors}${delimiter3}${path}`,
5685
+ PATH: path.startsWith(interceptors) ? path : `${interceptors}${delimiter4}${path}`,
5545
5686
  SHELL: shellBinary,
5546
5687
  /* The shell we displace, so the wrapper has something to hand the command to and
5547
5688
  never reads `SHELL` back to find itself. */
@@ -5552,11 +5693,11 @@ function environmentFor(base, home, sessionId, shellBinary) {
5552
5693
  function newSessionId() {
5553
5694
  return `ses_${randomUUID().slice(0, 12)}`;
5554
5695
  }
5555
- var defaultStart = (command, args, env, transcript) => new Promise((resolve5) => {
5696
+ var defaultStart = (command, args, env, transcript) => new Promise((resolve6) => {
5556
5697
  if (transcript === void 0) {
5557
5698
  const child2 = spawn2(command, [...args], { stdio: "inherit", env });
5558
- child2.on("exit", (code) => resolve5(code ?? 1));
5559
- child2.on("error", () => resolve5(127));
5699
+ child2.on("exit", (code) => resolve6(code ?? 1));
5700
+ child2.on("error", () => resolve6(127));
5560
5701
  return;
5561
5702
  }
5562
5703
  mkdirSync(join18(transcript, ".."), { recursive: true, mode: 448 });
@@ -5571,11 +5712,11 @@ var defaultStart = (command, args, env, transcript) => new Promise((resolve5) =>
5571
5712
  child.stderr?.pipe(log);
5572
5713
  child.on("exit", (code) => {
5573
5714
  log.end();
5574
- resolve5(code ?? 1);
5715
+ resolve6(code ?? 1);
5575
5716
  });
5576
5717
  child.on("error", () => {
5577
5718
  log.end();
5578
- resolve5(127);
5719
+ resolve6(127);
5579
5720
  });
5580
5721
  });
5581
5722
  function registerRunCommand(program, context, deps = {}) {
@@ -5588,7 +5729,14 @@ function registerRunCommand(program, context, deps = {}) {
5588
5729
  if (binary === void 0) {
5589
5730
  throw new Error("Name the command to run: memnox run -- claude");
5590
5731
  }
5591
- const home = (deps.home ?? homedir32)();
5732
+ if (!(deps.onPath ?? onPath)(binary)) {
5733
+ const meant = (deps.binaryMeantBy ?? binaryMeantBy)(binary);
5734
+ throw new Error(
5735
+ `"${binary}" is not on PATH, so there is nothing to start.` + (meant === null ? "" : `
5736
+ The binary for ${binary} is "${meant}": memnox run -- ${meant}`)
5737
+ );
5738
+ }
5739
+ const home = (deps.home ?? homedir33)();
5592
5740
  const sessionId = (deps.newId ?? newSessionId)();
5593
5741
  const env = environmentFor(process.env, home, sessionId, options.shell);
5594
5742
  if (options.role !== void 0) env[AGENT_ROLE_VAR] = options.role;
@@ -5612,9 +5760,9 @@ function registerRunCommand(program, context, deps = {}) {
5612
5760
  const transcript = options.transcript === true ? transcriptPathFor(home, sessionId) : void 0;
5613
5761
  if (transcript !== void 0) context.out.note(`transcript ${transcript}`);
5614
5762
  const start = deps.start ?? defaultStart;
5615
- const [executable, ...args] = guarded;
5763
+ const [executable2, ...args] = guarded;
5616
5764
  try {
5617
- process.exitCode = await start(executable ?? binary, args, env, transcript);
5765
+ process.exitCode = await start(executable2 ?? binary, args, env, transcript);
5618
5766
  } finally {
5619
5767
  const let_go = await releaseLeases(home, sessionId, deps);
5620
5768
  if (let_go > 0) {
@@ -5630,7 +5778,7 @@ function sandboxed(command, home, wanted, seams = {}) {
5630
5778
  const kernel = seams.kernel ?? release2();
5631
5779
  if (guardFor2(platform, kernel).guard !== OS_GUARD2.SEATBELT) return command;
5632
5780
  const profile = guardProfilePath(home);
5633
- if (!(seams.exists ?? existsSync12)(profile)) return command;
5781
+ if (!(seams.exists ?? existsSync9)(profile)) return command;
5634
5782
  return sandboxCommand(profile, command);
5635
5783
  }
5636
5784
  async function declareTask(home, sessionId, options, deps) {
@@ -5681,6 +5829,7 @@ async function keepMilestone(sessionId, binary, deps) {
5681
5829
  sessionId,
5682
5830
  note: `before ${binary}`
5683
5831
  });
5832
+ await milestones.forget();
5684
5833
  return taken.id;
5685
5834
  } catch {
5686
5835
  return null;
@@ -5688,8 +5837,7 @@ async function keepMilestone(sessionId, binary, deps) {
5688
5837
  }
5689
5838
 
5690
5839
  // src/commands/mcp.command.ts
5691
- import { execFileSync } from "child_process";
5692
- import { homedir as homedir33 } from "os";
5840
+ import { homedir as homedir34 } from "os";
5693
5841
  import { dirname as dirname7, join as join19 } from "path";
5694
5842
  import { copyFile, mkdir as mkdir7, readFile as readFile11, writeFile as writeFile9 } from "fs/promises";
5695
5843
  import {
@@ -5759,18 +5907,11 @@ async function writeConfig(home, file, servers, changed) {
5759
5907
  await writeFile9(file.path, `${JSON.stringify(next, null, 2)}
5760
5908
  `, "utf8");
5761
5909
  }
5762
- function proxyOnPath(resolve5 = defaultResolve) {
5763
- return resolve5(PROXY_BINARY3);
5910
+ function proxyOnPath(resolve6 = defaultResolve) {
5911
+ return resolve6(PROXY_BINARY3);
5764
5912
  }
5765
- var defaultResolve = (binary) => {
5766
- try {
5767
- execFileSync("command", ["-v", binary], { stdio: "ignore", shell: true });
5768
- return true;
5769
- } catch {
5770
- return false;
5771
- }
5772
- };
5773
- function registerMcpCommand(program, context, home = homedir33, resolveBinary = defaultResolve, project = () => process.cwd()) {
5913
+ var defaultResolve = (binary) => onPath(binary);
5914
+ function registerMcpCommand(program, context, home = homedir34, resolveBinary = defaultResolve, project = () => process.cwd()) {
5774
5915
  const mcp = program.command("mcp").description("Route this machine\u2019s MCP servers through the Memnox proxy");
5775
5916
  mcp.command("wrap").description("Repoint every MCP server at the proxy, keeping a backup").option("--dry-run", "print what would change and write nothing").action(async (options) => {
5776
5917
  if (options.dryRun !== true && !proxyOnPath(resolveBinary)) {
@@ -5846,19 +5987,21 @@ Install the CLI first (npm install -g memnox), then run this again.`
5846
5987
  }
5847
5988
 
5848
5989
  // src/commands/policy.command.ts
5849
- import { existsSync as existsSync13 } from "fs";
5850
- import { homedir as homedir34 } from "os";
5851
- import { join as join20, resolve as resolve4 } from "path";
5990
+ import { existsSync as existsSync10 } from "fs";
5991
+ import { mkdir as mkdir8, readFile as readFile12, writeFile as writeFile10 } from "fs/promises";
5992
+ import { homedir as homedir35 } from "os";
5993
+ import { dirname as dirname8, join as join20, resolve as resolve5 } from "path";
5852
5994
  import {
5853
5995
  DECISION_EFFECT as DECISION_EFFECT9,
5854
- loadPoliciesFromFile as loadPoliciesFromFile6,
5996
+ loadPoliciesFromFile as loadPoliciesFromFile5,
5855
5997
  loadPolicySet as loadPolicySet4,
5856
5998
  LocalGate as LocalGate6,
5857
- MEMNOX_HOME as MEMNOX_HOME11,
5999
+ MEMNOX_HOME as MEMNOX_HOME10,
5858
6000
  readPolicyRegistry as readPolicyRegistry6,
6001
+ renameEffectsIn,
5859
6002
  resolveAction
5860
6003
  } from "@memnox/core";
5861
- var REGISTRY_FILE5 = "policies.json";
6004
+ var REGISTRY_FILE4 = "policies.json";
5862
6005
  function splitCommand(input) {
5863
6006
  return (input.match(/"[^"]*"|'[^']*'|\S+/g) ?? []).map(
5864
6007
  (word) => word.replace(/^["']|["']$/g, "")
@@ -5881,34 +6024,38 @@ function requestFor(input, options) {
5881
6024
  }
5882
6025
  function registerPolicyCommand(program, context) {
5883
6026
  const policy = program.command("policy").description("Inspect the rules in force");
5884
- policy.command("check [file]").description("Read every rule file on this machine and say what will not load").option("--prune", "forget registered files that are no longer on the disk").action(async (file, options) => {
5885
- await checkPolicyFiles(context, file, options.prune === true);
5886
- });
6027
+ policy.command("check [file]").description("Read every rule file on this machine and say what will not load").option("--prune", "forget registered files that are no longer on the disk").option("--fix", "rewrite effects this version renamed, keeping a backup").action(
6028
+ async (file, options) => {
6029
+ if (options.fix === true) await fixRenamedEffects(context, file);
6030
+ await checkPolicyFiles(context, file, options.prune === true);
6031
+ }
6032
+ );
5887
6033
  policy.command("use [file]").description('Register a rule file, so the seams load it and not only "policy test"').action(async (file) => {
5888
- const path = resolve4(resolvePolicyFile(file));
5889
- if (!existsSync13(path)) {
6034
+ const path = resolve5(resolvePolicyFile(file));
6035
+ if (!existsSync10(path)) {
5890
6036
  throw new Error(
5891
6037
  `No rule file at ${path}. Write one with "memnox protect --yes".`
5892
6038
  );
5893
6039
  }
5894
- const policies = await loadPoliciesFromFile6(path);
6040
+ const policies = await loadPoliciesFromFile5(path);
5895
6041
  const before = await readPolicyRegistry6(
5896
- join20(homedir34(), MEMNOX_HOME11, REGISTRY_FILE5)
6042
+ join20(homedir35(), MEMNOX_HOME10, REGISTRY_FILE4)
5897
6043
  );
5898
- await registerPolicyFile(homedir34(), path);
6044
+ await registerPolicyFile(homedir35(), path);
5899
6045
  context.out.line(
5900
6046
  before.includes(path) ? `${path} was already registered; ${policies.length} rule(s) load at every seam.` : `Registered ${path} \u2014 ${policies.length} rule(s) now load at every seam.`
5901
6047
  );
5902
6048
  context.out.note('Check it with "memnox doctor --wiring".');
5903
6049
  });
5904
6050
  policy.command("test <action>").description("Evaluate one action against the rules, changing nothing").option("-f, --file <path>", "policy file (default: whichever exists)").option("-a, --agent <name>", "agent the rules are matched against", "agent").option("-t, --target <target>", "what the action operates on").action(async (action, options) => {
5905
- const file = resolvePolicyFile(options.file);
5906
- if (!existsSync13(file)) {
6051
+ const rules2 = await policySetInForce(homedir35(), options.file);
6052
+ if (rules2.policies.length === 0 && rules2.unreadable.length === 0) {
5907
6053
  throw new Error(
5908
- `No rules at ${file}. Write some first: memnox protect --interactive`
6054
+ `No rules at ${resolvePolicyFile(options.file)}. Write some first: memnox protect --interactive`
5909
6055
  );
5910
6056
  }
5911
- const gate = await LocalGate6.fromFiles([file], {
6057
+ sayWhatDidNotLoad(context, rules2);
6058
+ const gate = new LocalGate6(rules2.policies, {
5912
6059
  agentName: options.agent
5913
6060
  });
5914
6061
  const verdict = gate.evaluate(requestFor(action, options));
@@ -5940,7 +6087,7 @@ async function checkPolicyFiles(context, file, prune) {
5940
6087
  out.line(`${style.dim("gone")} ${missing}`);
5941
6088
  }
5942
6089
  if (set.missing.length > 0 && prune && file === void 0) {
5943
- await forgetPolicyFiles(homedir34(), set.missing);
6090
+ await forgetPolicyFiles(homedir35(), set.missing);
5944
6091
  out.line(`${style.dim("forgot")} ${set.missing.length} path(s) that are gone`);
5945
6092
  }
5946
6093
  for (const broken of set.unreadable) {
@@ -5954,65 +6101,37 @@ async function checkPolicyFiles(context, file, prune) {
5954
6101
  }
5955
6102
  if (set.unreadable.length > 0) process.exitCode = 1;
5956
6103
  }
6104
+ async function fixRenamedEffects(context, file) {
6105
+ const { out, style } = context;
6106
+ const files = file !== void 0 ? [resolve5(file)] : await allPolicyFiles();
6107
+ let touched = 0;
6108
+ for (const path of files) {
6109
+ let source;
6110
+ try {
6111
+ source = await readFile12(path, "utf8");
6112
+ } catch {
6113
+ continue;
6114
+ }
6115
+ const { text, renamed } = renameEffectsIn(source);
6116
+ if (renamed.length === 0) continue;
6117
+ const backup = backupPathFor(homedir35(), path);
6118
+ await mkdir8(dirname8(backup), { recursive: true, mode: 448 });
6119
+ await writeFile10(backup, source, "utf8");
6120
+ await writeFile10(path, text, "utf8");
6121
+ touched += 1;
6122
+ out.line(`${style.ok("fixed")} ${path} ${renamed.length} effect(s)`);
6123
+ for (const each of new Set(renamed)) out.line(` ${each}`);
6124
+ out.note(` kept the original at ${backup}`);
6125
+ }
6126
+ if (touched > 0) out.line("");
6127
+ }
5957
6128
  async function allPolicyFiles() {
5958
- const registered = await readPolicyRegistry6(
5959
- join20(homedir34(), MEMNOX_HOME11, REGISTRY_FILE5)
5960
- );
5961
- const here = resolvePolicyFile();
5962
- const found = new Set(registered);
5963
- if (existsSync13(here)) found.add(here);
5964
- return [...found];
6129
+ return policyFilesInForce(homedir35());
5965
6130
  }
5966
6131
 
5967
6132
  // src/commands/login.command.ts
5968
6133
  import { spawn as spawn3 } from "child_process";
5969
- import { homedir as homedir35 } from "os";
5970
-
5971
- // src/banner.ts
5972
- var LETTERS = [
5973
- ["## ##", "### ###", "## # ##", "## ##", "## ##"],
5974
- ["######", "##", "#####", "##", "######"],
5975
- ["## ##", "### ###", "## # ##", "## ##", "## ##"],
5976
- ["## ##", "### ##", "## # ##", "## ###", "## ##"],
5977
- [" #####", "## ##", "## ##", "## ##", " #####"],
5978
- ["## ##", " ## ##", " ###", " ## ##", "## ##"]
5979
- ];
5980
- var ROWS = 5;
5981
- var TRACKING = 1;
5982
- var FACE = "\u2588";
5983
- var SHADOW = "\u2584";
5984
- var DRAWN = "#";
5985
- function faceGrid() {
5986
- const grid = Array.from({ length: ROWS }, () => []);
5987
- for (const letter of LETTERS) {
5988
- const width = Math.max(...letter.map((row3) => row3.length));
5989
- for (let row3 = 0; row3 < ROWS; row3 += 1) {
5990
- const art = letter[row3] ?? "";
5991
- for (let column = 0; column < width + TRACKING; column += 1) {
5992
- grid[row3].push(art[column] === DRAWN);
5993
- }
5994
- }
5995
- }
5996
- return grid;
5997
- }
5998
- var at = (grid, row3, column) => row3 >= 0 && column >= 0 && (grid[row3]?.[column] ?? false);
5999
- var shadowed = (grid, row3, column) => row3 === ROWS && at(grid, ROWS - 1, column - 1);
6000
- function wordmark(style, plain) {
6001
- if (!style.decorated) return [plain];
6002
- const grid = faceGrid();
6003
- const width = grid[0]?.length ?? 0;
6004
- const lines = [];
6005
- for (let row3 = 0; row3 <= ROWS; row3 += 1) {
6006
- let line2 = "";
6007
- for (let column = 0; column < width; column += 1) {
6008
- if (at(grid, row3, column)) line2 += style.bold(FACE);
6009
- else if (shadowed(grid, row3, column)) line2 += style.dim(SHADOW);
6010
- else line2 += " ";
6011
- }
6012
- lines.push(line2);
6013
- }
6014
- return lines;
6015
- }
6134
+ import { homedir as homedir36 } from "os";
6016
6135
 
6017
6136
  // src/flow.ts
6018
6137
  var RAIL = "\u2502";
@@ -6040,13 +6159,8 @@ var Flow = class {
6040
6159
  say(mark, text) {
6041
6160
  this.out.note(`${this.gutter(mark)}${text}`);
6042
6161
  }
6043
- /** The wordmark, then the command's own name on a filled block. */
6162
+ /** The command's own name on a filled block. The wordmark above it is the program's. */
6044
6163
  open(name) {
6045
- if (this.on) {
6046
- this.out.note("");
6047
- for (const line2 of wordmark(this.style, "memnox")) this.out.note(line2);
6048
- this.out.note("");
6049
- }
6050
6164
  this.say(START, this.style.chip(name));
6051
6165
  this.say(RAIL, "");
6052
6166
  }
@@ -6135,8 +6249,10 @@ async function requestCode(options, publicKey) {
6135
6249
  }
6136
6250
  });
6137
6251
  if (answer.status !== 200 && answer.status !== 201) {
6252
+ const missing = answer.status === 404 || answer.status === 501;
6138
6253
  throw new EnrolmentRefused(
6139
- `the control plane refused the request (${answer.status}).`
6254
+ missing ? `${options.baseUrl} has no device-enrolment endpoint (${answer.status}), so it is not a Memnox control plane.
6255
+ Point at the right one with "memnox login --url <base>". This machine is unchanged.` : `the control plane refused the request (${answer.status}).`
6140
6256
  );
6141
6257
  }
6142
6258
  const offer = answer.body;
@@ -6188,7 +6304,7 @@ function accountFrom(options, keys, collected, at2) {
6188
6304
 
6189
6305
  // src/commands/login.command.ts
6190
6306
  var DEFAULT_BASE_URL = "https://api.memnox.com";
6191
- function registerLoginCommand(program, context, home = homedir35, seams = {}) {
6307
+ function registerLoginCommand(program, context, home = homedir36, seams = {}) {
6192
6308
  program.command("login").description("Connect this machine to a workspace, so it gets your rules").option("--url <base>", "the control plane", DEFAULT_BASE_URL).option("--enforce", "start in enforce rather than observe").option("--no-open", "print the URL instead of opening a browser").action(async (options) => {
6193
6309
  const { out, style } = context;
6194
6310
  const enrolment = {
@@ -6297,7 +6413,7 @@ function openBrowser(url) {
6297
6413
  }
6298
6414
 
6299
6415
  // src/commands/spend.command.ts
6300
- import { homedir as homedir36 } from "os";
6416
+ import { homedir as homedir37 } from "os";
6301
6417
  import { randomUUID as randomUUID2 } from "crypto";
6302
6418
  import {
6303
6419
  DECISION_EFFECT as DECISION_EFFECT10,
@@ -6308,7 +6424,7 @@ import {
6308
6424
  validateEvent
6309
6425
  } from "@memnox/core";
6310
6426
  var DEFAULT_OPERATION = "llm.completion";
6311
- function registerSpendCommand(program, context, home = homedir36, now = () => /* @__PURE__ */ new Date(), newId = randomUUID2) {
6427
+ function registerSpendCommand(program, context, home = homedir37, now = () => /* @__PURE__ */ new Date(), newId = randomUUID2) {
6312
6428
  program.command("spend <usd>").description("Record what an agent spent, so budgets and the breaker can see it").option("-s, --session <id>", `session it belongs to (default: $${SESSION_VAR4})`).option("-a, --agent <name>", "agent that spent it", "agent").option(
6313
6429
  "-f, --for <action>",
6314
6430
  "what it was spent on, in the action grammar rules use",
@@ -6354,9 +6470,9 @@ function registerSpendCommand(program, context, home = homedir36, now = () => /*
6354
6470
  }
6355
6471
 
6356
6472
  // src/commands/sync.command.ts
6357
- import { homedir as homedir37 } from "os";
6473
+ import { homedir as homedir38 } from "os";
6358
6474
  import { readAccount as readAccount4 } from "@memnox/core";
6359
- function registerSyncCommand(program, context, home = homedir37) {
6475
+ function registerSyncCommand(program, context, home = homedir38) {
6360
6476
  const sync = program.command("sync").description("Pull the rules this workspace publishes, and send what happened");
6361
6477
  sync.command("now", { isDefault: true }).description("Do a pass now rather than waiting for the next heartbeat").option("--json", "machine-readable output").action(async (options) => {
6362
6478
  if (await readAccount4(home()) === null) {
@@ -6451,6 +6567,10 @@ function reportPush(context, result) {
6451
6567
  // src/program.ts
6452
6568
  function buildProgram(context) {
6453
6569
  const program = new Command().name("memnox").description("Memnox \u2014 the execution trust layer for AI agents").version(CLI_VERSION);
6570
+ program.hook("preAction", (_program, command) => {
6571
+ if (command.opts()["json"] === true) return;
6572
+ masthead(context.out, context.style);
6573
+ });
6454
6574
  registerScanCommand(program, context);
6455
6575
  registerDiffCommand(program, context);
6456
6576
  registerExplainCommand(program, context);