@node9/proxy 1.9.1 → 1.9.3
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/README.md +25 -5
- package/dist/cli.js +31 -13
- package/dist/cli.mjs +31 -13
- package/dist/index.js +30 -13
- package/dist/index.mjs +30 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
### The "Sudo" Command for AI Agents.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@node9/proxy)
|
|
6
|
-
[](https://opensource.org/licenses/Apache-2.0)
|
|
7
7
|
[](https://huggingface.co/spaces/Node9ai/node9-security-demo)
|
|
8
8
|
[](https://node9.ai/docs)
|
|
9
9
|
|
|
@@ -15,18 +15,18 @@
|
|
|
15
15
|
|
|
16
16
|
## The "Aha!" Moment
|
|
17
17
|
|
|
18
|
-
**AIs
|
|
18
|
+
**AIs move fast.** Ask an agent to "ship the fix" and it might push straight to git without asking you.
|
|
19
19
|
|
|
20
20
|
<p align="center">
|
|
21
|
-
<img src="https://github.com/user-attachments/assets/
|
|
21
|
+
<img src="https://github.com/user-attachments/assets/4aa6e45b-9aba-4953-9ce3-548226622588" width="100%">
|
|
22
22
|
</p>
|
|
23
23
|
|
|
24
24
|
With Node9:
|
|
25
25
|
|
|
26
|
-
1. **AI attempts:** `Bash("
|
|
26
|
+
1. **AI attempts:** `Bash("git push origin main")`
|
|
27
27
|
2. **Node9 intercepts:** OS-native popup appears instantly
|
|
28
28
|
3. **You block it** — one click
|
|
29
|
-
4. **AI pivots:** _"I'll
|
|
29
|
+
4. **AI pivots:** _"I'll create a PR for review instead"_
|
|
30
30
|
|
|
31
31
|
---
|
|
32
32
|
|
|
@@ -128,6 +128,26 @@ configure(agent_name="my-agent", policy="require_approval")
|
|
|
128
128
|
|
|
129
129
|
---
|
|
130
130
|
|
|
131
|
+
## Flight Recorder & HUD
|
|
132
|
+
|
|
133
|
+
Every tool call your AI agent makes is recorded — command, arguments, result, and cost estimate. Node9 wires a live statusline into Claude Code that shows you what's happening in real time:
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
🛡 node9 | standard | [bash-safe] | ✅ 12 allowed 🛑 2 blocked 🚨 0 dlp | ~$0.43 | ⚡ no-force-push
|
|
137
|
+
📊 claude-opus-4-6 | ctx [████████░░░░░░░] 54% | 5h [██░░░░░░░░░░░░░] 12% | 7d [█░░░░░░░░░░░░░░] 7%
|
|
138
|
+
🗂 2 CLAUDE.md | 8 rules | 3 MCPs | 4 hooks
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**Line 1 — Security state:** active mode, enabled shields, session totals (allowed / blocked / DLP hits), estimated cost, last rule that fired.
|
|
142
|
+
|
|
143
|
+
**Line 2 — Context & rate limits:** model name, context window usage, 5-hour and 7-day token rate-limit bars — so you can see when an agent is burning through quota.
|
|
144
|
+
|
|
145
|
+
**Line 3 — Environment:** how many CLAUDE.md files, rules, MCP servers, and hooks are active in the current project.
|
|
146
|
+
|
|
147
|
+
The HUD is wired automatically by `node9 setup`. Full session logs land in `~/.node9/audit.log`.
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
131
151
|
## 📖 Full docs
|
|
132
152
|
|
|
133
153
|
Everything else — config reference, smart rules, stateful rules, trusted hosts, approval modes, CLI reference — is at **[node9.ai/docs](https://node9.ai/docs)**.
|
package/dist/cli.js
CHANGED
|
@@ -214,6 +214,7 @@ var init_config_schema = __esm({
|
|
|
214
214
|
errorMap: () => ({ message: "verdict must be one of: allow, review, block" })
|
|
215
215
|
}),
|
|
216
216
|
reason: import_zod.z.string().optional(),
|
|
217
|
+
description: import_zod.z.string().optional(),
|
|
217
218
|
// Unknown predicate names are filtered out rather than failing the whole rule.
|
|
218
219
|
// Failing the whole z.array() would cause sanitizeConfig to drop the entire
|
|
219
220
|
// `policy` top-level key, silently disabling ALL smart rules in the config.
|
|
@@ -791,7 +792,8 @@ var init_config = __esm({
|
|
|
791
792
|
}
|
|
792
793
|
],
|
|
793
794
|
verdict: "block",
|
|
794
|
-
reason: "Recursive delete of home directory is irreversible"
|
|
795
|
+
reason: "Recursive delete of home directory is irreversible",
|
|
796
|
+
description: "The AI wants to recursively delete your home directory. This will permanently destroy all your personal files and cannot be undone."
|
|
795
797
|
},
|
|
796
798
|
// ── SQL safety ────────────────────────────────────────────────────────
|
|
797
799
|
{
|
|
@@ -803,7 +805,8 @@ var init_config = __esm({
|
|
|
803
805
|
],
|
|
804
806
|
conditionMode: "all",
|
|
805
807
|
verdict: "review",
|
|
806
|
-
reason: "DELETE/UPDATE without WHERE clause \u2014 would affect every row in the table"
|
|
808
|
+
reason: "DELETE/UPDATE without WHERE clause \u2014 would affect every row in the table",
|
|
809
|
+
description: "The AI is running a SQL statement that will modify every row in the table \u2014 no WHERE filter was found. This could wipe or corrupt all your data."
|
|
807
810
|
},
|
|
808
811
|
{
|
|
809
812
|
name: "review-drop-truncate-shell",
|
|
@@ -818,7 +821,8 @@ var init_config = __esm({
|
|
|
818
821
|
],
|
|
819
822
|
conditionMode: "all",
|
|
820
823
|
verdict: "review",
|
|
821
|
-
reason: "SQL DDL destructive statement inside a shell command"
|
|
824
|
+
reason: "SQL DDL destructive statement inside a shell command",
|
|
825
|
+
description: "The AI wants to drop or truncate a database table via the shell. This permanently deletes the table structure or all its data."
|
|
822
826
|
},
|
|
823
827
|
// ── Git safety ────────────────────────────────────────────────────────
|
|
824
828
|
{
|
|
@@ -834,7 +838,8 @@ var init_config = __esm({
|
|
|
834
838
|
],
|
|
835
839
|
conditionMode: "all",
|
|
836
840
|
verdict: "block",
|
|
837
|
-
reason: "Force push overwrites remote history and cannot be undone"
|
|
841
|
+
reason: "Force push overwrites remote history and cannot be undone",
|
|
842
|
+
description: "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."
|
|
838
843
|
},
|
|
839
844
|
{
|
|
840
845
|
name: "review-git-push",
|
|
@@ -849,7 +854,8 @@ var init_config = __esm({
|
|
|
849
854
|
],
|
|
850
855
|
conditionMode: "all",
|
|
851
856
|
verdict: "review",
|
|
852
|
-
reason: "git push sends changes to a shared remote"
|
|
857
|
+
reason: "git push sends changes to a shared remote",
|
|
858
|
+
description: "The AI wants to push commits to a remote repository. Once pushed, those changes are visible to everyone with access."
|
|
853
859
|
},
|
|
854
860
|
{
|
|
855
861
|
name: "review-git-destructive",
|
|
@@ -864,7 +870,8 @@ var init_config = __esm({
|
|
|
864
870
|
],
|
|
865
871
|
conditionMode: "all",
|
|
866
872
|
verdict: "review",
|
|
867
|
-
reason: "Destructive git operation \u2014 discards history or working-tree changes"
|
|
873
|
+
reason: "Destructive git operation \u2014 discards history or working-tree changes",
|
|
874
|
+
description: "The AI wants to run a destructive git operation (reset, rebase, clean, or branch delete) that can permanently discard commits or uncommitted work."
|
|
868
875
|
},
|
|
869
876
|
// ── Shell safety ──────────────────────────────────────────────────────
|
|
870
877
|
{
|
|
@@ -873,7 +880,8 @@ var init_config = __esm({
|
|
|
873
880
|
conditions: [{ field: "command", op: "matches", value: "\\bsudo\\s", flags: "i" }],
|
|
874
881
|
conditionMode: "all",
|
|
875
882
|
verdict: "review",
|
|
876
|
-
reason: "Command requires elevated privileges"
|
|
883
|
+
reason: "Command requires elevated privileges",
|
|
884
|
+
description: "The AI wants to run a command as root (sudo). Commands with root access can modify system files, install software, or change security settings."
|
|
877
885
|
},
|
|
878
886
|
{
|
|
879
887
|
name: "review-curl-pipe-shell",
|
|
@@ -888,7 +896,8 @@ var init_config = __esm({
|
|
|
888
896
|
],
|
|
889
897
|
conditionMode: "all",
|
|
890
898
|
verdict: "block",
|
|
891
|
-
reason: "Piping remote script into a shell is a supply-chain attack vector"
|
|
899
|
+
reason: "Piping remote script into a shell is a supply-chain attack vector",
|
|
900
|
+
description: "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."
|
|
892
901
|
}
|
|
893
902
|
],
|
|
894
903
|
dlp: { enabled: true, scanIgnoredTools: true }
|
|
@@ -921,7 +930,8 @@ var init_config = __esm({
|
|
|
921
930
|
tool: "*",
|
|
922
931
|
conditions: [{ field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" }],
|
|
923
932
|
verdict: "review",
|
|
924
|
-
reason: "rm can permanently delete files \u2014 confirm the target path"
|
|
933
|
+
reason: "rm can permanently delete files \u2014 confirm the target path",
|
|
934
|
+
description: "The AI wants to delete files. Unlike moving to trash, rm is permanent \u2014 the files cannot be recovered without a backup."
|
|
925
935
|
},
|
|
926
936
|
// ── SQL safety (Safe by Default) ──────────────────────────────────────────
|
|
927
937
|
// These rules fire when an AI calls a database tool directly (e.g. MCP postgres,
|
|
@@ -933,14 +943,16 @@ var init_config = __esm({
|
|
|
933
943
|
tool: "*",
|
|
934
944
|
conditions: [{ field: "sql", op: "matches", value: "DROP\\s+TABLE", flags: "i" }],
|
|
935
945
|
verdict: "review",
|
|
936
|
-
reason: "DROP TABLE is irreversible \u2014 enable the postgres shield to block instead"
|
|
946
|
+
reason: "DROP TABLE is irreversible \u2014 enable the postgres shield to block instead",
|
|
947
|
+
description: "The AI wants to drop a database table. This permanently deletes the table and all its data \u2014 there is no undo."
|
|
937
948
|
},
|
|
938
949
|
{
|
|
939
950
|
name: "review-truncate-sql",
|
|
940
951
|
tool: "*",
|
|
941
952
|
conditions: [{ field: "sql", op: "matches", value: "TRUNCATE\\s+TABLE", flags: "i" }],
|
|
942
953
|
verdict: "review",
|
|
943
|
-
reason: "TRUNCATE removes all rows \u2014 enable the postgres shield to block instead"
|
|
954
|
+
reason: "TRUNCATE removes all rows \u2014 enable the postgres shield to block instead",
|
|
955
|
+
description: "The AI wants to truncate a database table, which instantly deletes every row. The table structure remains but all data is gone."
|
|
944
956
|
},
|
|
945
957
|
{
|
|
946
958
|
name: "review-drop-column-sql",
|
|
@@ -949,7 +961,8 @@ var init_config = __esm({
|
|
|
949
961
|
{ field: "sql", op: "matches", value: "ALTER\\s+TABLE.*DROP\\s+COLUMN", flags: "i" }
|
|
950
962
|
],
|
|
951
963
|
verdict: "review",
|
|
952
|
-
reason: "DROP COLUMN is irreversible \u2014 enable the postgres shield to block instead"
|
|
964
|
+
reason: "DROP COLUMN is irreversible \u2014 enable the postgres shield to block instead",
|
|
965
|
+
description: "The AI wants to drop a column from a database table. This permanently removes the column and all its data from every row."
|
|
953
966
|
}
|
|
954
967
|
];
|
|
955
968
|
cachedConfig = null;
|
|
@@ -1844,6 +1857,9 @@ async function evaluatePolicy(toolName, args, agent, cwd) {
|
|
|
1844
1857
|
reason: matchedRule.reason,
|
|
1845
1858
|
tier: 2,
|
|
1846
1859
|
ruleName: matchedRule.name ?? matchedRule.tool,
|
|
1860
|
+
...(matchedRule.description ?? matchedRule.reason) && {
|
|
1861
|
+
ruleDescription: matchedRule.description ?? matchedRule.reason
|
|
1862
|
+
},
|
|
1847
1863
|
...matchedRule.verdict === "block" && matchedRule.dependsOnState?.length && {
|
|
1848
1864
|
dependsOnStatePredicates: matchedRule.dependsOnState
|
|
1849
1865
|
},
|
|
@@ -3289,7 +3305,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3289
3305
|
blockedBy: "local-config",
|
|
3290
3306
|
blockedByLabel: policyResult.blockedByLabel,
|
|
3291
3307
|
ruleHit: policyResult.ruleName,
|
|
3292
|
-
...policyResult.recoveryCommand && { recoveryCommand: policyResult.recoveryCommand }
|
|
3308
|
+
...policyResult.recoveryCommand && { recoveryCommand: policyResult.recoveryCommand },
|
|
3309
|
+
...policyResult.ruleDescription && { ruleDescription: policyResult.ruleDescription }
|
|
3293
3310
|
};
|
|
3294
3311
|
}
|
|
3295
3312
|
}
|
|
@@ -9066,6 +9083,7 @@ RAW: ${raw}
|
|
|
9066
9083
|
writeTty(import_chalk5.default.red(`
|
|
9067
9084
|
\u{1F6D1} Node9 blocked "${toolName}"`));
|
|
9068
9085
|
}
|
|
9086
|
+
if (result2?.ruleDescription) writeTty(import_chalk5.default.white(` ${result2.ruleDescription}`));
|
|
9069
9087
|
writeTty(import_chalk5.default.gray(` Triggered by: ${blockedByContext}`));
|
|
9070
9088
|
if (result2?.changeHint) writeTty(import_chalk5.default.cyan(` To change: ${result2.changeHint}`));
|
|
9071
9089
|
if (result2?.recoveryCommand)
|
package/dist/cli.mjs
CHANGED
|
@@ -192,6 +192,7 @@ var init_config_schema = __esm({
|
|
|
192
192
|
errorMap: () => ({ message: "verdict must be one of: allow, review, block" })
|
|
193
193
|
}),
|
|
194
194
|
reason: z.string().optional(),
|
|
195
|
+
description: z.string().optional(),
|
|
195
196
|
// Unknown predicate names are filtered out rather than failing the whole rule.
|
|
196
197
|
// Failing the whole z.array() would cause sanitizeConfig to drop the entire
|
|
197
198
|
// `policy` top-level key, silently disabling ALL smart rules in the config.
|
|
@@ -769,7 +770,8 @@ var init_config = __esm({
|
|
|
769
770
|
}
|
|
770
771
|
],
|
|
771
772
|
verdict: "block",
|
|
772
|
-
reason: "Recursive delete of home directory is irreversible"
|
|
773
|
+
reason: "Recursive delete of home directory is irreversible",
|
|
774
|
+
description: "The AI wants to recursively delete your home directory. This will permanently destroy all your personal files and cannot be undone."
|
|
773
775
|
},
|
|
774
776
|
// ── SQL safety ────────────────────────────────────────────────────────
|
|
775
777
|
{
|
|
@@ -781,7 +783,8 @@ var init_config = __esm({
|
|
|
781
783
|
],
|
|
782
784
|
conditionMode: "all",
|
|
783
785
|
verdict: "review",
|
|
784
|
-
reason: "DELETE/UPDATE without WHERE clause \u2014 would affect every row in the table"
|
|
786
|
+
reason: "DELETE/UPDATE without WHERE clause \u2014 would affect every row in the table",
|
|
787
|
+
description: "The AI is running a SQL statement that will modify every row in the table \u2014 no WHERE filter was found. This could wipe or corrupt all your data."
|
|
785
788
|
},
|
|
786
789
|
{
|
|
787
790
|
name: "review-drop-truncate-shell",
|
|
@@ -796,7 +799,8 @@ var init_config = __esm({
|
|
|
796
799
|
],
|
|
797
800
|
conditionMode: "all",
|
|
798
801
|
verdict: "review",
|
|
799
|
-
reason: "SQL DDL destructive statement inside a shell command"
|
|
802
|
+
reason: "SQL DDL destructive statement inside a shell command",
|
|
803
|
+
description: "The AI wants to drop or truncate a database table via the shell. This permanently deletes the table structure or all its data."
|
|
800
804
|
},
|
|
801
805
|
// ── Git safety ────────────────────────────────────────────────────────
|
|
802
806
|
{
|
|
@@ -812,7 +816,8 @@ var init_config = __esm({
|
|
|
812
816
|
],
|
|
813
817
|
conditionMode: "all",
|
|
814
818
|
verdict: "block",
|
|
815
|
-
reason: "Force push overwrites remote history and cannot be undone"
|
|
819
|
+
reason: "Force push overwrites remote history and cannot be undone",
|
|
820
|
+
description: "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."
|
|
816
821
|
},
|
|
817
822
|
{
|
|
818
823
|
name: "review-git-push",
|
|
@@ -827,7 +832,8 @@ var init_config = __esm({
|
|
|
827
832
|
],
|
|
828
833
|
conditionMode: "all",
|
|
829
834
|
verdict: "review",
|
|
830
|
-
reason: "git push sends changes to a shared remote"
|
|
835
|
+
reason: "git push sends changes to a shared remote",
|
|
836
|
+
description: "The AI wants to push commits to a remote repository. Once pushed, those changes are visible to everyone with access."
|
|
831
837
|
},
|
|
832
838
|
{
|
|
833
839
|
name: "review-git-destructive",
|
|
@@ -842,7 +848,8 @@ var init_config = __esm({
|
|
|
842
848
|
],
|
|
843
849
|
conditionMode: "all",
|
|
844
850
|
verdict: "review",
|
|
845
|
-
reason: "Destructive git operation \u2014 discards history or working-tree changes"
|
|
851
|
+
reason: "Destructive git operation \u2014 discards history or working-tree changes",
|
|
852
|
+
description: "The AI wants to run a destructive git operation (reset, rebase, clean, or branch delete) that can permanently discard commits or uncommitted work."
|
|
846
853
|
},
|
|
847
854
|
// ── Shell safety ──────────────────────────────────────────────────────
|
|
848
855
|
{
|
|
@@ -851,7 +858,8 @@ var init_config = __esm({
|
|
|
851
858
|
conditions: [{ field: "command", op: "matches", value: "\\bsudo\\s", flags: "i" }],
|
|
852
859
|
conditionMode: "all",
|
|
853
860
|
verdict: "review",
|
|
854
|
-
reason: "Command requires elevated privileges"
|
|
861
|
+
reason: "Command requires elevated privileges",
|
|
862
|
+
description: "The AI wants to run a command as root (sudo). Commands with root access can modify system files, install software, or change security settings."
|
|
855
863
|
},
|
|
856
864
|
{
|
|
857
865
|
name: "review-curl-pipe-shell",
|
|
@@ -866,7 +874,8 @@ var init_config = __esm({
|
|
|
866
874
|
],
|
|
867
875
|
conditionMode: "all",
|
|
868
876
|
verdict: "block",
|
|
869
|
-
reason: "Piping remote script into a shell is a supply-chain attack vector"
|
|
877
|
+
reason: "Piping remote script into a shell is a supply-chain attack vector",
|
|
878
|
+
description: "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."
|
|
870
879
|
}
|
|
871
880
|
],
|
|
872
881
|
dlp: { enabled: true, scanIgnoredTools: true }
|
|
@@ -899,7 +908,8 @@ var init_config = __esm({
|
|
|
899
908
|
tool: "*",
|
|
900
909
|
conditions: [{ field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" }],
|
|
901
910
|
verdict: "review",
|
|
902
|
-
reason: "rm can permanently delete files \u2014 confirm the target path"
|
|
911
|
+
reason: "rm can permanently delete files \u2014 confirm the target path",
|
|
912
|
+
description: "The AI wants to delete files. Unlike moving to trash, rm is permanent \u2014 the files cannot be recovered without a backup."
|
|
903
913
|
},
|
|
904
914
|
// ── SQL safety (Safe by Default) ──────────────────────────────────────────
|
|
905
915
|
// These rules fire when an AI calls a database tool directly (e.g. MCP postgres,
|
|
@@ -911,14 +921,16 @@ var init_config = __esm({
|
|
|
911
921
|
tool: "*",
|
|
912
922
|
conditions: [{ field: "sql", op: "matches", value: "DROP\\s+TABLE", flags: "i" }],
|
|
913
923
|
verdict: "review",
|
|
914
|
-
reason: "DROP TABLE is irreversible \u2014 enable the postgres shield to block instead"
|
|
924
|
+
reason: "DROP TABLE is irreversible \u2014 enable the postgres shield to block instead",
|
|
925
|
+
description: "The AI wants to drop a database table. This permanently deletes the table and all its data \u2014 there is no undo."
|
|
915
926
|
},
|
|
916
927
|
{
|
|
917
928
|
name: "review-truncate-sql",
|
|
918
929
|
tool: "*",
|
|
919
930
|
conditions: [{ field: "sql", op: "matches", value: "TRUNCATE\\s+TABLE", flags: "i" }],
|
|
920
931
|
verdict: "review",
|
|
921
|
-
reason: "TRUNCATE removes all rows \u2014 enable the postgres shield to block instead"
|
|
932
|
+
reason: "TRUNCATE removes all rows \u2014 enable the postgres shield to block instead",
|
|
933
|
+
description: "The AI wants to truncate a database table, which instantly deletes every row. The table structure remains but all data is gone."
|
|
922
934
|
},
|
|
923
935
|
{
|
|
924
936
|
name: "review-drop-column-sql",
|
|
@@ -927,7 +939,8 @@ var init_config = __esm({
|
|
|
927
939
|
{ field: "sql", op: "matches", value: "ALTER\\s+TABLE.*DROP\\s+COLUMN", flags: "i" }
|
|
928
940
|
],
|
|
929
941
|
verdict: "review",
|
|
930
|
-
reason: "DROP COLUMN is irreversible \u2014 enable the postgres shield to block instead"
|
|
942
|
+
reason: "DROP COLUMN is irreversible \u2014 enable the postgres shield to block instead",
|
|
943
|
+
description: "The AI wants to drop a column from a database table. This permanently removes the column and all its data from every row."
|
|
931
944
|
}
|
|
932
945
|
];
|
|
933
946
|
cachedConfig = null;
|
|
@@ -1827,6 +1840,9 @@ async function evaluatePolicy(toolName, args, agent, cwd) {
|
|
|
1827
1840
|
reason: matchedRule.reason,
|
|
1828
1841
|
tier: 2,
|
|
1829
1842
|
ruleName: matchedRule.name ?? matchedRule.tool,
|
|
1843
|
+
...(matchedRule.description ?? matchedRule.reason) && {
|
|
1844
|
+
ruleDescription: matchedRule.description ?? matchedRule.reason
|
|
1845
|
+
},
|
|
1830
1846
|
...matchedRule.verdict === "block" && matchedRule.dependsOnState?.length && {
|
|
1831
1847
|
dependsOnStatePredicates: matchedRule.dependsOnState
|
|
1832
1848
|
},
|
|
@@ -3267,7 +3283,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
3267
3283
|
blockedBy: "local-config",
|
|
3268
3284
|
blockedByLabel: policyResult.blockedByLabel,
|
|
3269
3285
|
ruleHit: policyResult.ruleName,
|
|
3270
|
-
...policyResult.recoveryCommand && { recoveryCommand: policyResult.recoveryCommand }
|
|
3286
|
+
...policyResult.recoveryCommand && { recoveryCommand: policyResult.recoveryCommand },
|
|
3287
|
+
...policyResult.ruleDescription && { ruleDescription: policyResult.ruleDescription }
|
|
3271
3288
|
};
|
|
3272
3289
|
}
|
|
3273
3290
|
}
|
|
@@ -9041,6 +9058,7 @@ RAW: ${raw}
|
|
|
9041
9058
|
writeTty(chalk5.red(`
|
|
9042
9059
|
\u{1F6D1} Node9 blocked "${toolName}"`));
|
|
9043
9060
|
}
|
|
9061
|
+
if (result2?.ruleDescription) writeTty(chalk5.white(` ${result2.ruleDescription}`));
|
|
9044
9062
|
writeTty(chalk5.gray(` Triggered by: ${blockedByContext}`));
|
|
9045
9063
|
if (result2?.changeHint) writeTty(chalk5.cyan(` To change: ${result2.changeHint}`));
|
|
9046
9064
|
if (result2?.recoveryCommand)
|
package/dist/index.js
CHANGED
|
@@ -199,6 +199,7 @@ var SmartRuleSchema = import_zod.z.object({
|
|
|
199
199
|
errorMap: () => ({ message: "verdict must be one of: allow, review, block" })
|
|
200
200
|
}),
|
|
201
201
|
reason: import_zod.z.string().optional(),
|
|
202
|
+
description: import_zod.z.string().optional(),
|
|
202
203
|
// Unknown predicate names are filtered out rather than failing the whole rule.
|
|
203
204
|
// Failing the whole z.array() would cause sanitizeConfig to drop the entire
|
|
204
205
|
// `policy` top-level key, silently disabling ALL smart rules in the config.
|
|
@@ -492,7 +493,8 @@ var DEFAULT_CONFIG = {
|
|
|
492
493
|
}
|
|
493
494
|
],
|
|
494
495
|
verdict: "block",
|
|
495
|
-
reason: "Recursive delete of home directory is irreversible"
|
|
496
|
+
reason: "Recursive delete of home directory is irreversible",
|
|
497
|
+
description: "The AI wants to recursively delete your home directory. This will permanently destroy all your personal files and cannot be undone."
|
|
496
498
|
},
|
|
497
499
|
// ── SQL safety ────────────────────────────────────────────────────────
|
|
498
500
|
{
|
|
@@ -504,7 +506,8 @@ var DEFAULT_CONFIG = {
|
|
|
504
506
|
],
|
|
505
507
|
conditionMode: "all",
|
|
506
508
|
verdict: "review",
|
|
507
|
-
reason: "DELETE/UPDATE without WHERE clause \u2014 would affect every row in the table"
|
|
509
|
+
reason: "DELETE/UPDATE without WHERE clause \u2014 would affect every row in the table",
|
|
510
|
+
description: "The AI is running a SQL statement that will modify every row in the table \u2014 no WHERE filter was found. This could wipe or corrupt all your data."
|
|
508
511
|
},
|
|
509
512
|
{
|
|
510
513
|
name: "review-drop-truncate-shell",
|
|
@@ -519,7 +522,8 @@ var DEFAULT_CONFIG = {
|
|
|
519
522
|
],
|
|
520
523
|
conditionMode: "all",
|
|
521
524
|
verdict: "review",
|
|
522
|
-
reason: "SQL DDL destructive statement inside a shell command"
|
|
525
|
+
reason: "SQL DDL destructive statement inside a shell command",
|
|
526
|
+
description: "The AI wants to drop or truncate a database table via the shell. This permanently deletes the table structure or all its data."
|
|
523
527
|
},
|
|
524
528
|
// ── Git safety ────────────────────────────────────────────────────────
|
|
525
529
|
{
|
|
@@ -535,7 +539,8 @@ var DEFAULT_CONFIG = {
|
|
|
535
539
|
],
|
|
536
540
|
conditionMode: "all",
|
|
537
541
|
verdict: "block",
|
|
538
|
-
reason: "Force push overwrites remote history and cannot be undone"
|
|
542
|
+
reason: "Force push overwrites remote history and cannot be undone",
|
|
543
|
+
description: "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."
|
|
539
544
|
},
|
|
540
545
|
{
|
|
541
546
|
name: "review-git-push",
|
|
@@ -550,7 +555,8 @@ var DEFAULT_CONFIG = {
|
|
|
550
555
|
],
|
|
551
556
|
conditionMode: "all",
|
|
552
557
|
verdict: "review",
|
|
553
|
-
reason: "git push sends changes to a shared remote"
|
|
558
|
+
reason: "git push sends changes to a shared remote",
|
|
559
|
+
description: "The AI wants to push commits to a remote repository. Once pushed, those changes are visible to everyone with access."
|
|
554
560
|
},
|
|
555
561
|
{
|
|
556
562
|
name: "review-git-destructive",
|
|
@@ -565,7 +571,8 @@ var DEFAULT_CONFIG = {
|
|
|
565
571
|
],
|
|
566
572
|
conditionMode: "all",
|
|
567
573
|
verdict: "review",
|
|
568
|
-
reason: "Destructive git operation \u2014 discards history or working-tree changes"
|
|
574
|
+
reason: "Destructive git operation \u2014 discards history or working-tree changes",
|
|
575
|
+
description: "The AI wants to run a destructive git operation (reset, rebase, clean, or branch delete) that can permanently discard commits or uncommitted work."
|
|
569
576
|
},
|
|
570
577
|
// ── Shell safety ──────────────────────────────────────────────────────
|
|
571
578
|
{
|
|
@@ -574,7 +581,8 @@ var DEFAULT_CONFIG = {
|
|
|
574
581
|
conditions: [{ field: "command", op: "matches", value: "\\bsudo\\s", flags: "i" }],
|
|
575
582
|
conditionMode: "all",
|
|
576
583
|
verdict: "review",
|
|
577
|
-
reason: "Command requires elevated privileges"
|
|
584
|
+
reason: "Command requires elevated privileges",
|
|
585
|
+
description: "The AI wants to run a command as root (sudo). Commands with root access can modify system files, install software, or change security settings."
|
|
578
586
|
},
|
|
579
587
|
{
|
|
580
588
|
name: "review-curl-pipe-shell",
|
|
@@ -589,7 +597,8 @@ var DEFAULT_CONFIG = {
|
|
|
589
597
|
],
|
|
590
598
|
conditionMode: "all",
|
|
591
599
|
verdict: "block",
|
|
592
|
-
reason: "Piping remote script into a shell is a supply-chain attack vector"
|
|
600
|
+
reason: "Piping remote script into a shell is a supply-chain attack vector",
|
|
601
|
+
description: "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."
|
|
593
602
|
}
|
|
594
603
|
],
|
|
595
604
|
dlp: { enabled: true, scanIgnoredTools: true }
|
|
@@ -622,7 +631,8 @@ var ADVISORY_SMART_RULES = [
|
|
|
622
631
|
tool: "*",
|
|
623
632
|
conditions: [{ field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" }],
|
|
624
633
|
verdict: "review",
|
|
625
|
-
reason: "rm can permanently delete files \u2014 confirm the target path"
|
|
634
|
+
reason: "rm can permanently delete files \u2014 confirm the target path",
|
|
635
|
+
description: "The AI wants to delete files. Unlike moving to trash, rm is permanent \u2014 the files cannot be recovered without a backup."
|
|
626
636
|
},
|
|
627
637
|
// ── SQL safety (Safe by Default) ──────────────────────────────────────────
|
|
628
638
|
// These rules fire when an AI calls a database tool directly (e.g. MCP postgres,
|
|
@@ -634,14 +644,16 @@ var ADVISORY_SMART_RULES = [
|
|
|
634
644
|
tool: "*",
|
|
635
645
|
conditions: [{ field: "sql", op: "matches", value: "DROP\\s+TABLE", flags: "i" }],
|
|
636
646
|
verdict: "review",
|
|
637
|
-
reason: "DROP TABLE is irreversible \u2014 enable the postgres shield to block instead"
|
|
647
|
+
reason: "DROP TABLE is irreversible \u2014 enable the postgres shield to block instead",
|
|
648
|
+
description: "The AI wants to drop a database table. This permanently deletes the table and all its data \u2014 there is no undo."
|
|
638
649
|
},
|
|
639
650
|
{
|
|
640
651
|
name: "review-truncate-sql",
|
|
641
652
|
tool: "*",
|
|
642
653
|
conditions: [{ field: "sql", op: "matches", value: "TRUNCATE\\s+TABLE", flags: "i" }],
|
|
643
654
|
verdict: "review",
|
|
644
|
-
reason: "TRUNCATE removes all rows \u2014 enable the postgres shield to block instead"
|
|
655
|
+
reason: "TRUNCATE removes all rows \u2014 enable the postgres shield to block instead",
|
|
656
|
+
description: "The AI wants to truncate a database table, which instantly deletes every row. The table structure remains but all data is gone."
|
|
645
657
|
},
|
|
646
658
|
{
|
|
647
659
|
name: "review-drop-column-sql",
|
|
@@ -650,7 +662,8 @@ var ADVISORY_SMART_RULES = [
|
|
|
650
662
|
{ field: "sql", op: "matches", value: "ALTER\\s+TABLE.*DROP\\s+COLUMN", flags: "i" }
|
|
651
663
|
],
|
|
652
664
|
verdict: "review",
|
|
653
|
-
reason: "DROP COLUMN is irreversible \u2014 enable the postgres shield to block instead"
|
|
665
|
+
reason: "DROP COLUMN is irreversible \u2014 enable the postgres shield to block instead",
|
|
666
|
+
description: "The AI wants to drop a column from a database table. This permanently removes the column and all its data from every row."
|
|
654
667
|
}
|
|
655
668
|
];
|
|
656
669
|
var cachedConfig = null;
|
|
@@ -1665,6 +1678,9 @@ async function evaluatePolicy(toolName, args, agent, cwd) {
|
|
|
1665
1678
|
reason: matchedRule.reason,
|
|
1666
1679
|
tier: 2,
|
|
1667
1680
|
ruleName: matchedRule.name ?? matchedRule.tool,
|
|
1681
|
+
...(matchedRule.description ?? matchedRule.reason) && {
|
|
1682
|
+
ruleDescription: matchedRule.description ?? matchedRule.reason
|
|
1683
|
+
},
|
|
1668
1684
|
...matchedRule.verdict === "block" && matchedRule.dependsOnState?.length && {
|
|
1669
1685
|
dependsOnStatePredicates: matchedRule.dependsOnState
|
|
1670
1686
|
},
|
|
@@ -2795,7 +2811,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2795
2811
|
blockedBy: "local-config",
|
|
2796
2812
|
blockedByLabel: policyResult.blockedByLabel,
|
|
2797
2813
|
ruleHit: policyResult.ruleName,
|
|
2798
|
-
...policyResult.recoveryCommand && { recoveryCommand: policyResult.recoveryCommand }
|
|
2814
|
+
...policyResult.recoveryCommand && { recoveryCommand: policyResult.recoveryCommand },
|
|
2815
|
+
...policyResult.ruleDescription && { ruleDescription: policyResult.ruleDescription }
|
|
2799
2816
|
};
|
|
2800
2817
|
}
|
|
2801
2818
|
}
|
package/dist/index.mjs
CHANGED
|
@@ -169,6 +169,7 @@ var SmartRuleSchema = z.object({
|
|
|
169
169
|
errorMap: () => ({ message: "verdict must be one of: allow, review, block" })
|
|
170
170
|
}),
|
|
171
171
|
reason: z.string().optional(),
|
|
172
|
+
description: z.string().optional(),
|
|
172
173
|
// Unknown predicate names are filtered out rather than failing the whole rule.
|
|
173
174
|
// Failing the whole z.array() would cause sanitizeConfig to drop the entire
|
|
174
175
|
// `policy` top-level key, silently disabling ALL smart rules in the config.
|
|
@@ -462,7 +463,8 @@ var DEFAULT_CONFIG = {
|
|
|
462
463
|
}
|
|
463
464
|
],
|
|
464
465
|
verdict: "block",
|
|
465
|
-
reason: "Recursive delete of home directory is irreversible"
|
|
466
|
+
reason: "Recursive delete of home directory is irreversible",
|
|
467
|
+
description: "The AI wants to recursively delete your home directory. This will permanently destroy all your personal files and cannot be undone."
|
|
466
468
|
},
|
|
467
469
|
// ── SQL safety ────────────────────────────────────────────────────────
|
|
468
470
|
{
|
|
@@ -474,7 +476,8 @@ var DEFAULT_CONFIG = {
|
|
|
474
476
|
],
|
|
475
477
|
conditionMode: "all",
|
|
476
478
|
verdict: "review",
|
|
477
|
-
reason: "DELETE/UPDATE without WHERE clause \u2014 would affect every row in the table"
|
|
479
|
+
reason: "DELETE/UPDATE without WHERE clause \u2014 would affect every row in the table",
|
|
480
|
+
description: "The AI is running a SQL statement that will modify every row in the table \u2014 no WHERE filter was found. This could wipe or corrupt all your data."
|
|
478
481
|
},
|
|
479
482
|
{
|
|
480
483
|
name: "review-drop-truncate-shell",
|
|
@@ -489,7 +492,8 @@ var DEFAULT_CONFIG = {
|
|
|
489
492
|
],
|
|
490
493
|
conditionMode: "all",
|
|
491
494
|
verdict: "review",
|
|
492
|
-
reason: "SQL DDL destructive statement inside a shell command"
|
|
495
|
+
reason: "SQL DDL destructive statement inside a shell command",
|
|
496
|
+
description: "The AI wants to drop or truncate a database table via the shell. This permanently deletes the table structure or all its data."
|
|
493
497
|
},
|
|
494
498
|
// ── Git safety ────────────────────────────────────────────────────────
|
|
495
499
|
{
|
|
@@ -505,7 +509,8 @@ var DEFAULT_CONFIG = {
|
|
|
505
509
|
],
|
|
506
510
|
conditionMode: "all",
|
|
507
511
|
verdict: "block",
|
|
508
|
-
reason: "Force push overwrites remote history and cannot be undone"
|
|
512
|
+
reason: "Force push overwrites remote history and cannot be undone",
|
|
513
|
+
description: "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."
|
|
509
514
|
},
|
|
510
515
|
{
|
|
511
516
|
name: "review-git-push",
|
|
@@ -520,7 +525,8 @@ var DEFAULT_CONFIG = {
|
|
|
520
525
|
],
|
|
521
526
|
conditionMode: "all",
|
|
522
527
|
verdict: "review",
|
|
523
|
-
reason: "git push sends changes to a shared remote"
|
|
528
|
+
reason: "git push sends changes to a shared remote",
|
|
529
|
+
description: "The AI wants to push commits to a remote repository. Once pushed, those changes are visible to everyone with access."
|
|
524
530
|
},
|
|
525
531
|
{
|
|
526
532
|
name: "review-git-destructive",
|
|
@@ -535,7 +541,8 @@ var DEFAULT_CONFIG = {
|
|
|
535
541
|
],
|
|
536
542
|
conditionMode: "all",
|
|
537
543
|
verdict: "review",
|
|
538
|
-
reason: "Destructive git operation \u2014 discards history or working-tree changes"
|
|
544
|
+
reason: "Destructive git operation \u2014 discards history or working-tree changes",
|
|
545
|
+
description: "The AI wants to run a destructive git operation (reset, rebase, clean, or branch delete) that can permanently discard commits or uncommitted work."
|
|
539
546
|
},
|
|
540
547
|
// ── Shell safety ──────────────────────────────────────────────────────
|
|
541
548
|
{
|
|
@@ -544,7 +551,8 @@ var DEFAULT_CONFIG = {
|
|
|
544
551
|
conditions: [{ field: "command", op: "matches", value: "\\bsudo\\s", flags: "i" }],
|
|
545
552
|
conditionMode: "all",
|
|
546
553
|
verdict: "review",
|
|
547
|
-
reason: "Command requires elevated privileges"
|
|
554
|
+
reason: "Command requires elevated privileges",
|
|
555
|
+
description: "The AI wants to run a command as root (sudo). Commands with root access can modify system files, install software, or change security settings."
|
|
548
556
|
},
|
|
549
557
|
{
|
|
550
558
|
name: "review-curl-pipe-shell",
|
|
@@ -559,7 +567,8 @@ var DEFAULT_CONFIG = {
|
|
|
559
567
|
],
|
|
560
568
|
conditionMode: "all",
|
|
561
569
|
verdict: "block",
|
|
562
|
-
reason: "Piping remote script into a shell is a supply-chain attack vector"
|
|
570
|
+
reason: "Piping remote script into a shell is a supply-chain attack vector",
|
|
571
|
+
description: "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."
|
|
563
572
|
}
|
|
564
573
|
],
|
|
565
574
|
dlp: { enabled: true, scanIgnoredTools: true }
|
|
@@ -592,7 +601,8 @@ var ADVISORY_SMART_RULES = [
|
|
|
592
601
|
tool: "*",
|
|
593
602
|
conditions: [{ field: "command", op: "matches", value: "(^|&&|\\|\\||;)\\s*rm\\b" }],
|
|
594
603
|
verdict: "review",
|
|
595
|
-
reason: "rm can permanently delete files \u2014 confirm the target path"
|
|
604
|
+
reason: "rm can permanently delete files \u2014 confirm the target path",
|
|
605
|
+
description: "The AI wants to delete files. Unlike moving to trash, rm is permanent \u2014 the files cannot be recovered without a backup."
|
|
596
606
|
},
|
|
597
607
|
// ── SQL safety (Safe by Default) ──────────────────────────────────────────
|
|
598
608
|
// These rules fire when an AI calls a database tool directly (e.g. MCP postgres,
|
|
@@ -604,14 +614,16 @@ var ADVISORY_SMART_RULES = [
|
|
|
604
614
|
tool: "*",
|
|
605
615
|
conditions: [{ field: "sql", op: "matches", value: "DROP\\s+TABLE", flags: "i" }],
|
|
606
616
|
verdict: "review",
|
|
607
|
-
reason: "DROP TABLE is irreversible \u2014 enable the postgres shield to block instead"
|
|
617
|
+
reason: "DROP TABLE is irreversible \u2014 enable the postgres shield to block instead",
|
|
618
|
+
description: "The AI wants to drop a database table. This permanently deletes the table and all its data \u2014 there is no undo."
|
|
608
619
|
},
|
|
609
620
|
{
|
|
610
621
|
name: "review-truncate-sql",
|
|
611
622
|
tool: "*",
|
|
612
623
|
conditions: [{ field: "sql", op: "matches", value: "TRUNCATE\\s+TABLE", flags: "i" }],
|
|
613
624
|
verdict: "review",
|
|
614
|
-
reason: "TRUNCATE removes all rows \u2014 enable the postgres shield to block instead"
|
|
625
|
+
reason: "TRUNCATE removes all rows \u2014 enable the postgres shield to block instead",
|
|
626
|
+
description: "The AI wants to truncate a database table, which instantly deletes every row. The table structure remains but all data is gone."
|
|
615
627
|
},
|
|
616
628
|
{
|
|
617
629
|
name: "review-drop-column-sql",
|
|
@@ -620,7 +632,8 @@ var ADVISORY_SMART_RULES = [
|
|
|
620
632
|
{ field: "sql", op: "matches", value: "ALTER\\s+TABLE.*DROP\\s+COLUMN", flags: "i" }
|
|
621
633
|
],
|
|
622
634
|
verdict: "review",
|
|
623
|
-
reason: "DROP COLUMN is irreversible \u2014 enable the postgres shield to block instead"
|
|
635
|
+
reason: "DROP COLUMN is irreversible \u2014 enable the postgres shield to block instead",
|
|
636
|
+
description: "The AI wants to drop a column from a database table. This permanently removes the column and all its data from every row."
|
|
624
637
|
}
|
|
625
638
|
];
|
|
626
639
|
var cachedConfig = null;
|
|
@@ -1635,6 +1648,9 @@ async function evaluatePolicy(toolName, args, agent, cwd) {
|
|
|
1635
1648
|
reason: matchedRule.reason,
|
|
1636
1649
|
tier: 2,
|
|
1637
1650
|
ruleName: matchedRule.name ?? matchedRule.tool,
|
|
1651
|
+
...(matchedRule.description ?? matchedRule.reason) && {
|
|
1652
|
+
ruleDescription: matchedRule.description ?? matchedRule.reason
|
|
1653
|
+
},
|
|
1638
1654
|
...matchedRule.verdict === "block" && matchedRule.dependsOnState?.length && {
|
|
1639
1655
|
dependsOnStatePredicates: matchedRule.dependsOnState
|
|
1640
1656
|
},
|
|
@@ -2765,7 +2781,8 @@ async function _authorizeHeadlessCore(toolName, args, meta, options) {
|
|
|
2765
2781
|
blockedBy: "local-config",
|
|
2766
2782
|
blockedByLabel: policyResult.blockedByLabel,
|
|
2767
2783
|
ruleHit: policyResult.ruleName,
|
|
2768
|
-
...policyResult.recoveryCommand && { recoveryCommand: policyResult.recoveryCommand }
|
|
2784
|
+
...policyResult.recoveryCommand && { recoveryCommand: policyResult.recoveryCommand },
|
|
2785
|
+
...policyResult.ruleDescription && { ruleDescription: policyResult.ruleDescription }
|
|
2769
2786
|
};
|
|
2770
2787
|
}
|
|
2771
2788
|
}
|