dsh-permission-rules 0.2.0 → 0.4.0
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 +38 -0
- package/README.es.md +11 -6
- package/README.hi.md +11 -6
- package/README.md +11 -6
- package/README.pt.md +11 -6
- package/README.zh.md +11 -6
- package/docs/rules-format.en.md +7 -2
- package/docs/rules-format.md +7 -2
- package/docs/rules-format.schema.json +5 -0
- package/lib/index.js +467 -128
- package/lib/types/config.d.ts +12 -2
- package/lib/types/config.d.ts.map +1 -1
- package/lib/types/config.js +26 -2
- package/lib/types/config.js.map +1 -1
- package/lib/types/events.d.ts +14 -1
- package/lib/types/events.d.ts.map +1 -1
- package/lib/types/events.js +5 -0
- package/lib/types/events.js.map +1 -1
- package/lib/types/index.d.ts +8 -6
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +8 -6
- package/lib/types/index.js.map +1 -1
- package/lib/types/prose.d.ts +13 -2
- package/lib/types/prose.d.ts.map +1 -1
- package/lib/types/prose.js +50 -25
- package/lib/types/prose.js.map +1 -1
- package/lib/types/rules.d.ts +25 -2
- package/lib/types/rules.d.ts.map +1 -1
- package/lib/types/rules.js +37 -8
- package/lib/types/rules.js.map +1 -1
- package/lib/types/runtime.d.ts +84 -17
- package/lib/types/runtime.d.ts.map +1 -1
- package/lib/types/runtime.js +326 -71
- package/lib/types/runtime.js.map +1 -1
- package/package.json +103 -103
- package/src/config.ts +36 -2
- package/src/events.ts +14 -1
- package/src/index.ts +8 -6
- package/src/prose.ts +63 -27
- package/src/rules.ts +53 -8
- package/src/runtime.ts +320 -67
package/src/prose.ts
CHANGED
|
@@ -24,6 +24,8 @@ export interface UiProse {
|
|
|
24
24
|
reloadFailed: (error: string) => string
|
|
25
25
|
/** Warning appended to a listing whose last reload failed. */
|
|
26
26
|
lastReloadWarning: (error: string) => string
|
|
27
|
+
/** Notice appended to a listing when dry-run mode (`enforce: false`) is active. */
|
|
28
|
+
dryRunNotice: string
|
|
27
29
|
/** Rejection of an unknown `/rules` argument. */
|
|
28
30
|
unknownArg: (arg: string) => string
|
|
29
31
|
/** Usage hint. */
|
|
@@ -34,14 +36,20 @@ export interface UiProse {
|
|
|
34
36
|
noDecisions: string
|
|
35
37
|
/** Rejection of a malformed decisions count. */
|
|
36
38
|
invalidDecisionsCount: (arg: string) => string
|
|
37
|
-
/** One audit-decision row. */
|
|
38
|
-
decisionLine: (seq: number, action: string, toolName: string, ruleIndex: number | undefined, reason: string | undefined) => string
|
|
39
|
+
/** One audit-decision row (dry-run rows mark the actual outcome). */
|
|
40
|
+
decisionLine: (seq: number, action: string, toolName: string, ruleIndex: number | undefined, reason: string | undefined, dryRun: boolean, outcome: string | undefined) => string
|
|
39
41
|
/** Hit line of `/rules test`. */
|
|
40
42
|
testHit: (tool: string, ruleIndex: number, action: string, reason: string) => string
|
|
41
43
|
/** No-match line of `/rules test`. */
|
|
42
44
|
testNoMatch: (tool: string) => string
|
|
43
45
|
/** Rejection of unparseable `/rules test` JSON arguments. */
|
|
44
46
|
testBadJson: (text: string) => string
|
|
47
|
+
/** Rejection of an unknown `/rules test` flag. */
|
|
48
|
+
testUnknownFlag: (flag: string) => string
|
|
49
|
+
/** Rejection of a malformed `/rules test` flag (missing value, bad KEY=VALUE). */
|
|
50
|
+
testBadFlag: (flag: string) => string
|
|
51
|
+
/** Rejection of an unknown `--platform` value. */
|
|
52
|
+
testBadPlatform: (value: string) => string
|
|
45
53
|
/** Usage hint of `/rules test`. */
|
|
46
54
|
testUsage: string
|
|
47
55
|
/** Warning naming shadowed (unreachable) rules by 1-based number. */
|
|
@@ -54,6 +62,7 @@ export interface UiProse {
|
|
|
54
62
|
export interface DescribeTokens {
|
|
55
63
|
allTools: string
|
|
56
64
|
tools: string
|
|
65
|
+
agents: string
|
|
57
66
|
params: string
|
|
58
67
|
paths: string
|
|
59
68
|
absent: string
|
|
@@ -61,6 +70,8 @@ export interface DescribeTokens {
|
|
|
61
70
|
platform: string
|
|
62
71
|
disabled: string
|
|
63
72
|
tags: string
|
|
73
|
+
/** Prefix of the per-rule source-file attribution. */
|
|
74
|
+
src: string
|
|
64
75
|
}
|
|
65
76
|
|
|
66
77
|
/** Truncate long reasons/descriptions in one-line displays. */
|
|
@@ -75,20 +86,25 @@ const EN: UiProse = {
|
|
|
75
86
|
reloaded: (count, source) => `Reloaded ${count} rule(s) from ${source}.`,
|
|
76
87
|
reloadFailed: error => `Reload failed: ${error}. The previous rules are still active.`,
|
|
77
88
|
lastReloadWarning: error => `Warning: the last reload failed (${error}); the rules listed above are the previous ones.`,
|
|
89
|
+
dryRunNotice: 'Dry-run mode (enforce: false): deny/ask hits are audit-logged only — every call is passed through.',
|
|
78
90
|
unknownArg: arg => `Unknown /rules argument "${arg}". ${EN.usage}`,
|
|
79
|
-
usage: 'Usage: /rules [reload | decisions [n] | test <tool> <json-args>]',
|
|
91
|
+
usage: 'Usage: /rules [list | reload | decisions [n] | test <tool> <json-args>]',
|
|
80
92
|
decisionsHeader: (shown, total) => `Last ${shown} of ${total} permission decision(s) for this session:`,
|
|
81
93
|
noDecisions: 'No permission decisions recorded in this session yet.',
|
|
82
94
|
invalidDecisionsCount: arg => `Invalid decisions count "${arg}": give a positive integer (default 10).`,
|
|
83
|
-
decisionLine: (seq, action, toolName, ruleIndex, reason) => {
|
|
95
|
+
decisionLine: (seq, action, toolName, ruleIndex, reason, dryRun, outcome) => {
|
|
84
96
|
const rule = ruleIndex === undefined ? '' : ` (rule ${ruleIndex + 1})`
|
|
97
|
+
const dry = dryRun ? (outcome === undefined ? ' (dry-run)' : ` (dry-run → ${outcome})`) : ''
|
|
85
98
|
const why = reason === undefined ? '' : `: ${short(reason)}`
|
|
86
|
-
return `- #${seq} ${action} ${toolName}${rule}${why}`
|
|
99
|
+
return `- #${seq} ${action} ${toolName}${rule}${dry}${why}`
|
|
87
100
|
},
|
|
88
101
|
testHit: (tool, ruleIndex, action, reason) => `"${tool}" matches rule ${ruleIndex + 1} (${action}): ${short(reason)}`,
|
|
89
102
|
testNoMatch: tool => `"${tool}" matches no rule — the call passes through.`,
|
|
90
103
|
testBadJson: text => `Invalid JSON arguments "${text}": the arguments must parse as JSON.`,
|
|
91
|
-
|
|
104
|
+
testUnknownFlag: flag => `Unknown /rules test flag "${flag}". ${EN.testUsage}`,
|
|
105
|
+
testBadFlag: flag => `Invalid /rules test flag "${flag}": expected a value. ${EN.testUsage}`,
|
|
106
|
+
testBadPlatform: value => `Unknown platform "${value}" in --platform: expected one of aix, android, darwin, freebsd, linux, openbsd, sunos, win32. ${EN.testUsage}`,
|
|
107
|
+
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"}',
|
|
92
108
|
unreachableWarning: numbers => `Warning: rule${numbers.length > 1 ? 's' : ''} ${numbers.join(', ')} ${numbers.length > 1 ? 'are' : 'is'} unreachable (shadowed by an earlier catch-all rule).`,
|
|
93
109
|
emptySource: '(no rule file — empty rule set)',
|
|
94
110
|
}
|
|
@@ -100,20 +116,25 @@ const ZH: UiProse = {
|
|
|
100
116
|
reloaded: (count, source) => `已重载 ${count} 条规则,来自 ${source}。`,
|
|
101
117
|
reloadFailed: error => `重载失败:${error}。之前的规则仍然生效。`,
|
|
102
118
|
lastReloadWarning: error => `警告:上次重载失败(${error});以上列出的是之前的规则。`,
|
|
119
|
+
dryRunNotice: '干跑模式(enforce: false):deny/ask 命中仅写入审计日志——所有调用照常透传。',
|
|
103
120
|
unknownArg: arg => `未知的 /rules 参数 "${arg}"。${ZH.usage}`,
|
|
104
|
-
usage: '用法:/rules [reload | decisions [n] | test <工具名> <json-参数>]',
|
|
121
|
+
usage: '用法:/rules [list | reload | decisions [n] | test <工具名> <json-参数>]',
|
|
105
122
|
decisionsHeader: (shown, total) => `本会话最近 ${shown}/${total} 条权限裁决:`,
|
|
106
123
|
noDecisions: '本会话还没有记录任何权限裁决。',
|
|
107
124
|
invalidDecisionsCount: arg => `无效的裁决条数 "${arg}":请给正整数(默认 10)。`,
|
|
108
|
-
decisionLine: (seq, action, toolName, ruleIndex, reason) => {
|
|
125
|
+
decisionLine: (seq, action, toolName, ruleIndex, reason, dryRun, outcome) => {
|
|
109
126
|
const rule = ruleIndex === undefined ? '' : `(规则 ${ruleIndex + 1})`
|
|
127
|
+
const dry = dryRun ? (outcome === undefined ? '(干跑)' : `(干跑 → ${outcome})`) : ''
|
|
110
128
|
const why = reason === undefined ? '' : `:${short(reason)}`
|
|
111
|
-
return `- #${seq} ${action} ${toolName}${rule}${why}`
|
|
129
|
+
return `- #${seq} ${action} ${toolName}${rule}${dry}${why}`
|
|
112
130
|
},
|
|
113
131
|
testHit: (tool, ruleIndex, action, reason) => `"${tool}" 命中规则 ${ruleIndex + 1}(${action}):${short(reason)}`,
|
|
114
132
|
testNoMatch: tool => `"${tool}" 未命中任何规则——该调用将透传。`,
|
|
115
133
|
testBadJson: text => `无效的 JSON 参数 "${text}":参数必须能被解析为 JSON。`,
|
|
116
|
-
|
|
134
|
+
testUnknownFlag: flag => `未知的 /rules test 标志 "${flag}"。${ZH.testUsage}`,
|
|
135
|
+
testBadFlag: flag => `无效的 /rules test 标志 "${flag}":缺少取值。${ZH.testUsage}`,
|
|
136
|
+
testBadPlatform: value => `未知的平台 "${value}"(--platform):应为 aix、android、darwin、freebsd、linux、openbsd、sunos、win32 之一。${ZH.testUsage}`,
|
|
137
|
+
testUsage: '用法:/rules test [--cwd <目录>] [--env 键=值]... [--agent <选择器>]... [--platform <平台名>] <工具名> <json-参数>,例如 /rules test bash {"command":"git push origin main"}',
|
|
117
138
|
unreachableWarning: numbers => `警告:规则 ${numbers.join('、')} 不可达(被前面的通配规则遮蔽)。`,
|
|
118
139
|
emptySource: '(无规则文件——空规则集)',
|
|
119
140
|
}
|
|
@@ -125,20 +146,25 @@ const ES: UiProse = {
|
|
|
125
146
|
reloaded: (count, source) => `Recargadas ${count} regla(s) desde ${source}.`,
|
|
126
147
|
reloadFailed: error => `Error de recarga: ${error}. Las reglas anteriores siguen activas.`,
|
|
127
148
|
lastReloadWarning: error => `Aviso: la última recarga falló (${error}); las reglas mostradas son las anteriores.`,
|
|
149
|
+
dryRunNotice: 'Modo simulación (enforce: false): los aciertos deny/ask solo se registran en auditoría — todas las llamadas pasan.',
|
|
128
150
|
unknownArg: arg => `Argumento de /rules desconocido "${arg}". ${ES.usage}`,
|
|
129
|
-
usage: 'Uso: /rules [reload | decisions [n] | test <herramienta> <json-args>]',
|
|
151
|
+
usage: 'Uso: /rules [list | reload | decisions [n] | test <herramienta> <json-args>]',
|
|
130
152
|
decisionsHeader: (shown, total) => `Últimas ${shown} de ${total} decisión(es) de permiso de esta sesión:`,
|
|
131
153
|
noDecisions: 'Esta sesión aún no registra decisiones de permiso.',
|
|
132
154
|
invalidDecisionsCount: arg => `Cantidad de decisiones inválida "${arg}": indique un entero positivo (por defecto 10).`,
|
|
133
|
-
decisionLine: (seq, action, toolName, ruleIndex, reason) => {
|
|
155
|
+
decisionLine: (seq, action, toolName, ruleIndex, reason, dryRun, outcome) => {
|
|
134
156
|
const rule = ruleIndex === undefined ? '' : ` (regla ${ruleIndex + 1})`
|
|
157
|
+
const dry = dryRun ? (outcome === undefined ? ' (simulación)' : ` (simulación → ${outcome})`) : ''
|
|
135
158
|
const why = reason === undefined ? '' : `: ${short(reason)}`
|
|
136
|
-
return `- #${seq} ${action} ${toolName}${rule}${why}`
|
|
159
|
+
return `- #${seq} ${action} ${toolName}${rule}${dry}${why}`
|
|
137
160
|
},
|
|
138
161
|
testHit: (tool, ruleIndex, action, reason) => `"${tool}" coincide con la regla ${ruleIndex + 1} (${action}): ${short(reason)}`,
|
|
139
162
|
testNoMatch: tool => `"${tool}" no coincide con ninguna regla — la llamada pasa sin restricciones.`,
|
|
140
163
|
testBadJson: text => `Argumentos JSON inválidos "${text}": los argumentos deben poderse analizar como JSON.`,
|
|
141
|
-
|
|
164
|
+
testUnknownFlag: flag => `Bandera de /rules test desconocida "${flag}". ${ES.testUsage}`,
|
|
165
|
+
testBadFlag: flag => `Bandera de /rules test inválida "${flag}": falta un valor. ${ES.testUsage}`,
|
|
166
|
+
testBadPlatform: value => `Plataforma desconocida "${value}" en --platform: se esperaba una de aix, android, darwin, freebsd, linux, openbsd, sunos, win32. ${ES.testUsage}`,
|
|
167
|
+
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"}',
|
|
142
168
|
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).`,
|
|
143
169
|
emptySource: '(sin archivo de reglas — conjunto vacío)',
|
|
144
170
|
}
|
|
@@ -150,20 +176,25 @@ const PT: UiProse = {
|
|
|
150
176
|
reloaded: (count, source) => `Recarregadas ${count} regra(s) de ${source}.`,
|
|
151
177
|
reloadFailed: error => `Falha ao recarregar: ${error}. As regras anteriores continuam ativas.`,
|
|
152
178
|
lastReloadWarning: error => `Aviso: a última recarga falhou (${error}); as regras listadas são as anteriores.`,
|
|
179
|
+
dryRunNotice: 'Modo simulação (enforce: false): acertos deny/ask são apenas registrados na auditoria — todas as chamadas passam.',
|
|
153
180
|
unknownArg: arg => `Argumento de /rules desconhecido "${arg}". ${PT.usage}`,
|
|
154
|
-
usage: 'Uso: /rules [reload | decisions [n] | test <ferramenta> <json-args>]',
|
|
181
|
+
usage: 'Uso: /rules [list | reload | decisions [n] | test <ferramenta> <json-args>]',
|
|
155
182
|
decisionsHeader: (shown, total) => `Últimas ${shown} de ${total} decisão(ões) de permissão desta sessão:`,
|
|
156
183
|
noDecisions: 'Esta sessão ainda não registrou decisões de permissão.',
|
|
157
184
|
invalidDecisionsCount: arg => `Quantidade de decisões inválida "${arg}": informe um inteiro positivo (padrão 10).`,
|
|
158
|
-
decisionLine: (seq, action, toolName, ruleIndex, reason) => {
|
|
185
|
+
decisionLine: (seq, action, toolName, ruleIndex, reason, dryRun, outcome) => {
|
|
159
186
|
const rule = ruleIndex === undefined ? '' : ` (regra ${ruleIndex + 1})`
|
|
187
|
+
const dry = dryRun ? (outcome === undefined ? ' (simulação)' : ` (simulação → ${outcome})`) : ''
|
|
160
188
|
const why = reason === undefined ? '' : `: ${short(reason)}`
|
|
161
|
-
return `- #${seq} ${action} ${toolName}${rule}${why}`
|
|
189
|
+
return `- #${seq} ${action} ${toolName}${rule}${dry}${why}`
|
|
162
190
|
},
|
|
163
191
|
testHit: (tool, ruleIndex, action, reason) => `"${tool}" corresponde à regra ${ruleIndex + 1} (${action}): ${short(reason)}`,
|
|
164
192
|
testNoMatch: tool => `"${tool}" não corresponde a nenhuma regra — a chamada passa livremente.`,
|
|
165
193
|
testBadJson: text => `Argumentos JSON inválidos "${text}": os argumentos precisam ser JSON analisável.`,
|
|
166
|
-
|
|
194
|
+
testUnknownFlag: flag => `Sinalizador de /rules test desconhecido "${flag}". ${PT.testUsage}`,
|
|
195
|
+
testBadFlag: flag => `Sinalizador de /rules test inválido "${flag}": falta um valor. ${PT.testUsage}`,
|
|
196
|
+
testBadPlatform: value => `Plataforma desconhecida "${value}" em --platform: esperava-se uma de aix, android, darwin, freebsd, linux, openbsd, sunos, win32. ${PT.testUsage}`,
|
|
197
|
+
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"}',
|
|
167
198
|
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).`,
|
|
168
199
|
emptySource: '(sem arquivo de regras — conjunto vazio)',
|
|
169
200
|
}
|
|
@@ -175,20 +206,25 @@ const HI: UiProse = {
|
|
|
175
206
|
reloaded: (count, source) => `${count} नियम पुनः लोड किए गए, स्रोत ${source}।`,
|
|
176
207
|
reloadFailed: error => `पुनः लोड विफल: ${error}। पिछले नियम अब भी सक्रिय हैं।`,
|
|
177
208
|
lastReloadWarning: error => `चेतावनी: अंतिम पुनः लोड विफल रहा (${error}); ऊपर सूचीबद्ध नियम पिछले वाले हैं।`,
|
|
209
|
+
dryRunNotice: 'ड्राई-रन मोड (enforce: false): deny/ask हिट केवल ऑडिट में दर्ज होते हैं — हर कॉल पास हो जाती है।',
|
|
178
210
|
unknownArg: arg => `अज्ञात /rules तर्क "${arg}"। ${HI.usage}`,
|
|
179
|
-
usage: 'उपयोग: /rules [reload | decisions [n] | test <टूल> <json-args>]',
|
|
211
|
+
usage: 'उपयोग: /rules [list | reload | decisions [n] | test <टूल> <json-args>]',
|
|
180
212
|
decisionsHeader: (shown, total) => `इस सत्र के अंतिम ${shown}/${total} अनुमति निर्णय:`,
|
|
181
213
|
noDecisions: 'इस सत्र में अभी कोई अनुमति निर्णय दर्ज नहीं है।',
|
|
182
214
|
invalidDecisionsCount: arg => `अमान्य निर्णय-संख्या "${arg}": एक धनात्मक पूर्णांक दें (डिफ़ॉल्ट 10)।`,
|
|
183
|
-
decisionLine: (seq, action, toolName, ruleIndex, reason) => {
|
|
215
|
+
decisionLine: (seq, action, toolName, ruleIndex, reason, dryRun, outcome) => {
|
|
184
216
|
const rule = ruleIndex === undefined ? '' : ` (नियम ${ruleIndex + 1})`
|
|
217
|
+
const dry = dryRun ? (outcome === undefined ? ' (ड्राई-रन)' : ` (ड्राई-रन → ${outcome})`) : ''
|
|
185
218
|
const why = reason === undefined ? '' : `: ${short(reason)}`
|
|
186
|
-
return `- #${seq} ${action} ${toolName}${rule}${why}`
|
|
219
|
+
return `- #${seq} ${action} ${toolName}${rule}${dry}${why}`
|
|
187
220
|
},
|
|
188
221
|
testHit: (tool, ruleIndex, action, reason) => `"${tool}" नियम ${ruleIndex + 1} से मेल खाता है (${action}): ${short(reason)}`,
|
|
189
222
|
testNoMatch: tool => `"${tool}" किसी नियम से मेल नहीं खाता — कॉल बिना रोक पास होगी।`,
|
|
190
223
|
testBadJson: text => `अमान्य JSON तर्क "${text}": तर्क JSON के रूप में पार्स होने चाहिए।`,
|
|
191
|
-
|
|
224
|
+
testUnknownFlag: flag => `अज्ञात /rules test फ़्लैग "${flag}"। ${HI.testUsage}`,
|
|
225
|
+
testBadFlag: flag => `अमान्य /rules test फ़्लैग "${flag}": मान गायब है। ${HI.testUsage}`,
|
|
226
|
+
testBadPlatform: value => `अज्ञात प्लेटफ़ॉर्म "${value}" (--platform): aix, android, darwin, freebsd, linux, openbsd, sunos, win32 में से एक अपेक्षित। ${HI.testUsage}`,
|
|
227
|
+
testUsage: 'उपयोग: /rules test [--cwd <dir>] [--env KEY=मान]... [--agent <चयनकर्ता>]... [--platform <नाम>] <टूल> <json-args>, जैसे /rules test bash {"command":"git push origin main"}',
|
|
192
228
|
unreachableWarning: numbers => `चेतावनी: नियम ${numbers.join(', ')} अप्राप्य हैं (पहले वाले सर्व-मिलान नियम से ढके हुए)।`,
|
|
193
229
|
emptySource: '(कोई नियम फ़ाइल नहीं — खाली नियम-समूह)',
|
|
194
230
|
}
|
|
@@ -198,9 +234,9 @@ export const UI_PROSE: Readonly<Record<UiLanguage, UiProse>> = { en: EN, zh: ZH,
|
|
|
198
234
|
|
|
199
235
|
/** `describeRule` match-dimension tokens by language. */
|
|
200
236
|
export const DESCRIBE_TOKENS: Readonly<Record<UiLanguage, DescribeTokens>> = {
|
|
201
|
-
en: { allTools: 'all tools', tools: 'tools', params: 'params', paths: 'paths', absent: 'absent', when: 'when', platform: 'platform', disabled: 'disabled', tags: 'tags' },
|
|
202
|
-
zh: { allTools: '全部工具', tools: '工具', params: '参数', paths: '路径', absent: '缺省键', when: '条件', platform: '平台', disabled: '已禁用', tags: '标签' },
|
|
203
|
-
es: { allTools: 'todas las herramientas', tools: 'herramientas', params: 'parámetros', paths: 'rutas', absent: 'ausentes', when: 'cuándo', platform: 'plataforma', disabled: 'deshabilitada', tags: 'etiquetas' },
|
|
204
|
-
pt: { allTools: 'todas as ferramentas', tools: 'ferramentas', params: 'parâmetros', paths: 'caminhos', absent: 'ausentes', when: 'quando', platform: 'plataforma', disabled: 'desativada', tags: 'etiquetas' },
|
|
205
|
-
hi: { allTools: 'सभी टूल', tools: 'टूल', params: 'पैरामीटर', paths: 'पथ', absent: 'अनुपस्थित', when: 'शर्त', platform: 'प्लेटफ़ॉर्म', disabled: 'अक्षम', tags: 'टैग' },
|
|
237
|
+
en: { allTools: 'all tools', tools: 'tools', agents: 'agents', params: 'params', paths: 'paths', absent: 'absent', when: 'when', platform: 'platform', disabled: 'disabled', tags: 'tags', src: 'src' },
|
|
238
|
+
zh: { allTools: '全部工具', tools: '工具', agents: '代理', params: '参数', paths: '路径', absent: '缺省键', when: '条件', platform: '平台', disabled: '已禁用', tags: '标签', src: '来源' },
|
|
239
|
+
es: { allTools: 'todas las herramientas', tools: 'herramientas', agents: 'agentes', params: 'parámetros', paths: 'rutas', absent: 'ausentes', when: 'cuándo', platform: 'plataforma', disabled: 'deshabilitada', tags: 'etiquetas', src: 'origen' },
|
|
240
|
+
pt: { allTools: 'todas as ferramentas', tools: 'ferramentas', agents: 'agentes', params: 'parâmetros', paths: 'caminhos', absent: 'ausentes', when: 'quando', platform: 'plataforma', disabled: 'desativada', tags: 'etiquetas', src: 'origem' },
|
|
241
|
+
hi: { allTools: 'सभी टूल', tools: 'टूल', agents: 'एजेंट', params: 'पैरामीटर', paths: 'पथ', absent: 'अनुपस्थित', when: 'शर्त', platform: 'प्लेटफ़ॉर्म', disabled: 'अक्षम', tags: 'टैग', src: 'स्रोत' },
|
|
206
242
|
}
|
package/src/rules.ts
CHANGED
|
@@ -68,6 +68,14 @@ export interface RuleMatchDoc {
|
|
|
68
68
|
paths: string[]
|
|
69
69
|
/** Argument keys that must be ABSENT; every listed key must be missing. */
|
|
70
70
|
absent: string[]
|
|
71
|
+
/**
|
|
72
|
+
* Agent-identity selectors (globs against the calling agent's session
|
|
73
|
+
* header: `main`, `subagent`, and `preset:<name>` candidates); ANY
|
|
74
|
+
* selector matching ANY candidate satisfies the dimension. Empty matches
|
|
75
|
+
* every agent. Unknown agent identity never matches, so agent-scoped
|
|
76
|
+
* rules fail closed.
|
|
77
|
+
*/
|
|
78
|
+
agents: string[]
|
|
71
79
|
/** Environment/platform conditions; every listed env key must be present and match. */
|
|
72
80
|
when: WhenDoc
|
|
73
81
|
}
|
|
@@ -92,6 +100,8 @@ export interface CompiledRule {
|
|
|
92
100
|
readonly description?: string
|
|
93
101
|
readonly tags: readonly string[]
|
|
94
102
|
readonly tools: readonly RegExp[]
|
|
103
|
+
/** Agent-selector globs; empty = every agent. */
|
|
104
|
+
readonly agents: readonly RegExp[]
|
|
95
105
|
/** Param key → positive/negative pattern lists. */
|
|
96
106
|
readonly params: ReadonlyMap<string, { positive: readonly RegExp[]; negative: readonly RegExp[] }>
|
|
97
107
|
readonly paths: readonly RegExp[]
|
|
@@ -137,6 +147,12 @@ export interface MatchContext {
|
|
|
137
147
|
readonly platform?: string
|
|
138
148
|
/** Environment table; defaults to `process.env` when omitted. */
|
|
139
149
|
readonly env?: Readonly<Record<string, string | undefined>>
|
|
150
|
+
/**
|
|
151
|
+
* Agent-identity candidate strings the `agents` dimension matches
|
|
152
|
+
* against (`main`, `subagent`, `preset:<name>`); omitted (or empty)
|
|
153
|
+
* means the identity is unknown and agent-scoped rules never match.
|
|
154
|
+
*/
|
|
155
|
+
readonly agents?: readonly string[]
|
|
140
156
|
}
|
|
141
157
|
|
|
142
158
|
/**
|
|
@@ -207,6 +223,7 @@ export function compileRulesChain(entries: readonly RulesChainEntry[], options:
|
|
|
207
223
|
/** Compile one parsed rule into its hot-path form. */
|
|
208
224
|
function compileRule(entry: RuleDocEntry, index: number, sourceIndex: number, options: CompileOptions): CompiledRule {
|
|
209
225
|
const tools = entry.match.tools.map(pattern => compileToolPattern(pattern, options.maxGlobStars))
|
|
226
|
+
const agents = entry.match.agents.map(pattern => compileToolPattern(pattern, options.maxGlobStars))
|
|
210
227
|
const params = new Map<string, { positive: readonly RegExp[]; negative: readonly RegExp[] }>()
|
|
211
228
|
for (const [key, patterns] of Object.entries(entry.match.params)) {
|
|
212
229
|
params.set(key, compileParamPatterns(key, patterns, options))
|
|
@@ -225,6 +242,7 @@ function compileRule(entry: RuleDocEntry, index: number, sourceIndex: number, op
|
|
|
225
242
|
...(entry.description !== undefined ? { description: entry.description } : {}),
|
|
226
243
|
tags: entry.tags,
|
|
227
244
|
tools,
|
|
245
|
+
agents,
|
|
228
246
|
params,
|
|
229
247
|
paths,
|
|
230
248
|
absent: entry.match.absent,
|
|
@@ -263,6 +281,7 @@ export function matchRules(
|
|
|
263
281
|
for (const rule of ruleset.rules) {
|
|
264
282
|
if (!rule.enabled) continue
|
|
265
283
|
if (!matchTools(rule, toolName)) continue
|
|
284
|
+
if (!matchAgents(rule, context)) continue
|
|
266
285
|
if (!matchWhen(rule, context)) continue
|
|
267
286
|
if (rule.params.size > 0 && (argRecord === undefined || !matchParams(rule, argRecord))) continue
|
|
268
287
|
if (rule.absent.length > 0 && !matchAbsent(rule, argRecord)) continue
|
|
@@ -296,20 +315,25 @@ export function findUnreachableRules(ruleset: CompiledRuleset): number[] {
|
|
|
296
315
|
|
|
297
316
|
/** Whether every match dimension of one rule is empty. */
|
|
298
317
|
function isCatchAll(rule: CompiledRule): boolean {
|
|
299
|
-
return rule.tools.length === 0 && rule.params.size === 0 && rule.paths.length === 0 && rule.absent.length === 0 && rule.when === undefined
|
|
318
|
+
return rule.tools.length === 0 && rule.agents.length === 0 && rule.params.size === 0 && rule.paths.length === 0 && rule.absent.length === 0 && rule.when === undefined
|
|
300
319
|
}
|
|
301
320
|
|
|
302
321
|
/**
|
|
303
322
|
* One-line human summary of a compiled rule, for `/rules` output. The
|
|
304
323
|
* 1-based number, action, match dimensions, and reason render first; a
|
|
305
|
-
* disabled marker, tags, and a truncated description follow.
|
|
324
|
+
* disabled marker, tags, and a truncated description follow. When `source`
|
|
325
|
+
* is provided (multi-file chains), the rule's origin file is attributed on
|
|
326
|
+
* the line.
|
|
306
327
|
* @param rule - the compiled rule.
|
|
307
328
|
* @param tokens - dimension-prefix vocabulary for the configured language.
|
|
329
|
+
* @param source - display path of the rule's source file, when the listing
|
|
330
|
+
* spans several files.
|
|
308
331
|
* @returns a single display line.
|
|
309
332
|
*/
|
|
310
|
-
export function describeRule(rule: CompiledRule, tokens: DescribeTokens): string {
|
|
333
|
+
export function describeRule(rule: CompiledRule, tokens: DescribeTokens, source?: string): string {
|
|
311
334
|
const parts: string[] = []
|
|
312
335
|
if (rule.source.match.tools.length > 0) parts.push(`${tokens.tools}:${rule.source.match.tools.join(',')}`)
|
|
336
|
+
if (rule.source.match.agents.length > 0) parts.push(`${tokens.agents}:${rule.source.match.agents.join(',')}`)
|
|
313
337
|
const params = Object.entries(rule.source.match.params)
|
|
314
338
|
.map(([key, patterns]) => `${key}=${patterns.join('|')}`)
|
|
315
339
|
.join(' ')
|
|
@@ -323,15 +347,17 @@ export function describeRule(rule: CompiledRule, tokens: DescribeTokens): string
|
|
|
323
347
|
if (whenParts.length > 0 || platformPart.length > 0) parts.push(`${tokens.when}:${whenParts}${whenParts.length > 0 && platformPart.length > 0 ? ' ' : ''}${platformPart}`)
|
|
324
348
|
const match = parts.length > 0 ? `[${parts.join(' ')}]` : `[${tokens.allTools}]`
|
|
325
349
|
const disabled = rule.enabled ? '' : ` (${tokens.disabled})`
|
|
350
|
+
const sourcePart = source !== undefined && source.length > 0 ? ` [${tokens.src}:${source}]` : ''
|
|
326
351
|
const description = rule.description !== undefined ? ` (${truncate(rule.description, DISPLAY_TRUNCATE)})` : ''
|
|
327
352
|
const tags = rule.tags.length > 0 ? ` [${tokens.tags}:${rule.tags.join(',')}]` : ''
|
|
328
|
-
return `${rule.index + 1}. ${rule.action}${disabled} ${match}: ${truncate(rule.reason, DISPLAY_TRUNCATE)}${description}${tags}`
|
|
353
|
+
return `${rule.index + 1}. ${rule.action}${disabled} ${match}${sourcePart}: ${truncate(rule.reason, DISPLAY_TRUNCATE)}${description}${tags}`
|
|
329
354
|
}
|
|
330
355
|
|
|
331
356
|
/** The localized dimension-prefix vocabulary {@link describeRule} renders. */
|
|
332
357
|
export interface DescribeTokens {
|
|
333
358
|
readonly allTools: string
|
|
334
359
|
readonly tools: string
|
|
360
|
+
readonly agents: string
|
|
335
361
|
readonly params: string
|
|
336
362
|
readonly paths: string
|
|
337
363
|
readonly absent: string
|
|
@@ -339,6 +365,8 @@ export interface DescribeTokens {
|
|
|
339
365
|
readonly platform: string
|
|
340
366
|
readonly disabled: string
|
|
341
367
|
readonly tags: string
|
|
368
|
+
/** Prefix of the per-rule source-file attribution. */
|
|
369
|
+
readonly src: string
|
|
342
370
|
}
|
|
343
371
|
|
|
344
372
|
/** Cut a display string at `limit` characters, marking truncation with `…`. */
|
|
@@ -352,6 +380,19 @@ function matchTools(rule: CompiledRule, toolName: string): boolean {
|
|
|
352
380
|
return rule.tools.some(regex => regex.test(toolName))
|
|
353
381
|
}
|
|
354
382
|
|
|
383
|
+
/**
|
|
384
|
+
* Whether a rule's agents dimension selects the caller. Any selector glob
|
|
385
|
+
* matching any identity candidate satisfies the dimension; unknown
|
|
386
|
+
* identity (no agent or no candidates) fails the dimension, so an
|
|
387
|
+
* agent-scoped rule can never fire where the caller cannot be identified.
|
|
388
|
+
*/
|
|
389
|
+
function matchAgents(rule: CompiledRule, context: MatchContext): boolean {
|
|
390
|
+
if (rule.agents.length === 0) return true
|
|
391
|
+
const candidates = context.agents
|
|
392
|
+
if (candidates === undefined || candidates.length === 0) return false
|
|
393
|
+
return rule.agents.some(regex => candidates.some(candidate => regex.test(candidate)))
|
|
394
|
+
}
|
|
395
|
+
|
|
355
396
|
/** Whether a rule's params dimension holds: EVERY key present and matching. */
|
|
356
397
|
function matchParams(rule: CompiledRule, args: Record<string, unknown>): boolean {
|
|
357
398
|
for (const [key, { positive, negative }] of rule.params) {
|
|
@@ -474,14 +515,15 @@ function parseRuleEntry(raw: unknown, index: number): RuleDocEntry {
|
|
|
474
515
|
|
|
475
516
|
/** Validate the `match` block of one rule. */
|
|
476
517
|
function parseMatch(raw: unknown, at: string): RuleMatchDoc {
|
|
477
|
-
if (raw === undefined) return { tools: [], params: {}, paths: [], absent: [], when: { env: {}, platform: [] } }
|
|
518
|
+
if (raw === undefined) return { tools: [], agents: [], params: {}, paths: [], absent: [], when: { env: {}, platform: [] } }
|
|
478
519
|
const record = asRecord(raw, `${at}.match`)
|
|
479
|
-
const unknownFields = Object.keys(record).filter(key => key !== 'tools' && key !== 'params' && key !== 'paths' && key !== 'absent' && key !== 'when')
|
|
520
|
+
const unknownFields = Object.keys(record).filter(key => key !== 'tools' && key !== 'agents' && key !== 'params' && key !== 'paths' && key !== 'absent' && key !== 'when')
|
|
480
521
|
if (unknownFields.length > 0) {
|
|
481
|
-
throw new RuleError(`${at}.match: unknown field${unknownFields.length > 1 ? 's' : ''} ${unknownFields.map(k => JSON.stringify(k)).join(', ')} (allowed: tools, params, paths, absent, when)`)
|
|
522
|
+
throw new RuleError(`${at}.match: unknown field${unknownFields.length > 1 ? 's' : ''} ${unknownFields.map(k => JSON.stringify(k)).join(', ')} (allowed: tools, agents, params, paths, absent, when)`)
|
|
482
523
|
}
|
|
483
524
|
return {
|
|
484
525
|
tools: parseStringList(record['tools'], `${at}.match.tools`),
|
|
526
|
+
agents: parseStringList(record['agents'], `${at}.match.agents`),
|
|
485
527
|
params: parseParams(record['params'], `${at}.match.params`),
|
|
486
528
|
paths: parseStringList(record['paths'], `${at}.match.paths`),
|
|
487
529
|
absent: parseStringList(record['absent'], `${at}.match.absent`),
|
|
@@ -671,7 +713,10 @@ function relFromRoot(root: string, path: string, rest: string, rootRest: string,
|
|
|
671
713
|
const restLower = caseInsensitive ? rest.toLowerCase() : rest
|
|
672
714
|
const rootRestLower = caseInsensitive ? rootRest.toLowerCase() : rootRest
|
|
673
715
|
if (rootRestLower.length === 0) return rest.slice(1)
|
|
674
|
-
|
|
716
|
+
// A candidate equal to the root itself — modulo case only when the
|
|
717
|
+
// comparison is case-insensitive — is the workspace root, not a relative
|
|
718
|
+
// path inside it, and is dropped explicitly.
|
|
719
|
+
if (path === root || (caseInsensitive && path.toLowerCase() === root.toLowerCase())) return ''
|
|
675
720
|
return restLower.startsWith(`${rootRestLower}/`) ? rest.slice(rootRest.length + 1) : ''
|
|
676
721
|
}
|
|
677
722
|
|