Skip to content

Commit 950837d

Browse files
committed
feat(rules): add plain-English description field to SmartRule
- Add optional description field to SmartRule interface - Pass ruleDescription through policy → orchestrator → check.ts - Show description in /dev/tty review/block card for human-readable context - Add descriptions to all DEFAULT_CONFIG built-in rules and ADVISORY_SMART_RULES
1 parent b043891 commit 950837d

File tree

4 files changed

+36
-0
lines changed

4 files changed

+36
-0
lines changed

src/auth/orchestrator.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ export interface AuthResult {
5757
observeWouldBlock?: boolean;
5858
/** Recovery command to suggest when a stateful rule hard-blocks (e.g. "npm test"). */
5959
recoveryCommand?: string;
60+
/** Plain-English description of what triggered this result (from SmartRule.description). */
61+
ruleDescription?: string;
6062
}
6163

6264
// ── Taint helpers ────────────────────────────────────────────────────────────
@@ -451,6 +453,7 @@ async function _authorizeHeadlessCore(
451453
blockedByLabel: policyResult.blockedByLabel,
452454
ruleHit: policyResult.ruleName,
453455
...(policyResult.recoveryCommand && { recoveryCommand: policyResult.recoveryCommand }),
456+
...(policyResult.ruleDescription && { ruleDescription: policyResult.ruleDescription }),
454457
};
455458
}
456459
}

src/cli/commands/check.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ export function registerCheckCommand(program: Command): void {
144144
changeHint?: string;
145145
blockedByLabel?: string;
146146
recoveryCommand?: string;
147+
ruleDescription?: string;
147148
}
148149
) => {
149150
// 1. Determine the context (User vs Policy)
@@ -176,6 +177,7 @@ export function registerCheckCommand(program: Command): void {
176177
} else {
177178
writeTty(chalk.red(`\n🛑 Node9 blocked "${toolName}"`));
178179
}
180+
if (result?.ruleDescription) writeTty(chalk.white(` ${result.ruleDescription}`));
179181
writeTty(chalk.gray(` Triggered by: ${blockedByContext}`));
180182
if (result?.changeHint) writeTty(chalk.cyan(` To change: ${result.changeHint}`));
181183
if (result?.recoveryCommand)

src/config/index.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ export interface SmartRule {
3838
* Shown to the developer on /dev/tty and passed to the AI as a hint.
3939
* Example: "npm test" */
4040
recoveryCommand?: string;
41+
/** Plain-English explanation of what this rule does and why it matters.
42+
* Shown to the user in the review/block card instead of (or alongside) the raw command.
43+
* Example: "Force push rewrites shared history and can permanently destroy teammates' work." */
44+
description?: string;
4145
}
4246

4347
export interface EnvironmentConfig {
@@ -183,6 +187,8 @@ export const DEFAULT_CONFIG: Config = {
183187
],
184188
verdict: 'block',
185189
reason: 'Recursive delete of home directory is irreversible',
190+
description:
191+
'The AI wants to recursively delete your home directory. This will permanently destroy all your personal files and cannot be undone.',
186192
},
187193
// ── SQL safety ────────────────────────────────────────────────────────
188194
{
@@ -195,6 +201,8 @@ export const DEFAULT_CONFIG: Config = {
195201
conditionMode: 'all',
196202
verdict: 'review',
197203
reason: 'DELETE/UPDATE without WHERE clause — would affect every row in the table',
204+
description:
205+
'The AI is running a SQL statement that will modify every row in the table — no WHERE filter was found. This could wipe or corrupt all your data.',
198206
},
199207
{
200208
name: 'review-drop-truncate-shell',
@@ -210,6 +218,8 @@ export const DEFAULT_CONFIG: Config = {
210218
conditionMode: 'all',
211219
verdict: 'review',
212220
reason: 'SQL DDL destructive statement inside a shell command',
221+
description:
222+
'The AI wants to drop or truncate a database table via the shell. This permanently deletes the table structure or all its data.',
213223
},
214224
// ── Git safety ────────────────────────────────────────────────────────
215225
{
@@ -226,6 +236,8 @@ export const DEFAULT_CONFIG: Config = {
226236
conditionMode: 'all',
227237
verdict: 'block',
228238
reason: 'Force push overwrites remote history and cannot be undone',
239+
description:
240+
'The AI wants to force push to a remote git branch. This rewrites shared history and can permanently destroy commits that teammates have already pulled.',
229241
},
230242
{
231243
name: 'review-git-push',
@@ -241,6 +253,8 @@ export const DEFAULT_CONFIG: Config = {
241253
conditionMode: 'all',
242254
verdict: 'review',
243255
reason: 'git push sends changes to a shared remote',
256+
description:
257+
'The AI wants to push commits to a remote repository. Once pushed, those changes are visible to everyone with access.',
244258
},
245259
{
246260
name: 'review-git-destructive',
@@ -257,6 +271,8 @@ export const DEFAULT_CONFIG: Config = {
257271
conditionMode: 'all',
258272
verdict: 'review',
259273
reason: 'Destructive git operation — discards history or working-tree changes',
274+
description:
275+
'The AI wants to run a destructive git operation (reset, rebase, clean, or branch delete) that can permanently discard commits or uncommitted work.',
260276
},
261277
// ── Shell safety ──────────────────────────────────────────────────────
262278
{
@@ -266,6 +282,8 @@ export const DEFAULT_CONFIG: Config = {
266282
conditionMode: 'all',
267283
verdict: 'review',
268284
reason: 'Command requires elevated privileges',
285+
description:
286+
'The AI wants to run a command as root (sudo). Commands with root access can modify system files, install software, or change security settings.',
269287
},
270288
{
271289
name: 'review-curl-pipe-shell',
@@ -281,6 +299,8 @@ export const DEFAULT_CONFIG: Config = {
281299
conditionMode: 'all',
282300
verdict: 'block',
283301
reason: 'Piping remote script into a shell is a supply-chain attack vector',
302+
description:
303+
'The AI wants to download a script from the internet and run it immediately, without you seeing what it contains. This is one of the most common ways malware gets installed.',
284304
},
285305
],
286306
dlp: { enabled: true, scanIgnoredTools: true },
@@ -321,6 +341,8 @@ const ADVISORY_SMART_RULES: SmartRule[] = [
321341
conditions: [{ field: 'command', op: 'matches', value: '(^|&&|\\|\\||;)\\s*rm\\b' }],
322342
verdict: 'review',
323343
reason: 'rm can permanently delete files — confirm the target path',
344+
description:
345+
'The AI wants to delete files. Unlike moving to trash, rm is permanent — the files cannot be recovered without a backup.',
324346
},
325347
// ── SQL safety (Safe by Default) ──────────────────────────────────────────
326348
// These rules fire when an AI calls a database tool directly (e.g. MCP postgres,
@@ -333,13 +355,17 @@ const ADVISORY_SMART_RULES: SmartRule[] = [
333355
conditions: [{ field: 'sql', op: 'matches', value: 'DROP\\s+TABLE', flags: 'i' }],
334356
verdict: 'review',
335357
reason: 'DROP TABLE is irreversible — enable the postgres shield to block instead',
358+
description:
359+
'The AI wants to drop a database table. This permanently deletes the table and all its data — there is no undo.',
336360
},
337361
{
338362
name: 'review-truncate-sql',
339363
tool: '*',
340364
conditions: [{ field: 'sql', op: 'matches', value: 'TRUNCATE\\s+TABLE', flags: 'i' }],
341365
verdict: 'review',
342366
reason: 'TRUNCATE removes all rows — enable the postgres shield to block instead',
367+
description:
368+
'The AI wants to truncate a database table, which instantly deletes every row. The table structure remains but all data is gone.',
343369
},
344370
{
345371
name: 'review-drop-column-sql',
@@ -349,6 +375,8 @@ const ADVISORY_SMART_RULES: SmartRule[] = [
349375
],
350376
verdict: 'review',
351377
reason: 'DROP COLUMN is irreversible — enable the postgres shield to block instead',
378+
description:
379+
'The AI wants to drop a column from a database table. This permanently removes the column and all its data from every row.',
352380
},
353381
];
354382

src/policy/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,8 @@ export async function evaluatePolicy(
258258
dependsOnStatePredicates?: string[];
259259
/** Recovery command to suggest when this rule hard-blocks (from SmartRule.recoveryCommand). */
260260
recoveryCommand?: string;
261+
/** Plain-English description of what the rule does (from SmartRule.description). */
262+
ruleDescription?: string;
261263
}> {
262264
const config = getConfig();
263265

@@ -278,6 +280,7 @@ export async function evaluatePolicy(
278280
reason: matchedRule.reason,
279281
tier: 2,
280282
ruleName: matchedRule.name ?? matchedRule.tool,
283+
...(matchedRule.description && { ruleDescription: matchedRule.description }),
281284
...(matchedRule.verdict === 'block' &&
282285
matchedRule.dependsOnState?.length && {
283286
dependsOnStatePredicates: matchedRule.dependsOnState,

0 commit comments

Comments
 (0)