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/CHANGELOG.md +31 -0
- package/README.es.md +13 -7
- package/README.hi.md +13 -7
- package/README.md +13 -7
- package/README.pt.md +13 -7
- package/README.zh.md +13 -7
- package/cordis.patch.yml +3 -0
- package/docs/rules-format.en.md +4 -2
- package/docs/rules-format.md +4 -2
- package/lib/index.js +308 -81
- package/lib/types/config.d.ts +12 -0
- package/lib/types/config.d.ts.map +1 -1
- package/lib/types/config.js +3 -0
- package/lib/types/config.js.map +1 -1
- package/lib/types/events.d.ts +25 -6
- package/lib/types/events.d.ts.map +1 -1
- package/lib/types/events.js +22 -4
- package/lib/types/events.js.map +1 -1
- package/lib/types/index.d.ts +1 -1
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +1 -1
- package/lib/types/index.js.map +1 -1
- package/lib/types/prose.d.ts +6 -0
- package/lib/types/prose.d.ts.map +1 -1
- package/lib/types/prose.js +25 -15
- package/lib/types/prose.js.map +1 -1
- package/lib/types/rules.d.ts +8 -2
- package/lib/types/rules.d.ts.map +1 -1
- package/lib/types/rules.js +8 -3
- package/lib/types/rules.js.map +1 -1
- package/lib/types/runtime.d.ts +79 -17
- package/lib/types/runtime.d.ts.map +1 -1
- package/lib/types/runtime.js +249 -52
- package/lib/types/runtime.js.map +1 -1
- package/package.json +1 -1
- package/src/config.ts +15 -0
- package/src/events.ts +28 -6
- package/src/index.ts +1 -1
- package/src/prose.ts +31 -15
- package/src/rules.ts +10 -3
- package/src/runtime.ts +255 -49
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
1249
|
-
this.byCwd.set(
|
|
1302
|
+
this.byCwd.delete(key);
|
|
1303
|
+
this.byCwd.set(key, existing);
|
|
1250
1304
|
return existing;
|
|
1251
1305
|
}
|
|
1252
|
-
this.evictIfFull(
|
|
1306
|
+
this.evictIfFull(key);
|
|
1253
1307
|
try {
|
|
1254
1308
|
const loaded = this.load(cwd);
|
|
1255
|
-
this.byCwd.set(
|
|
1256
|
-
this.reconcileWatch(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
1287
|
-
this.reconcileWatch(
|
|
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(
|
|
1347
|
+
if (previous !== void 0) this.byCwd.set(key, {
|
|
1293
1348
|
...previous,
|
|
1294
1349
|
lastError: message
|
|
1295
1350
|
});
|
|
1296
|
-
else
|
|
1297
|
-
cwd
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
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
|
|
1365
|
-
*
|
|
1366
|
-
*
|
|
1367
|
-
*
|
|
1368
|
-
*
|
|
1369
|
-
*
|
|
1370
|
-
*
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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,
|
|
1484
|
-
* agent-identity candidates for the `agents` dimension
|
|
1485
|
-
*
|
|
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
|
|
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
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
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.
|
|
1646
|
-
* rule
|
|
1647
|
-
*
|
|
1648
|
-
*
|
|
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.
|
|
1652
|
-
|
|
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())
|
|
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
|
-
/**
|
|
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
|
-
|
|
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 };
|