dsh-permission-rules 0.3.0 → 0.4.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.
package/lib/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { createRequire } from "node:module";
1
2
  import { existsSync, readFileSync } from "node:fs";
2
3
  import { dirname, isAbsolute, join, resolve } from "node:path";
3
4
  import chokidar from "chokidar";
@@ -32,7 +33,8 @@ const Config = z.object({
32
33
  audit: z.union(["all", "hits"]).default("all"),
33
34
  searchUp: z.boolean().default(false),
34
35
  maxGlobStars: z.number().default(2),
35
- enforce: z.boolean().default(true)
36
+ enforce: z.boolean().default(true),
37
+ allowUnmarkedAudit: z.boolean().default(false)
36
38
  });
37
39
  /**
38
40
  * Validate raw values and fill explicit defaults. A `maxRules`,
@@ -70,6 +72,7 @@ function resolveConfig(config = {}) {
70
72
  assertBoolean("searchUp", searchUp);
71
73
  assertBoolean("caseInsensitivePaths", config.caseInsensitivePaths ?? process.platform === "win32");
72
74
  assertBoolean("enforce", config.enforce ?? true);
75
+ assertBoolean("allowUnmarkedAudit", config.allowUnmarkedAudit ?? false);
73
76
  return {
74
77
  rulesFile,
75
78
  fallbackPath: config.fallbackPath,
@@ -84,7 +87,8 @@ function resolveConfig(config = {}) {
84
87
  audit: config.audit ?? "all",
85
88
  searchUp,
86
89
  maxGlobStars,
87
- enforce: config.enforce ?? true
90
+ enforce: config.enforce ?? true,
91
+ allowUnmarkedAudit: config.allowUnmarkedAudit ?? false
88
92
  };
89
93
  }
90
94
  /** Throw unless `key` is one of `allowed` (TypeScript's closed enums are not runtime checks). */
@@ -563,12 +567,16 @@ function isCatchAll(rule) {
563
567
  /**
564
568
  * One-line human summary of a compiled rule, for `/rules` output. The
565
569
  * 1-based number, action, match dimensions, and reason render first; a
566
- * disabled marker, tags, and a truncated description follow.
570
+ * disabled marker, tags, and a truncated description follow. When `source`
571
+ * is provided (multi-file chains), the rule's origin file is attributed on
572
+ * the line.
567
573
  * @param rule - the compiled rule.
568
574
  * @param tokens - dimension-prefix vocabulary for the configured language.
575
+ * @param source - display path of the rule's source file, when the listing
576
+ * spans several files.
569
577
  * @returns a single display line.
570
578
  */
571
- function describeRule(rule, tokens) {
579
+ function describeRule(rule, tokens, source) {
572
580
  const parts = [];
573
581
  if (rule.source.match.tools.length > 0) parts.push(`${tokens.tools}:${rule.source.match.tools.join(",")}`);
574
582
  if (rule.source.match.agents.length > 0) parts.push(`${tokens.agents}:${rule.source.match.agents.join(",")}`);
@@ -581,9 +589,10 @@ function describeRule(rule, tokens) {
581
589
  if (whenParts.length > 0 || platformPart.length > 0) parts.push(`${tokens.when}:${whenParts}${whenParts.length > 0 && platformPart.length > 0 ? " " : ""}${platformPart}`);
582
590
  const match = parts.length > 0 ? `[${parts.join(" ")}]` : `[${tokens.allTools}]`;
583
591
  const disabled = rule.enabled ? "" : ` (${tokens.disabled})`;
592
+ const sourcePart = source !== void 0 && source.length > 0 ? ` [${tokens.src}:${source}]` : "";
584
593
  const description = rule.description !== void 0 ? ` (${truncate(rule.description, DISPLAY_TRUNCATE)})` : "";
585
594
  const tags = rule.tags.length > 0 ? ` [${tokens.tags}:${rule.tags.join(",")}]` : "";
586
- return `${rule.index + 1}. ${rule.action}${disabled} ${match}: ${truncate(rule.reason, DISPLAY_TRUNCATE)}${description}${tags}`;
595
+ return `${rule.index + 1}. ${rule.action}${disabled} ${match}${sourcePart}: ${truncate(rule.reason, DISPLAY_TRUNCATE)}${description}${tags}`;
587
596
  }
588
597
  /** Cut a display string at `limit` characters, marking truncation with `…`. */
589
598
  function truncate(text, limit) {
@@ -926,9 +935,10 @@ const EN = {
926
935
  lastReloadWarning: (error) => `Warning: the last reload failed (${error}); the rules listed above are the previous ones.`,
927
936
  dryRunNotice: "Dry-run mode (enforce: false): deny/ask hits are audit-logged only — every call is passed through.",
928
937
  unknownArg: (arg) => `Unknown /rules argument "${arg}". ${EN.usage}`,
929
- usage: "Usage: /rules [reload | decisions [n] | test <tool> <json-args>]",
938
+ usage: "Usage: /rules [list | reload | decisions [n] | test <tool> <json-args>]",
930
939
  decisionsHeader: (shown, total) => `Last ${shown} of ${total} permission decision(s) for this session:`,
931
940
  noDecisions: "No permission decisions recorded in this session yet.",
941
+ auditDisabledNotice: "Session-log audit is disabled on this host: its Session.append predates the ignorable marker and would make sessions unresumable elsewhere (set allowUnmarkedAudit: true to opt back in).",
932
942
  invalidDecisionsCount: (arg) => `Invalid decisions count "${arg}": give a positive integer (default 10).`,
933
943
  decisionLine: (seq, action, toolName, ruleIndex, reason, dryRun, outcome) => {
934
944
  return `- #${seq} ${action} ${toolName}${ruleIndex === void 0 ? "" : ` (rule ${ruleIndex + 1})`}${dryRun ? outcome === void 0 ? " (dry-run)" : ` (dry-run → ${outcome})` : ""}${reason === void 0 ? "" : `: ${short(reason)}`}`;
@@ -938,7 +948,8 @@ const EN = {
938
948
  testBadJson: (text) => `Invalid JSON arguments "${text}": the arguments must parse as JSON.`,
939
949
  testUnknownFlag: (flag) => `Unknown /rules test flag "${flag}". ${EN.testUsage}`,
940
950
  testBadFlag: (flag) => `Invalid /rules test flag "${flag}": expected a value. ${EN.testUsage}`,
941
- testUsage: "Usage: /rules test [--cwd <dir>] [--env KEY=VALUE]... [--agent <selector>]... <tool> <json-args>, e.g. /rules test bash {\"command\":\"git push origin main\"}",
951
+ testBadPlatform: (value) => `Unknown platform "${value}" in --platform: expected one of aix, android, darwin, freebsd, linux, openbsd, sunos, win32. ${EN.testUsage}`,
952
+ testUsage: "Usage: /rules test [--cwd <dir>] [--env KEY=VALUE]... [--agent <selector>]... [--platform <name>] <tool> <json-args>, e.g. /rules test bash {\"command\":\"git push origin main\"}",
942
953
  unreachableWarning: (numbers) => `Warning: rule${numbers.length > 1 ? "s" : ""} ${numbers.join(", ")} ${numbers.length > 1 ? "are" : "is"} unreachable (shadowed by an earlier catch-all rule).`,
943
954
  emptySource: "(no rule file — empty rule set)"
944
955
  };
@@ -951,9 +962,10 @@ const ZH = {
951
962
  lastReloadWarning: (error) => `警告:上次重载失败(${error});以上列出的是之前的规则。`,
952
963
  dryRunNotice: "干跑模式(enforce: false):deny/ask 命中仅写入审计日志——所有调用照常透传。",
953
964
  unknownArg: (arg) => `未知的 /rules 参数 "${arg}"。${ZH.usage}`,
954
- usage: "用法:/rules [reload | decisions [n] | test <工具名> <json-参数>]",
965
+ usage: "用法:/rules [list | reload | decisions [n] | test <工具名> <json-参数>]",
955
966
  decisionsHeader: (shown, total) => `本会话最近 ${shown}/${total} 条权限裁决:`,
956
967
  noDecisions: "本会话还没有记录任何权限裁决。",
968
+ auditDisabledNotice: "本宿主上会话日志审计已停用:其 Session.append 早于 ignorable 标记支持,会令会话在其他宿主上无法恢复(设 allowUnmarkedAudit: true 可重新开启)。",
957
969
  invalidDecisionsCount: (arg) => `无效的裁决条数 "${arg}":请给正整数(默认 10)。`,
958
970
  decisionLine: (seq, action, toolName, ruleIndex, reason, dryRun, outcome) => {
959
971
  return `- #${seq} ${action} ${toolName}${ruleIndex === void 0 ? "" : `(规则 ${ruleIndex + 1})`}${dryRun ? outcome === void 0 ? "(干跑)" : `(干跑 → ${outcome})` : ""}${reason === void 0 ? "" : `:${short(reason)}`}`;
@@ -963,7 +975,8 @@ const ZH = {
963
975
  testBadJson: (text) => `无效的 JSON 参数 "${text}":参数必须能被解析为 JSON。`,
964
976
  testUnknownFlag: (flag) => `未知的 /rules test 标志 "${flag}"。${ZH.testUsage}`,
965
977
  testBadFlag: (flag) => `无效的 /rules test 标志 "${flag}":缺少取值。${ZH.testUsage}`,
966
- testUsage: "用法:/rules test [--cwd <目录>] [--env 键=值]... [--agent <选择器>]... <工具名> <json-参数>,例如 /rules test bash {\"command\":\"git push origin main\"}",
978
+ testBadPlatform: (value) => `未知的平台 "${value}"(--platform):应为 aix、android、darwin、freebsd、linux、openbsd、sunos、win32 之一。${ZH.testUsage}`,
979
+ testUsage: "用法:/rules test [--cwd <目录>] [--env 键=值]... [--agent <选择器>]... [--platform <平台名>] <工具名> <json-参数>,例如 /rules test bash {\"command\":\"git push origin main\"}",
967
980
  unreachableWarning: (numbers) => `警告:规则 ${numbers.join("、")} 不可达(被前面的通配规则遮蔽)。`,
968
981
  emptySource: "(无规则文件——空规则集)"
969
982
  };
@@ -976,9 +989,10 @@ const ES = {
976
989
  lastReloadWarning: (error) => `Aviso: la última recarga falló (${error}); las reglas mostradas son las anteriores.`,
977
990
  dryRunNotice: "Modo simulación (enforce: false): los aciertos deny/ask solo se registran en auditoría — todas las llamadas pasan.",
978
991
  unknownArg: (arg) => `Argumento de /rules desconocido "${arg}". ${ES.usage}`,
979
- usage: "Uso: /rules [reload | decisions [n] | test <herramienta> <json-args>]",
992
+ usage: "Uso: /rules [list | reload | decisions [n] | test <herramienta> <json-args>]",
980
993
  decisionsHeader: (shown, total) => `Últimas ${shown} de ${total} decisión(es) de permiso de esta sesión:`,
981
994
  noDecisions: "Esta sesión aún no registra decisiones de permiso.",
995
+ auditDisabledNotice: "La auditoría del registro de sesión está desactivada en este host: su Session.append es anterior al marcador ignorable y haría las sesiones irrecuperables en otros hosts (establece allowUnmarkedAudit: true para reactivarla).",
982
996
  invalidDecisionsCount: (arg) => `Cantidad de decisiones inválida "${arg}": indique un entero positivo (por defecto 10).`,
983
997
  decisionLine: (seq, action, toolName, ruleIndex, reason, dryRun, outcome) => {
984
998
  return `- #${seq} ${action} ${toolName}${ruleIndex === void 0 ? "" : ` (regla ${ruleIndex + 1})`}${dryRun ? outcome === void 0 ? " (simulación)" : ` (simulación → ${outcome})` : ""}${reason === void 0 ? "" : `: ${short(reason)}`}`;
@@ -988,7 +1002,8 @@ const ES = {
988
1002
  testBadJson: (text) => `Argumentos JSON inválidos "${text}": los argumentos deben poderse analizar como JSON.`,
989
1003
  testUnknownFlag: (flag) => `Bandera de /rules test desconocida "${flag}". ${ES.testUsage}`,
990
1004
  testBadFlag: (flag) => `Bandera de /rules test inválida "${flag}": falta un valor. ${ES.testUsage}`,
991
- testUsage: "Uso: /rules test [--cwd <dir>] [--env CLAVE=VALOR]... [--agent <selector>]... <herramienta> <json-args>, p. ej. /rules test bash {\"command\":\"git push origin main\"}",
1005
+ testBadPlatform: (value) => `Plataforma desconocida "${value}" en --platform: se esperaba una de aix, android, darwin, freebsd, linux, openbsd, sunos, win32. ${ES.testUsage}`,
1006
+ testUsage: "Uso: /rules test [--cwd <dir>] [--env CLAVE=VALOR]... [--agent <selector>]... [--platform <nombre>] <herramienta> <json-args>, p. ej. /rules test bash {\"command\":\"git push origin main\"}",
992
1007
  unreachableWarning: (numbers) => `Aviso: ${numbers.length > 1 ? "las reglas" : "la regla"} ${numbers.join(", ")} ${numbers.length > 1 ? "son inalcanzables" : "es inalcanzable"} (tapada por una regla general anterior).`,
993
1008
  emptySource: "(sin archivo de reglas — conjunto vacío)"
994
1009
  };
@@ -1001,9 +1016,10 @@ const PT = {
1001
1016
  lastReloadWarning: (error) => `Aviso: a última recarga falhou (${error}); as regras listadas são as anteriores.`,
1002
1017
  dryRunNotice: "Modo simulação (enforce: false): acertos deny/ask são apenas registrados na auditoria — todas as chamadas passam.",
1003
1018
  unknownArg: (arg) => `Argumento de /rules desconhecido "${arg}". ${PT.usage}`,
1004
- usage: "Uso: /rules [reload | decisions [n] | test <ferramenta> <json-args>]",
1019
+ usage: "Uso: /rules [list | reload | decisions [n] | test <ferramenta> <json-args>]",
1005
1020
  decisionsHeader: (shown, total) => `Últimas ${shown} de ${total} decisão(ões) de permissão desta sessão:`,
1006
1021
  noDecisions: "Esta sessão ainda não registrou decisões de permissão.",
1022
+ auditDisabledNotice: "A auditoria do registro da sessão está desativada neste host: seu Session.append é anterior ao marcador ignorable e tornaria as sessões irrecuperáveis em outros hosts (defina allowUnmarkedAudit: true para reativar).",
1007
1023
  invalidDecisionsCount: (arg) => `Quantidade de decisões inválida "${arg}": informe um inteiro positivo (padrão 10).`,
1008
1024
  decisionLine: (seq, action, toolName, ruleIndex, reason, dryRun, outcome) => {
1009
1025
  return `- #${seq} ${action} ${toolName}${ruleIndex === void 0 ? "" : ` (regra ${ruleIndex + 1})`}${dryRun ? outcome === void 0 ? " (simulação)" : ` (simulação → ${outcome})` : ""}${reason === void 0 ? "" : `: ${short(reason)}`}`;
@@ -1013,7 +1029,8 @@ const PT = {
1013
1029
  testBadJson: (text) => `Argumentos JSON inválidos "${text}": os argumentos precisam ser JSON analisável.`,
1014
1030
  testUnknownFlag: (flag) => `Sinalizador de /rules test desconhecido "${flag}". ${PT.testUsage}`,
1015
1031
  testBadFlag: (flag) => `Sinalizador de /rules test inválido "${flag}": falta um valor. ${PT.testUsage}`,
1016
- testUsage: "Uso: /rules test [--cwd <dir>] [--env CHAVE=VALOR]... [--agent <seletor>]... <ferramenta> <json-args>, ex.: /rules test bash {\"command\":\"git push origin main\"}",
1032
+ testBadPlatform: (value) => `Plataforma desconhecida "${value}" em --platform: esperava-se uma de aix, android, darwin, freebsd, linux, openbsd, sunos, win32. ${PT.testUsage}`,
1033
+ testUsage: "Uso: /rules test [--cwd <dir>] [--env CHAVE=VALOR]... [--agent <seletor>]... [--platform <nome>] <ferramenta> <json-args>, ex.: /rules test bash {\"command\":\"git push origin main\"}",
1017
1034
  unreachableWarning: (numbers) => `Aviso: ${numbers.length > 1 ? "as regras" : "a regra"} ${numbers.join(", ")} ${numbers.length > 1 ? "são inalcançáveis" : "é inalcançável"} (encoberta por uma regra geral anterior).`,
1018
1035
  emptySource: "(sem arquivo de regras — conjunto vazio)"
1019
1036
  };
@@ -1026,9 +1043,10 @@ const HI = {
1026
1043
  lastReloadWarning: (error) => `चेतावनी: अंतिम पुनः लोड विफल रहा (${error}); ऊपर सूचीबद्ध नियम पिछले वाले हैं।`,
1027
1044
  dryRunNotice: "ड्राई-रन मोड (enforce: false): deny/ask हिट केवल ऑडिट में दर्ज होते हैं — हर कॉल पास हो जाती है।",
1028
1045
  unknownArg: (arg) => `अज्ञात /rules तर्क "${arg}"। ${HI.usage}`,
1029
- usage: "उपयोग: /rules [reload | decisions [n] | test <टूल> <json-args>]",
1046
+ usage: "उपयोग: /rules [list | reload | decisions [n] | test <टूल> <json-args>]",
1030
1047
  decisionsHeader: (shown, total) => `इस सत्र के अंतिम ${shown}/${total} अनुमति निर्णय:`,
1031
1048
  noDecisions: "इस सत्र में अभी कोई अनुमति निर्णय दर्ज नहीं है।",
1049
+ auditDisabledNotice: "इस होस्ट पर सत्र-लॉग ऑडिट अक्षम है: इसका Session.append ignorable मार्कर से पुराना है और सत्र अन्य होस्ट पर अप्राप्य बन जाते (पुनः चालू करने के लिए allowUnmarkedAudit: true सेट करें)।",
1032
1050
  invalidDecisionsCount: (arg) => `अमान्य निर्णय-संख्या "${arg}": एक धनात्मक पूर्णांक दें (डिफ़ॉल्ट 10)।`,
1033
1051
  decisionLine: (seq, action, toolName, ruleIndex, reason, dryRun, outcome) => {
1034
1052
  return `- #${seq} ${action} ${toolName}${ruleIndex === void 0 ? "" : ` (नियम ${ruleIndex + 1})`}${dryRun ? outcome === void 0 ? " (ड्राई-रन)" : ` (ड्राई-रन → ${outcome})` : ""}${reason === void 0 ? "" : `: ${short(reason)}`}`;
@@ -1038,7 +1056,8 @@ const HI = {
1038
1056
  testBadJson: (text) => `अमान्य JSON तर्क "${text}": तर्क JSON के रूप में पार्स होने चाहिए।`,
1039
1057
  testUnknownFlag: (flag) => `अज्ञात /rules test फ़्लैग "${flag}"। ${HI.testUsage}`,
1040
1058
  testBadFlag: (flag) => `अमान्य /rules test फ़्लैग "${flag}": मान गायब है। ${HI.testUsage}`,
1041
- testUsage: "उपयोग: /rules test [--cwd <dir>] [--env KEY=मान]... [--agent <चयनकर्ता>]... <टूल> <json-args>, जैसे /rules test bash {\"command\":\"git push origin main\"}",
1059
+ testBadPlatform: (value) => `अज्ञात प्लेटफ़ॉर्म "${value}" (--platform): aix, android, darwin, freebsd, linux, openbsd, sunos, win32 में से एक अपेक्षित। ${HI.testUsage}`,
1060
+ testUsage: "उपयोग: /rules test [--cwd <dir>] [--env KEY=मान]... [--agent <चयनकर्ता>]... [--platform <नाम>] <टूल> <json-args>, जैसे /rules test bash {\"command\":\"git push origin main\"}",
1042
1061
  unreachableWarning: (numbers) => `चेतावनी: नियम ${numbers.join(", ")} अप्राप्य हैं (पहले वाले सर्व-मिलान नियम से ढके हुए)।`,
1043
1062
  emptySource: "(कोई नियम फ़ाइल नहीं — खाली नियम-समूह)"
1044
1063
  };
@@ -1062,7 +1081,8 @@ const DESCRIBE_TOKENS = {
1062
1081
  when: "when",
1063
1082
  platform: "platform",
1064
1083
  disabled: "disabled",
1065
- tags: "tags"
1084
+ tags: "tags",
1085
+ src: "src"
1066
1086
  },
1067
1087
  zh: {
1068
1088
  allTools: "全部工具",
@@ -1074,7 +1094,8 @@ const DESCRIBE_TOKENS = {
1074
1094
  when: "条件",
1075
1095
  platform: "平台",
1076
1096
  disabled: "已禁用",
1077
- tags: "标签"
1097
+ tags: "标签",
1098
+ src: "来源"
1078
1099
  },
1079
1100
  es: {
1080
1101
  allTools: "todas las herramientas",
@@ -1086,7 +1107,8 @@ const DESCRIBE_TOKENS = {
1086
1107
  when: "cuándo",
1087
1108
  platform: "plataforma",
1088
1109
  disabled: "deshabilitada",
1089
- tags: "etiquetas"
1110
+ tags: "etiquetas",
1111
+ src: "origen"
1090
1112
  },
1091
1113
  pt: {
1092
1114
  allTools: "todas as ferramentas",
@@ -1098,7 +1120,8 @@ const DESCRIBE_TOKENS = {
1098
1120
  when: "quando",
1099
1121
  platform: "plataforma",
1100
1122
  disabled: "desativada",
1101
- tags: "etiquetas"
1123
+ tags: "etiquetas",
1124
+ src: "origem"
1102
1125
  },
1103
1126
  hi: {
1104
1127
  allTools: "सभी टूल",
@@ -1110,10 +1133,25 @@ const DESCRIBE_TOKENS = {
1110
1133
  when: "शर्त",
1111
1134
  platform: "प्लेटफ़ॉर्म",
1112
1135
  disabled: "अक्षम",
1113
- tags: "टैग"
1136
+ tags: "टैग",
1137
+ src: "स्रोत"
1114
1138
  }
1115
1139
  };
1116
1140
  //#endregion
1141
+ //#region src/events.ts
1142
+ /**
1143
+ * Whether an `append` call actually honored the `ignorable` marker: the
1144
+ * logged event returned by the host carries `ignorable === true` on
1145
+ * marker-aware builds and nothing on pre-marker builds. `false` (or any
1146
+ * non-event return) means the host dropped the marker and the event landed
1147
+ * unmarked — the runtime then degrades instead of polluting further logs.
1148
+ * @param result - the return value of the audit append.
1149
+ * @returns true only when the marker is present on the returned envelope.
1150
+ */
1151
+ function isMarkedAuditEvent(result) {
1152
+ return typeof result === "object" && result !== null && result.ignorable === true;
1153
+ }
1154
+ //#endregion
1117
1155
  //#region src/runtime.ts
1118
1156
  /**
1119
1157
  * Runtime of `dsh-permission-rules`: per-workspace rule loading (project
@@ -1122,7 +1160,9 @@ const DESCRIBE_TOKENS = {
1122
1160
  * deny/ask decision (and NEVER short-circuits on allow or passthrough),
1123
1161
  * the `permissionRules/decision` audit event, the `/rules` session command
1124
1162
  * (`list | reload | decisions [n] | test [flags] <tool> <json>`), and
1125
- * Chokidar-driven reloads. Every registration is an effect.
1163
+ * Chokidar-driven reloads (effective rule files plus candidate watches on
1164
+ * expected-but-absent files, so mid-session creation is adopted). Every
1165
+ * registration is an effect.
1126
1166
  * @module dsh-permission-rules/runtime
1127
1167
  */
1128
1168
  /**
@@ -1134,10 +1174,12 @@ var PermissionRulesRuntime = class {
1134
1174
  config;
1135
1175
  /** Loaded (or failed) rules per workspace cwd, least-recently-used first for eviction. */
1136
1176
  byCwd = /* @__PURE__ */ new Map();
1137
- /** Live watchers per rule-file path, with the cwds each serves. */
1177
+ /** Live watchers per watched path (rule file or candidate ancestor directory), with the cwds each serves. */
1138
1178
  watchers = /* @__PURE__ */ new Map();
1139
1179
  /** Debounce timers per rule-file path. */
1140
1180
  timers = /* @__PURE__ */ new Map();
1181
+ /** Whether the host honors the audit envelope's `ignorable` marker: unknown until the first decision (or peer-version check). */
1182
+ auditSupport = "unknown";
1141
1183
  constructor(ctx, config) {
1142
1184
  this.ctx = ctx;
1143
1185
  this.config = config;
@@ -1232,6 +1274,17 @@ var PermissionRulesRuntime = class {
1232
1274
  }
1233
1275
  }
1234
1276
  /**
1277
+ * Canonical per-workspace cache key: the resolved root, case-folded on
1278
+ * Windows so differently-spelled paths to the same workspace share one
1279
+ * cache entry and one watcher set instead of doubling both.
1280
+ * @param cwd - the session's absolute workspace root.
1281
+ * @returns the canonical key.
1282
+ */
1283
+ cacheKey(cwd) {
1284
+ const resolved = resolve(cwd);
1285
+ return process.platform === "win32" ? resolved.toLowerCase() : resolved;
1286
+ }
1287
+ /**
1235
1288
  * The rules in effect for one cwd, loading on first use. Under
1236
1289
  * `badFilePolicy: 'fail'` a bad initial load throws on EVERY use (the
1237
1290
  * pending tool call errors loudly) while the watcher keeps observing the
@@ -1242,23 +1295,24 @@ var PermissionRulesRuntime = class {
1242
1295
  * @returns the loaded rules.
1243
1296
  */
1244
1297
  rulesFor(cwd) {
1245
- const existing = this.byCwd.get(cwd);
1298
+ const key = this.cacheKey(cwd);
1299
+ const existing = this.byCwd.get(key);
1246
1300
  if (existing !== void 0) {
1247
1301
  if (existing.failed === true) throw new RuleError(existing.lastError ?? `rule load failed for ${cwd}`);
1248
- this.byCwd.delete(cwd);
1249
- this.byCwd.set(cwd, existing);
1302
+ this.byCwd.delete(key);
1303
+ this.byCwd.set(key, existing);
1250
1304
  return existing;
1251
1305
  }
1252
- this.evictIfFull(cwd);
1306
+ this.evictIfFull(key);
1253
1307
  try {
1254
1308
  const loaded = this.load(cwd);
1255
- this.byCwd.set(cwd, loaded);
1256
- this.reconcileWatch(cwd, loaded.sources);
1309
+ this.byCwd.set(key, loaded);
1310
+ this.reconcileWatch(key, loaded.sources);
1257
1311
  return loaded;
1258
1312
  } catch (error) {
1259
1313
  const message = error instanceof Error ? error.message : String(error);
1260
1314
  const sources = this.resolveSources(cwd);
1261
- this.byCwd.set(cwd, {
1315
+ this.byCwd.set(key, {
1262
1316
  cwd,
1263
1317
  sources,
1264
1318
  compiled: {
@@ -1268,7 +1322,7 @@ var PermissionRulesRuntime = class {
1268
1322
  lastError: message,
1269
1323
  failed: true
1270
1324
  });
1271
- this.reconcileWatch(cwd, sources);
1325
+ this.reconcileWatch(key, sources);
1272
1326
  throw error;
1273
1327
  }
1274
1328
  }
@@ -1280,29 +1334,34 @@ var PermissionRulesRuntime = class {
1280
1334
  * @param cwd - the workspace root.
1281
1335
  */
1282
1336
  reload(cwd) {
1283
- const previous = this.byCwd.get(cwd);
1337
+ const key = this.cacheKey(cwd);
1338
+ const previous = this.byCwd.get(key);
1284
1339
  try {
1285
1340
  const loaded = this.load(cwd);
1286
- this.byCwd.set(cwd, loaded);
1287
- this.reconcileWatch(cwd, loaded.sources);
1341
+ this.byCwd.set(key, loaded);
1342
+ this.reconcileWatch(key, loaded.sources);
1288
1343
  this.ctx.logger.info(`permission-rules: reloaded ${loaded.compiled.rules.length} rule(s) from ${loaded.sources.join(", ") || "(empty rule set)"} for ${cwd}`);
1289
1344
  } catch (error) {
1290
1345
  const message = error instanceof Error ? error.message : String(error);
1291
1346
  this.ctx.logger.warn(`permission-rules: reload failed for ${cwd}: ${message} (keeping previous rules)`);
1292
- if (previous !== void 0) this.byCwd.set(cwd, {
1347
+ if (previous !== void 0) this.byCwd.set(key, {
1293
1348
  ...previous,
1294
1349
  lastError: message
1295
1350
  });
1296
- else this.byCwd.set(cwd, {
1297
- cwd,
1298
- sources: this.resolveSources(cwd),
1299
- compiled: {
1300
- rules: [],
1301
- caseInsensitivePaths: this.config.caseInsensitivePaths
1302
- },
1303
- lastError: message,
1304
- failed: true
1305
- });
1351
+ else {
1352
+ const sources = this.resolveSources(cwd);
1353
+ this.byCwd.set(key, {
1354
+ cwd,
1355
+ sources,
1356
+ compiled: {
1357
+ rules: [],
1358
+ caseInsensitivePaths: this.config.caseInsensitivePaths
1359
+ },
1360
+ lastError: message,
1361
+ failed: true
1362
+ });
1363
+ this.reconcileWatch(key, sources);
1364
+ }
1306
1365
  }
1307
1366
  }
1308
1367
  /**
@@ -1361,13 +1420,17 @@ var PermissionRulesRuntime = class {
1361
1420
  * Append the log-only `permissionRules/decision` audit event for every
1362
1421
  * decision (passthrough included unless `audit: 'hits'`), requesting the
1363
1422
  * envelope's `ignorable: true` marker so any harness build can load the
1364
- * log (readers that do not know the type skip the audit record instead of
1365
- * refusing the session). `source` names the matched rule's file, or the
1366
- * nearest effective file on a passthrough; `cwd` names the workspace the
1367
- * rules were resolved for; `dryRun` marks would-be deny/ask hits under
1368
- * `enforce: false`. Agentless calls have no session to audit; append
1369
- * failures are contained so an audit hiccup can never change a
1370
- * permission decision.
1423
+ * log. Hosts whose `Session.append` predates the marker (the rc.6 line)
1424
+ * silently drop it, leaving sessions unresumable on stricter hosts — the
1425
+ * runtime therefore detects such hosts BEFORE the first append (peer
1426
+ * version) and re-checks after the first append (returned envelope), then
1427
+ * degrades: session-log audit is disabled with a one-time warning unless
1428
+ * `allowUnmarkedAudit: true` opts back in. `source` names the matched
1429
+ * rule's file, or the nearest effective file on a passthrough; `cwd`
1430
+ * names the workspace the rules were resolved for; `dryRun` marks
1431
+ * would-be deny/ask hits under `enforce: false`. Agentless calls have no
1432
+ * session to audit; append failures are contained so an audit hiccup can
1433
+ * never change a permission decision.
1371
1434
  * @param exec - the pending call.
1372
1435
  * @param loaded - the rules in effect.
1373
1436
  * @param hit - the first matching rule, or undefined for passthrough.
@@ -1378,8 +1441,17 @@ var PermissionRulesRuntime = class {
1378
1441
  if (this.config.audit === "hits" && hit === void 0) return;
1379
1442
  const agent = exec.agent;
1380
1443
  if (agent === void 0) return;
1444
+ if (this.auditSupport === "unsupported") return;
1445
+ if (this.auditSupport === "unknown" && !this.config.allowUnmarkedAudit) {
1446
+ const version = this.peerVersion();
1447
+ if (version !== null && isUnmarkedHostVersion(version)) {
1448
+ this.auditSupport = "unsupported";
1449
+ this.warnUnmarkedAuditHost();
1450
+ return;
1451
+ }
1452
+ }
1381
1453
  try {
1382
- agent.session.append("permissionRules/decision", {
1454
+ const result = this.appendAudit(agent, {
1383
1455
  toolName: exec.name,
1384
1456
  callId: exec.callId,
1385
1457
  source: hit === void 0 ? loaded.sources[0] ?? "" : loaded.sources[hit.rule.sourceIndex] ?? "",
@@ -1391,11 +1463,35 @@ var PermissionRulesRuntime = class {
1391
1463
  reason: hit.rule.reason
1392
1464
  } : {},
1393
1465
  ...dryRun ? { dryRun: true } : {}
1394
- }, { ignorable: true });
1466
+ });
1467
+ if (this.auditSupport === "unknown" && !this.config.allowUnmarkedAudit) {
1468
+ if (isMarkedAuditEvent(result)) this.auditSupport = "supported";
1469
+ else {
1470
+ this.auditSupport = "unsupported";
1471
+ this.warnUnmarkedAuditHost();
1472
+ }
1473
+ }
1395
1474
  } catch (error) {
1396
1475
  this.ctx.logger.warn(`permission-rules: audit append failed: ${String(error)}`);
1397
1476
  }
1398
1477
  }
1478
+ /** Append one audit event through the session surface; the probe seam for host-capability detection. */
1479
+ appendAudit(agent, data) {
1480
+ return agent.session.append("permissionRules/decision", data, { ignorable: true });
1481
+ }
1482
+ /** The installed `@deepseek-ai/dsh-session` version, or `null` when unresolvable (falls back to the append probe). */
1483
+ peerVersion() {
1484
+ try {
1485
+ const pkg = createRequire(import.meta.url)("@deepseek-ai/dsh-session/package.json");
1486
+ return typeof pkg.version === "string" ? pkg.version : null;
1487
+ } catch {
1488
+ return null;
1489
+ }
1490
+ }
1491
+ /** One-time warning that session-log audit was disabled to keep session logs loadable. */
1492
+ warnUnmarkedAuditHost() {
1493
+ this.ctx.logger.warn("permission-rules: this host drops the ignorable marker on audit events (Session.append predates it), which would make sessions unresumable on stricter harness builds — session-log audit is disabled; set allowUnmarkedAudit: true to opt back in, and repair existing logs with scripts/repair-session-logs.mjs (see https://github.com/PerryLink/dsh-permission-rules/issues/2)");
1494
+ }
1399
1495
  /**
1400
1496
  * Execute the `/rules` command: bare `/rules` lists the active rules and
1401
1497
  * their sources; `/rules reload` re-reads the chain; `/rules decisions
@@ -1418,7 +1514,7 @@ var PermissionRulesRuntime = class {
1418
1514
  text: prose.unknownArg(invocation.rawInput.trim())
1419
1515
  };
1420
1516
  this.reload(cwd);
1421
- const reloaded = this.byCwd.get(cwd);
1517
+ const reloaded = this.byCwd.get(this.cacheKey(cwd));
1422
1518
  if (reloaded?.lastError !== void 0) return {
1423
1519
  kind: "error",
1424
1520
  text: prose.reloadFailed(reloaded.lastError)
@@ -1430,6 +1526,10 @@ var PermissionRulesRuntime = class {
1430
1526
  text: prose.reloaded(rules.length, source)
1431
1527
  };
1432
1528
  }
1529
+ if (verb === "list" && rest.length > 0) return {
1530
+ kind: "error",
1531
+ text: prose.unknownArg(invocation.rawInput.trim())
1532
+ };
1433
1533
  if (verb === "decisions") {
1434
1534
  if (rest.length > 1) return {
1435
1535
  kind: "error",
@@ -1447,7 +1547,7 @@ var PermissionRulesRuntime = class {
1447
1547
  return this.decisionsCommand(invocation, count, prose);
1448
1548
  }
1449
1549
  if (verb === "test") return this.testCommand(raw, verbRaw ?? "", invocation, cwd, prose);
1450
- if (verb !== "") return {
1550
+ if (verb !== "" && verb !== "list") return {
1451
1551
  kind: "error",
1452
1552
  text: prose.unknownArg(invocation.rawInput.trim())
1453
1553
  };
@@ -1464,7 +1564,8 @@ var PermissionRulesRuntime = class {
1464
1564
  if (loaded.sources.length === 0) lines.push(prose.noRules(cwd, this.config.fallbackPath !== void 0 ? prose.fallbackMissing : ""));
1465
1565
  else {
1466
1566
  lines.push(prose.rulesHeader(loaded.compiled.rules.length, loaded.sources, cwd));
1467
- lines.push(...loaded.compiled.rules.map((rule) => describeRule(rule, DESCRIBE_TOKENS[this.config.language])));
1567
+ const multiSource = loaded.sources.length > 1;
1568
+ lines.push(...loaded.compiled.rules.map((rule) => describeRule(rule, DESCRIBE_TOKENS[this.config.language], multiSource ? this.displaySource(loaded.sources[rule.sourceIndex] ?? "", cwd) : void 0)));
1468
1569
  const unreachable = findUnreachableRules(loaded.compiled);
1469
1570
  if (unreachable.length > 0) lines.push(prose.unreachableWarning(unreachable.map((index) => index + 1)));
1470
1571
  }
@@ -1480,9 +1581,11 @@ var PermissionRulesRuntime = class {
1480
1581
  * Execute `/rules test` with optional leading flags: `--cwd <dir>`
1481
1582
  * evaluates against that workspace (rule discovery AND path
1482
1583
  * normalization), `--env KEY=VALUE` (repeatable) overrides host env for
1483
- * `when.env` matching, and `--agent <selector>` (repeatable) supplies
1484
- * agent-identity candidates for the `agents` dimension. The JSON
1485
- * argument tail is kept verbatim, so quoted JSON survives unchanged.
1584
+ * `when.env` matching, `--agent <selector>` (repeatable) supplies
1585
+ * agent-identity candidates for the `agents` dimension, and
1586
+ * `--platform <name>` overrides the host platform for `when.platform`.
1587
+ * The JSON argument tail is kept verbatim, so quoted JSON survives
1588
+ * unchanged.
1486
1589
  * @param raw - the full raw command input.
1487
1590
  * @param verbRaw - the verb as typed.
1488
1591
  * @param invocation - the received command invocation (session agent).
@@ -1495,6 +1598,7 @@ var PermissionRulesRuntime = class {
1495
1598
  const envOverrides = {};
1496
1599
  const agentSelectors = [];
1497
1600
  let testCwd;
1601
+ let testPlatform;
1498
1602
  let parsed = nextToken(rest);
1499
1603
  while (parsed !== void 0 && parsed.token.startsWith("--")) {
1500
1604
  const flag = parsed.token;
@@ -1512,7 +1616,13 @@ var PermissionRulesRuntime = class {
1512
1616
  };
1513
1617
  envOverrides[value.token.slice(0, equals)] = value.token.slice(equals + 1);
1514
1618
  } else if (flag === "--agent") agentSelectors.push(value.token);
1515
- else return {
1619
+ else if (flag === "--platform") {
1620
+ if (!PLATFORMS.includes(value.token)) return {
1621
+ kind: "error",
1622
+ text: prose.testBadPlatform(value.token)
1623
+ };
1624
+ testPlatform = value.token;
1625
+ } else return {
1516
1626
  kind: "error",
1517
1627
  text: prose.testUnknownFlag(flag)
1518
1628
  };
@@ -1545,7 +1655,7 @@ var PermissionRulesRuntime = class {
1545
1655
  };
1546
1656
  }
1547
1657
  const context = {
1548
- platform: process.platform,
1658
+ platform: testPlatform ?? process.platform,
1549
1659
  env: Object.keys(envOverrides).length === 0 ? process.env : {
1550
1660
  ...process.env,
1551
1661
  ...envOverrides
@@ -1558,19 +1668,26 @@ var PermissionRulesRuntime = class {
1558
1668
  text: hit === void 0 ? prose.testNoMatch(tool) : prose.testHit(tool, hit.ruleIndex, hit.rule.action, hit.rule.reason)
1559
1669
  };
1560
1670
  }
1671
+ /** Render one rule-file source for per-rule attribution: workspace-relative when inside the cwd, raw otherwise. */
1672
+ displaySource(source, cwd) {
1673
+ if (source.length === 0) return "";
1674
+ const relative = normalizeWorkspacePath(cwd, source, this.config.caseInsensitivePaths);
1675
+ return relative.length > 0 ? relative : source;
1676
+ }
1561
1677
  /** Render the session's `permissionRules/decision` audit trail, newest last. */
1562
1678
  decisionsCommand(invocation, count, prose) {
1563
1679
  const decisions = invocation.agent.session.events.filter((event) => event.type === "permissionRules/decision");
1564
- if (decisions.length === 0) return {
1565
- kind: "success",
1566
- text: prose.noDecisions
1567
- };
1568
- const shown = decisions.slice(-count);
1569
- const lines = [prose.decisionsHeader(shown.length, decisions.length)];
1570
- for (const event of shown) {
1571
- const data = event.data;
1572
- lines.push(prose.decisionLine(event.seq, data.action, data.toolName, data.ruleIndex, data.reason, data.dryRun === true, data.outcome));
1680
+ const lines = [];
1681
+ if (decisions.length === 0) lines.push(prose.noDecisions);
1682
+ else {
1683
+ const shown = decisions.slice(-count);
1684
+ lines.push(prose.decisionsHeader(shown.length, decisions.length));
1685
+ for (const event of shown) {
1686
+ const data = event.data;
1687
+ lines.push(prose.decisionLine(event.seq, data.action, data.toolName, data.ruleIndex, data.reason, data.dryRun === true, data.outcome));
1688
+ }
1573
1689
  }
1690
+ if (this.auditSupport === "unsupported") lines.push(prose.auditDisabledNotice);
1574
1691
  return {
1575
1692
  kind: "success",
1576
1693
  text: lines.join("\n")
@@ -1642,16 +1759,97 @@ var PermissionRulesRuntime = class {
1642
1759
  * Rebind `cwd` to its current rule-source chain. When a concrete source
1643
1760
  * left the chain (deleted file, fallback switch), detach the workspace
1644
1761
  * from watchers serving other sources and close the ones left empty, so
1645
- * long-running hosts cannot accumulate stale watchers. An empty chain (no
1646
- * rule file in effect) keeps the previous watchers alive: they own the
1647
- * "file recreated → adopted" signal, and the workspace-cache bound keeps
1648
- * the set of such watchers finite.
1762
+ * long-running hosts cannot accumulate stale watchers. Expected-but-absent
1763
+ * rule files (the project file when it is not effective, and a configured
1764
+ * fallback that does not exist) are covered by candidate watchers on
1765
+ * their deepest existing ancestor directory, so a file created
1766
+ * mid-session is adopted without a manual `/rules reload`. With
1767
+ * `searchUp`, only the immediate cwd-level candidate is watched —
1768
+ * deeper ancestors are discovered on the next load.
1649
1769
  */
1650
1770
  reconcileWatch(cwd, sources) {
1651
- if (sources.length === 0) return;
1652
- for (const [watchedSource, entry] of this.watchers) if (!sources.includes(watchedSource)) entry.cwds.delete(cwd);
1771
+ for (const [watchedSource, entry] of this.watchers) if (!sources.includes(watchedSource)) {
1772
+ entry.cwds.delete(cwd);
1773
+ entry.candidates?.delete(cwd);
1774
+ }
1653
1775
  this.pruneWatchers();
1654
1776
  for (const source of sources) this.attachWatch(cwd, source);
1777
+ for (const candidate of this.candidateSources(cwd)) {
1778
+ if (sources.includes(candidate) || existsSync(candidate)) continue;
1779
+ this.attachCandidateWatch(cwd, candidate);
1780
+ }
1781
+ }
1782
+ /**
1783
+ * The rule-file paths a workspace COULD be served by: the project file
1784
+ * (an absolute `rulesFile`, or `<cwd>/<rulesFile>` — the immediate level
1785
+ * only under `searchUp`) plus the configured fallback.
1786
+ */
1787
+ candidateSources(cwd) {
1788
+ const candidates = [isAbsolute(this.config.rulesFile) ? this.config.rulesFile : join(cwd, this.config.rulesFile)];
1789
+ const fallback = this.config.fallbackPath;
1790
+ if (fallback !== void 0) candidates.push(isAbsolute(fallback) ? fallback : resolve(fallback));
1791
+ return candidates;
1792
+ }
1793
+ /**
1794
+ * Watch one expected-but-absent rule file through its deepest existing
1795
+ * ancestor directory. Chokidar cannot reliably watch a missing path when
1796
+ * its parent is also missing, but directory watching is dependable; every
1797
+ * relevant event re-checks existence and only triggers a reload once the
1798
+ * candidate actually appeared, so unrelated workspace activity while the
1799
+ * file is absent costs a stat, not a reload.
1800
+ */
1801
+ attachCandidateWatch(cwd, candidate) {
1802
+ if (!this.config.watch) return;
1803
+ const dir = this.deepestExistingDir(dirname(candidate));
1804
+ const existing = this.watchers.get(dir);
1805
+ if (existing !== void 0) {
1806
+ existing.cwds.add(cwd);
1807
+ existing.candidates?.set(cwd, candidate);
1808
+ return;
1809
+ }
1810
+ const cwds = /* @__PURE__ */ new Set([cwd]);
1811
+ const candidates = /* @__PURE__ */ new Map([[cwd, candidate]]);
1812
+ const watcher = chokidar.watch(dir, {
1813
+ persistent: true,
1814
+ ignoreInitial: true
1815
+ });
1816
+ const onEvent = () => this.scheduleReload(dir);
1817
+ watcher.on("add", onEvent);
1818
+ watcher.on("addDir", onEvent);
1819
+ watcher.on("change", onEvent);
1820
+ watcher.on("unlink", onEvent);
1821
+ watcher.on("unlinkDir", onEvent);
1822
+ watcher.on("error", (error) => {
1823
+ this.ctx.logger.warn(`permission-rules: watcher error on ${dir}: ${String(error)}`);
1824
+ });
1825
+ this.watchers.set(dir, {
1826
+ cwds,
1827
+ candidates,
1828
+ close: () => {
1829
+ watcher.close().catch((error) => {
1830
+ this.ctx.logger.warn(`permission-rules: failed to close watcher on ${dir}: ${String(error)}`);
1831
+ });
1832
+ }
1833
+ });
1834
+ this.ctx.effect(() => () => {
1835
+ this.watchers.get(dir)?.close();
1836
+ this.watchers.delete(dir);
1837
+ const timer = this.timers.get(dir);
1838
+ if (timer !== void 0) {
1839
+ clearTimeout(timer);
1840
+ this.timers.delete(dir);
1841
+ }
1842
+ });
1843
+ }
1844
+ /** The deepest existing ancestor directory of `dir` (candidate watchers target directories, never missing files). */
1845
+ deepestExistingDir(dir) {
1846
+ let current = dir;
1847
+ for (;;) {
1848
+ if (existsSync(current)) return current;
1849
+ const parent = dirname(current);
1850
+ if (parent === current) return current;
1851
+ current = parent;
1852
+ }
1655
1853
  }
1656
1854
  /** Close and drop watchers whose workspace sets are empty, clearing their debounce timers. */
1657
1855
  pruneWatchers() {
@@ -1678,7 +1876,10 @@ var PermissionRulesRuntime = class {
1678
1876
  const oldest = this.byCwd.keys().next().value;
1679
1877
  if (oldest === void 0) return;
1680
1878
  this.byCwd.delete(oldest);
1681
- for (const entry of this.watchers.values()) entry.cwds.delete(oldest);
1879
+ for (const entry of this.watchers.values()) {
1880
+ entry.cwds.delete(oldest);
1881
+ entry.candidates?.delete(oldest);
1882
+ }
1682
1883
  this.pruneWatchers();
1683
1884
  }
1684
1885
  /** Number of live watchers, for observability and tests. */
@@ -1689,13 +1890,24 @@ var PermissionRulesRuntime = class {
1689
1890
  pendingReloadCount() {
1690
1891
  return this.timers.size;
1691
1892
  }
1692
- /** Debounce watch events into one reload per stability window. */
1893
+ /**
1894
+ * Debounce watch events into one reload per stability window. A
1895
+ * candidate (directory) watcher only reloads a cwd once its expected
1896
+ * file actually exists — unrelated events while the file is absent are
1897
+ * dropped before any re-read.
1898
+ */
1693
1899
  scheduleReload(source) {
1694
1900
  const existing = this.timers.get(source);
1695
1901
  if (existing !== void 0) clearTimeout(existing);
1696
1902
  const timer = setTimeout(() => {
1697
1903
  this.timers.delete(source);
1698
- for (const cwd of this.watchers.get(source)?.cwds ?? []) this.reload(cwd);
1904
+ const entry = this.watchers.get(source);
1905
+ if (entry === void 0) return;
1906
+ for (const cwd of entry.cwds) {
1907
+ const candidate = entry.candidates?.get(cwd);
1908
+ if (candidate !== void 0 && !existsSync(candidate)) continue;
1909
+ this.reload(cwd);
1910
+ }
1699
1911
  }, this.config.watchStabilityThresholdMs);
1700
1912
  this.timers.set(source, timer);
1701
1913
  }
@@ -1714,11 +1926,26 @@ function apply(ctx, config) {
1714
1926
  ctx.commands.register({
1715
1927
  name: "rules",
1716
1928
  description: "list, reload, audit, or dry-test the active permission rules for this workspace",
1717
- input: { hint: "[reload | decisions [n] | test [--cwd <dir>] [--env K=V] [--agent <sel>] <tool> <json-args>]" },
1929
+ input: { hint: "[list | reload | decisions [n] | test [--cwd <dir>] [--env K=V] [--agent <sel>] [--platform <name>] <tool> <json-args>]" },
1718
1930
  handler: (invocation) => runtime.command(invocation)
1719
1931
  });
1720
1932
  }
1721
1933
  /**
1934
+ * Whether a `@deepseek-ai/dsh-session` version line predates the
1935
+ * `ignorable` envelope-marker surface: the `0.1.0-rc.6` line and earlier
1936
+ * silently drop the marker from `Session.append` options, so audit events
1937
+ * written by those builds land unmarked and break resume on stricter
1938
+ * hosts. Non-matching (post-rc.6, stable, or unresolvable) versions are
1939
+ * treated as possibly-marker-aware and verified by the append probe.
1940
+ * @param version - the installed peer version string.
1941
+ * @returns true for the known-unmarked rc.1–rc.6 lines.
1942
+ */
1943
+ function isUnmarkedHostVersion(version) {
1944
+ const match = /^0\.1\.0-rc\.(\d+)$/.exec(version.trim());
1945
+ if (match === null) return false;
1946
+ return Number(match[1]) <= 6;
1947
+ }
1948
+ /**
1722
1949
  * Identity candidates for the `agents` match dimension, derived from the
1723
1950
  * caller agent's session header: `main` for top-level sessions, `subagent`
1724
1951
  * for subagent children (`header.origin === 'subagent'`), and
@@ -1793,4 +2020,4 @@ function nextToken(text) {
1793
2020
  const name = "permission-rules";
1794
2021
  const inject = ["commands", "tools"];
1795
2022
  //#endregion
1796
- export { Config, GlobError, PATH_CANDIDATE_KEYS, PLATFORMS, PermissionRulesRuntime, RULE_ACTIONS, RuleError, apply, compileGlob, compilePatternRegex, compileRules, compileRulesChain, describeRule, extractPathCandidates, findUnreachableRules, inject, matchRules, name, normalizeWorkspacePath, parseRulesDocument, resolveConfig };
2023
+ export { Config, GlobError, PATH_CANDIDATE_KEYS, PLATFORMS, PermissionRulesRuntime, RULE_ACTIONS, RuleError, apply, compileGlob, compilePatternRegex, compileRules, compileRulesChain, describeRule, extractPathCandidates, findUnreachableRules, inject, isMarkedAuditEvent, isUnmarkedHostVersion, matchRules, name, normalizeWorkspacePath, parseRulesDocument, resolveConfig };