@aliou/pi-guardrails 0.11.2 → 0.12.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/README.md +72 -167
- package/extensions/guardrails/commands/examples/index.ts +520 -0
- package/extensions/guardrails/commands/onboarding/config.ts +54 -0
- package/{src/commands/onboarding-command.ts → extensions/guardrails/commands/onboarding/index.ts} +5 -31
- package/extensions/guardrails/commands/settings/add-rule-wizard.ts +267 -0
- package/extensions/guardrails/commands/settings/examples.ts +399 -0
- package/extensions/guardrails/commands/settings/index.ts +596 -0
- package/extensions/guardrails/commands/settings/path-list-editor.ts +158 -0
- package/extensions/guardrails/commands/settings/scope-picker-submenu.ts +69 -0
- package/extensions/guardrails/commands/settings/utils.ts +108 -0
- package/extensions/guardrails/components/onboarding-choice-step.ts +140 -0
- package/extensions/guardrails/components/onboarding-finish-step.ts +50 -0
- package/extensions/guardrails/components/onboarding-intro-step.ts +30 -0
- package/extensions/guardrails/components/onboarding-types.ts +10 -0
- package/extensions/guardrails/components/onboarding-wizard.ts +116 -0
- package/{src → extensions/guardrails}/components/pattern-editor.ts +11 -10
- package/extensions/guardrails/index.ts +106 -0
- package/extensions/guardrails/rules.test.ts +107 -0
- package/extensions/guardrails/rules.ts +119 -0
- package/extensions/guardrails/targets.test.ts +44 -0
- package/extensions/guardrails/targets.ts +66 -0
- package/extensions/path-access/grants.test.ts +47 -0
- package/extensions/path-access/grants.ts +68 -0
- package/extensions/path-access/index.ts +143 -0
- package/extensions/path-access/prompt.ts +196 -0
- package/extensions/path-access/rules.test.ts +46 -0
- package/extensions/path-access/rules.ts +37 -0
- package/extensions/path-access/targets.test.ts +40 -0
- package/extensions/path-access/targets.ts +19 -0
- package/extensions/permission-gate/grants.ts +21 -0
- package/extensions/permission-gate/index.ts +122 -0
- package/extensions/permission-gate/prompt.ts +222 -0
- package/extensions/permission-gate/rules.test.ts +132 -0
- package/extensions/permission-gate/rules.ts +72 -0
- package/package.json +18 -20
- package/schema.json +286 -0
- package/src/core/check.test.ts +169 -0
- package/src/core/check.ts +38 -0
- package/src/{hooks/permission-gate/dangerous-commands.test.ts → core/commands/dangerous.test.ts} +134 -2
- package/src/{hooks/permission-gate/dangerous-commands.ts → core/commands/dangerous.ts} +119 -1
- package/src/core/commands/index.ts +15 -0
- package/src/core/index.ts +13 -0
- package/src/{utils/path-access.test.ts → core/paths/access.test.ts} +1 -5
- package/src/core/paths/index.ts +14 -0
- package/src/{utils → core/shell}/command-args.test.ts +31 -20
- package/src/core/shell/index.ts +2 -0
- package/src/core/types.ts +55 -0
- package/src/shared/config/defaults.ts +118 -0
- package/src/shared/config/index.ts +17 -0
- package/src/shared/config/loader.ts +64 -0
- package/src/shared/config/migration/001-v0-format-upgrade.ts +107 -0
- package/src/shared/config/migration/002-strip-toolchain-fields.ts +39 -0
- package/src/shared/config/migration/003-strip-command-explainer-fields.ts +42 -0
- package/src/shared/config/migration/004-env-files-to-policies.ts +87 -0
- package/src/shared/config/migration/005-normalize-allowed-paths.ts +43 -0
- package/src/shared/config/migration/006-apply-builtin-defaults.ts +19 -0
- package/src/shared/config/migration/007-mark-onboarding-done.ts +25 -0
- package/src/shared/config/migration/index.ts +44 -0
- package/src/shared/config/migration/version.ts +7 -0
- package/src/shared/config/types.ts +141 -0
- package/src/shared/events.ts +100 -0
- package/src/shared/index.ts +6 -0
- package/src/shared/matching.test.ts +86 -0
- package/src/{utils → shared}/matching.ts +4 -4
- package/src/{utils → shared/paths}/bash-paths.test.ts +11 -2
- package/src/{utils → shared/paths}/bash-paths.ts +4 -4
- package/src/shared/paths/index.ts +1 -0
- package/src/shared/warnings.ts +17 -0
- package/docs/defaults.md +0 -140
- package/docs/examples.md +0 -170
- package/src/commands/onboarding.ts +0 -390
- package/src/commands/settings-command.ts +0 -1616
- package/src/config.ts +0 -392
- package/src/hooks/index.ts +0 -11
- package/src/hooks/path-access.ts +0 -395
- package/src/hooks/permission-gate/index.test.ts +0 -332
- package/src/hooks/permission-gate/index.ts +0 -595
- package/src/hooks/policies.ts +0 -322
- package/src/index.ts +0 -96
- package/src/lib/executor.ts +0 -280
- package/src/lib/index.ts +0 -16
- package/src/lib/model-resolver.ts +0 -47
- package/src/lib/timing.ts +0 -42
- package/src/lib/types.ts +0 -115
- package/src/utils/events.ts +0 -32
- package/src/utils/migration.test.ts +0 -58
- package/src/utils/migration.ts +0 -340
- package/src/utils/warnings.ts +0 -7
- /package/src/{utils/path-access.ts → core/paths/access.ts} +0 -0
- /package/src/{utils → core/paths}/path.test.ts +0 -0
- /package/src/{utils → core/paths}/path.ts +0 -0
- /package/src/{utils/shell-utils.ts → core/shell/ast.ts} +0 -0
- /package/src/{utils → core/shell}/command-args.ts +0 -0
- /package/src/{utils/glob-expander.ts → shared/glob.ts} +0 -0
package/schema.json
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$ref": "#/definitions/GuardrailsConfig",
|
|
3
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
4
|
+
"definitions": {
|
|
5
|
+
"DangerousPattern": {
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"description": "Permission gate pattern. When regex is false (default), the pattern is matched as substring against the raw command string. When regex is true, uses full regex against the raw string.",
|
|
8
|
+
"properties": {
|
|
9
|
+
"description": {
|
|
10
|
+
"description": "Optional description surfaced to the agent when the pattern triggers (e.g. auto-deny reason).",
|
|
11
|
+
"type": "string"
|
|
12
|
+
},
|
|
13
|
+
"pattern": {
|
|
14
|
+
"type": "string"
|
|
15
|
+
},
|
|
16
|
+
"regex": {
|
|
17
|
+
"type": "boolean"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"required": [
|
|
21
|
+
"description",
|
|
22
|
+
"pattern"
|
|
23
|
+
],
|
|
24
|
+
"type": "object"
|
|
25
|
+
},
|
|
26
|
+
"GuardrailsConfig": {
|
|
27
|
+
"additionalProperties": false,
|
|
28
|
+
"properties": {
|
|
29
|
+
"$schema": {
|
|
30
|
+
"description": "JSON Schema URL for editor autocomplete and validation. Added automatically when Guardrails writes the file.",
|
|
31
|
+
"type": "string"
|
|
32
|
+
},
|
|
33
|
+
"applyBuiltinDefaults": {
|
|
34
|
+
"description": "When true, include Guardrails built-in policy rules before user rules are merged.",
|
|
35
|
+
"type": "boolean"
|
|
36
|
+
},
|
|
37
|
+
"enabled": {
|
|
38
|
+
"description": "Enable or disable all Guardrails checks.",
|
|
39
|
+
"type": "boolean"
|
|
40
|
+
},
|
|
41
|
+
"envFiles": {
|
|
42
|
+
"additionalProperties": false,
|
|
43
|
+
"properties": {
|
|
44
|
+
"allowedPatterns": {
|
|
45
|
+
"items": {
|
|
46
|
+
"$ref": "#/definitions/PatternConfig"
|
|
47
|
+
},
|
|
48
|
+
"type": "array"
|
|
49
|
+
},
|
|
50
|
+
"blockMessage": {
|
|
51
|
+
"type": "string"
|
|
52
|
+
},
|
|
53
|
+
"onlyBlockIfExists": {
|
|
54
|
+
"type": "boolean"
|
|
55
|
+
},
|
|
56
|
+
"protectedDirectories": {
|
|
57
|
+
"items": {
|
|
58
|
+
"$ref": "#/definitions/PatternConfig"
|
|
59
|
+
},
|
|
60
|
+
"type": "array"
|
|
61
|
+
},
|
|
62
|
+
"protectedPatterns": {
|
|
63
|
+
"items": {
|
|
64
|
+
"$ref": "#/definitions/PatternConfig"
|
|
65
|
+
},
|
|
66
|
+
"type": "array"
|
|
67
|
+
},
|
|
68
|
+
"protectedTools": {
|
|
69
|
+
"items": {
|
|
70
|
+
"type": "string"
|
|
71
|
+
},
|
|
72
|
+
"type": "array"
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
"type": "object"
|
|
76
|
+
},
|
|
77
|
+
"features": {
|
|
78
|
+
"additionalProperties": false,
|
|
79
|
+
"description": "Enable or disable individual Guardrails feature extensions.",
|
|
80
|
+
"properties": {
|
|
81
|
+
"pathAccess": {
|
|
82
|
+
"type": "boolean"
|
|
83
|
+
},
|
|
84
|
+
"permissionGate": {
|
|
85
|
+
"type": "boolean"
|
|
86
|
+
},
|
|
87
|
+
"policies": {
|
|
88
|
+
"type": "boolean"
|
|
89
|
+
},
|
|
90
|
+
"protectEnvFiles": {
|
|
91
|
+
"type": "boolean"
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
"type": "object"
|
|
95
|
+
},
|
|
96
|
+
"onboarding": {
|
|
97
|
+
"additionalProperties": false,
|
|
98
|
+
"description": "Tracks whether the setup wizard has been completed. Usually managed by Guardrails.",
|
|
99
|
+
"properties": {
|
|
100
|
+
"completed": {
|
|
101
|
+
"description": "Whether onboarding is complete.",
|
|
102
|
+
"type": "boolean"
|
|
103
|
+
},
|
|
104
|
+
"completedAt": {
|
|
105
|
+
"description": "ISO timestamp for when onboarding completed.",
|
|
106
|
+
"type": "string"
|
|
107
|
+
},
|
|
108
|
+
"version": {
|
|
109
|
+
"description": "Guardrails config schema marker used when onboarding completed.",
|
|
110
|
+
"type": "string"
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
"type": "object"
|
|
114
|
+
},
|
|
115
|
+
"pathAccess": {
|
|
116
|
+
"$ref": "#/definitions/PathAccessConfig",
|
|
117
|
+
"description": "Outside-workspace path access settings."
|
|
118
|
+
},
|
|
119
|
+
"permissionGate": {
|
|
120
|
+
"additionalProperties": false,
|
|
121
|
+
"description": "Dangerous bash command detection and confirmation settings.",
|
|
122
|
+
"properties": {
|
|
123
|
+
"allowedPatterns": {
|
|
124
|
+
"description": "Command patterns that bypass dangerous command prompts.",
|
|
125
|
+
"items": {
|
|
126
|
+
"$ref": "#/definitions/PatternConfig"
|
|
127
|
+
},
|
|
128
|
+
"type": "array"
|
|
129
|
+
},
|
|
130
|
+
"autoDenyPatterns": {
|
|
131
|
+
"description": "Command patterns that are always blocked without prompting.",
|
|
132
|
+
"items": {
|
|
133
|
+
"$ref": "#/definitions/PatternConfig"
|
|
134
|
+
},
|
|
135
|
+
"type": "array"
|
|
136
|
+
},
|
|
137
|
+
"customPatterns": {
|
|
138
|
+
"description": "If set, replaces the default dangerous command patterns entirely.",
|
|
139
|
+
"items": {
|
|
140
|
+
"$ref": "#/definitions/DangerousPattern"
|
|
141
|
+
},
|
|
142
|
+
"type": "array"
|
|
143
|
+
},
|
|
144
|
+
"patterns": {
|
|
145
|
+
"description": "Additional dangerous command patterns.",
|
|
146
|
+
"items": {
|
|
147
|
+
"$ref": "#/definitions/DangerousPattern"
|
|
148
|
+
},
|
|
149
|
+
"type": "array"
|
|
150
|
+
},
|
|
151
|
+
"requireConfirmation": {
|
|
152
|
+
"description": "When true, prompt before running dangerous commands. When false, only warn.",
|
|
153
|
+
"type": "boolean"
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
"type": "object"
|
|
157
|
+
},
|
|
158
|
+
"policies": {
|
|
159
|
+
"additionalProperties": false,
|
|
160
|
+
"description": "File protection policies.",
|
|
161
|
+
"properties": {
|
|
162
|
+
"rules": {
|
|
163
|
+
"description": "Named policy rules. Rules with the same id override earlier rules across scopes.",
|
|
164
|
+
"items": {
|
|
165
|
+
"$ref": "#/definitions/PolicyRule"
|
|
166
|
+
},
|
|
167
|
+
"type": "array"
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
"type": "object"
|
|
171
|
+
},
|
|
172
|
+
"version": {
|
|
173
|
+
"description": "Internal config schema marker for migration/debugging. Not tied to the package version.",
|
|
174
|
+
"type": "string"
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
"type": "object"
|
|
178
|
+
},
|
|
179
|
+
"PathAccessConfig": {
|
|
180
|
+
"additionalProperties": false,
|
|
181
|
+
"properties": {
|
|
182
|
+
"allowedPaths": {
|
|
183
|
+
"items": {
|
|
184
|
+
"type": "string"
|
|
185
|
+
},
|
|
186
|
+
"type": "array"
|
|
187
|
+
},
|
|
188
|
+
"mode": {
|
|
189
|
+
"$ref": "#/definitions/PathAccessMode"
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
"type": "object"
|
|
193
|
+
},
|
|
194
|
+
"PathAccessMode": {
|
|
195
|
+
"enum": [
|
|
196
|
+
"allow",
|
|
197
|
+
"ask",
|
|
198
|
+
"block"
|
|
199
|
+
],
|
|
200
|
+
"type": "string"
|
|
201
|
+
},
|
|
202
|
+
"PatternConfig": {
|
|
203
|
+
"additionalProperties": false,
|
|
204
|
+
"description": "A pattern with explicit matching mode. Default: glob for files, substring for commands. regex: true means full regex matching.",
|
|
205
|
+
"properties": {
|
|
206
|
+
"description": {
|
|
207
|
+
"description": "Optional description surfaced to the agent when the pattern triggers (e.g. auto-deny reason).",
|
|
208
|
+
"type": "string"
|
|
209
|
+
},
|
|
210
|
+
"pattern": {
|
|
211
|
+
"type": "string"
|
|
212
|
+
},
|
|
213
|
+
"regex": {
|
|
214
|
+
"type": "boolean"
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
"required": [
|
|
218
|
+
"pattern"
|
|
219
|
+
],
|
|
220
|
+
"type": "object"
|
|
221
|
+
},
|
|
222
|
+
"PolicyRule": {
|
|
223
|
+
"additionalProperties": false,
|
|
224
|
+
"description": "A named policy rule. Matches files by patterns and enforces a protection level.",
|
|
225
|
+
"properties": {
|
|
226
|
+
"allowedPatterns": {
|
|
227
|
+
"description": "Optional exceptions.",
|
|
228
|
+
"items": {
|
|
229
|
+
"$ref": "#/definitions/PatternConfig"
|
|
230
|
+
},
|
|
231
|
+
"type": "array"
|
|
232
|
+
},
|
|
233
|
+
"blockMessage": {
|
|
234
|
+
"description": "Message shown when blocked; supports {file} placeholder.",
|
|
235
|
+
"type": "string"
|
|
236
|
+
},
|
|
237
|
+
"description": {
|
|
238
|
+
"description": "Human-readable description.",
|
|
239
|
+
"type": "string"
|
|
240
|
+
},
|
|
241
|
+
"enabled": {
|
|
242
|
+
"description": "Per-rule toggle. Default true.",
|
|
243
|
+
"type": "boolean"
|
|
244
|
+
},
|
|
245
|
+
"id": {
|
|
246
|
+
"description": "Stable identifier used for deduplication across scopes.",
|
|
247
|
+
"type": "string"
|
|
248
|
+
},
|
|
249
|
+
"name": {
|
|
250
|
+
"description": "Optional display name for settings/UI.",
|
|
251
|
+
"type": "string"
|
|
252
|
+
},
|
|
253
|
+
"onlyIfExists": {
|
|
254
|
+
"description": "Block only when file exists on disk. Default true.",
|
|
255
|
+
"type": "boolean"
|
|
256
|
+
},
|
|
257
|
+
"patterns": {
|
|
258
|
+
"description": "File patterns to protect.",
|
|
259
|
+
"items": {
|
|
260
|
+
"$ref": "#/definitions/PatternConfig"
|
|
261
|
+
},
|
|
262
|
+
"type": "array"
|
|
263
|
+
},
|
|
264
|
+
"protection": {
|
|
265
|
+
"$ref": "#/definitions/Protection",
|
|
266
|
+
"description": "Protection level."
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
"required": [
|
|
270
|
+
"id",
|
|
271
|
+
"patterns",
|
|
272
|
+
"protection"
|
|
273
|
+
],
|
|
274
|
+
"type": "object"
|
|
275
|
+
},
|
|
276
|
+
"Protection": {
|
|
277
|
+
"description": "Protection level for a policy rule.",
|
|
278
|
+
"enum": [
|
|
279
|
+
"none",
|
|
280
|
+
"readOnly",
|
|
281
|
+
"noAccess"
|
|
282
|
+
],
|
|
283
|
+
"type": "string"
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { checkAction, resolveDecision } from "./check";
|
|
3
|
+
import type { Action, Rule, Safety } from "./types";
|
|
4
|
+
|
|
5
|
+
const commandAction: Action = { kind: "command", command: "rm -rf /tmp/test" };
|
|
6
|
+
|
|
7
|
+
type TestMeta = { pattern: string; source: "test" };
|
|
8
|
+
|
|
9
|
+
const testMetadata: TestMeta = { pattern: "rm -rf", source: "test" };
|
|
10
|
+
|
|
11
|
+
describe("checkAction", () => {
|
|
12
|
+
it("returns safe when no rules match", async () => {
|
|
13
|
+
const rules: Rule[] = [
|
|
14
|
+
{
|
|
15
|
+
key: "sudo",
|
|
16
|
+
check: () => ({ kind: "pass" }),
|
|
17
|
+
},
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
await expect(checkAction(commandAction, rules)).resolves.toEqual({
|
|
21
|
+
kind: "safe",
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("returns dangerous for the first matching rule", async () => {
|
|
26
|
+
const secondCheck = vi.fn(() => ({
|
|
27
|
+
kind: "match" as const,
|
|
28
|
+
reason: "second match",
|
|
29
|
+
metadata: testMetadata,
|
|
30
|
+
}));
|
|
31
|
+
const rules: Rule<TestMeta>[] = [
|
|
32
|
+
{
|
|
33
|
+
key: "first",
|
|
34
|
+
check: () => ({
|
|
35
|
+
kind: "match",
|
|
36
|
+
reason: "first match",
|
|
37
|
+
metadata: testMetadata,
|
|
38
|
+
}),
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
key: "second",
|
|
42
|
+
check: secondCheck,
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
await expect(checkAction(commandAction, rules)).resolves.toEqual({
|
|
47
|
+
kind: "dangerous",
|
|
48
|
+
action: commandAction,
|
|
49
|
+
key: "first",
|
|
50
|
+
reason: "first match",
|
|
51
|
+
metadata: testMetadata,
|
|
52
|
+
});
|
|
53
|
+
expect(secondCheck).not.toHaveBeenCalled();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("supports async rules", async () => {
|
|
57
|
+
const rules: Rule[] = [
|
|
58
|
+
{
|
|
59
|
+
key: "async",
|
|
60
|
+
check: async (action) =>
|
|
61
|
+
action.kind === "command"
|
|
62
|
+
? {
|
|
63
|
+
kind: "match",
|
|
64
|
+
reason: "async match",
|
|
65
|
+
metadata: null,
|
|
66
|
+
}
|
|
67
|
+
: { kind: "pass" },
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
await expect(checkAction(commandAction, rules)).resolves.toMatchObject({
|
|
72
|
+
kind: "dangerous",
|
|
73
|
+
key: "async",
|
|
74
|
+
reason: "async match",
|
|
75
|
+
metadata: null,
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("propagates rule errors", async () => {
|
|
80
|
+
const error = new Error("rule failed");
|
|
81
|
+
const rules: Rule[] = [
|
|
82
|
+
{
|
|
83
|
+
key: "broken",
|
|
84
|
+
check: () => {
|
|
85
|
+
throw error;
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
await expect(checkAction(commandAction, rules)).rejects.toThrow(error);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("propagates async rule rejections", async () => {
|
|
94
|
+
const error = new Error("async rule failed");
|
|
95
|
+
const rules: Rule[] = [
|
|
96
|
+
{
|
|
97
|
+
key: "broken-async",
|
|
98
|
+
check: async () => {
|
|
99
|
+
throw error;
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
await expect(checkAction(commandAction, rules)).rejects.toThrow(error);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("preserves typed match metadata", async () => {
|
|
108
|
+
const rules: Rule<TestMeta>[] = [
|
|
109
|
+
{
|
|
110
|
+
key: "typed",
|
|
111
|
+
check: () => ({
|
|
112
|
+
kind: "match",
|
|
113
|
+
reason: "typed match",
|
|
114
|
+
metadata: testMetadata,
|
|
115
|
+
}),
|
|
116
|
+
},
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
const safety = await checkAction(commandAction, rules);
|
|
120
|
+
|
|
121
|
+
if (safety.kind !== "dangerous") {
|
|
122
|
+
throw new Error("expected dangerous safety");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
expect(safety.metadata.pattern).toBe("rm -rf");
|
|
126
|
+
|
|
127
|
+
const decision = resolveDecision(safety, "prompt");
|
|
128
|
+
|
|
129
|
+
if (decision.kind !== "prompt") {
|
|
130
|
+
throw new Error("expected prompt decision");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
expect(decision.risk.metadata.pattern).toBe("rm -rf");
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe("resolveDecision", () => {
|
|
138
|
+
const dangerous: Safety = {
|
|
139
|
+
kind: "dangerous",
|
|
140
|
+
action: commandAction,
|
|
141
|
+
key: "rm-rf",
|
|
142
|
+
reason: "recursive force delete",
|
|
143
|
+
metadata: null,
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
it("allows safe actions", () => {
|
|
147
|
+
expect(resolveDecision({ kind: "safe" }, "denied")).toEqual({
|
|
148
|
+
kind: "allow",
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("allows dangerous actions when permission is granted", () => {
|
|
153
|
+
expect(resolveDecision(dangerous, "granted")).toEqual({ kind: "allow" });
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("denies dangerous actions when permission is denied", () => {
|
|
157
|
+
expect(resolveDecision(dangerous, "denied")).toEqual({
|
|
158
|
+
kind: "deny",
|
|
159
|
+
reason: "recursive force delete",
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("prompts for dangerous actions when permission is prompt", () => {
|
|
164
|
+
expect(resolveDecision(dangerous, "prompt")).toEqual({
|
|
165
|
+
kind: "prompt",
|
|
166
|
+
risk: dangerous,
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Action, Decision, PermissionState, Rule, Safety } from "./types";
|
|
2
|
+
|
|
3
|
+
export async function checkAction<TMeta = null>(
|
|
4
|
+
action: Action,
|
|
5
|
+
rules: readonly Rule<TMeta>[],
|
|
6
|
+
): Promise<Safety<TMeta>> {
|
|
7
|
+
for (const rule of rules) {
|
|
8
|
+
const result = await rule.check(action);
|
|
9
|
+
|
|
10
|
+
if (result.kind === "match") {
|
|
11
|
+
return {
|
|
12
|
+
kind: "dangerous",
|
|
13
|
+
action,
|
|
14
|
+
key: rule.key,
|
|
15
|
+
reason: result.reason,
|
|
16
|
+
metadata: result.metadata,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return { kind: "safe" };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function resolveDecision<TMeta = null>(
|
|
25
|
+
safety: Safety<TMeta>,
|
|
26
|
+
permissionState: PermissionState,
|
|
27
|
+
): Decision<TMeta> {
|
|
28
|
+
if (safety.kind === "safe") return { kind: "allow" };
|
|
29
|
+
|
|
30
|
+
switch (permissionState) {
|
|
31
|
+
case "granted":
|
|
32
|
+
return { kind: "allow" };
|
|
33
|
+
case "denied":
|
|
34
|
+
return { kind: "deny", reason: safety.reason };
|
|
35
|
+
case "prompt":
|
|
36
|
+
return { kind: "prompt", risk: safety };
|
|
37
|
+
}
|
|
38
|
+
}
|
package/src/{hooks/permission-gate/dangerous-commands.test.ts → core/commands/dangerous.test.ts}
RENAMED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
BUILTIN_MATCHERS,
|
|
4
|
+
checkDangerousCommand,
|
|
5
|
+
compileCommandPatterns,
|
|
6
|
+
matchDangerousCommand,
|
|
7
|
+
} from "./dangerous";
|
|
3
8
|
|
|
4
9
|
/**
|
|
5
10
|
* Helper to run all matchers against a command string.
|
|
@@ -108,7 +113,7 @@ describe("dd matcher", () => {
|
|
|
108
113
|
).toBe("disk write operation");
|
|
109
114
|
});
|
|
110
115
|
|
|
111
|
-
it("
|
|
116
|
+
it("matches dd writing to /dev/null", () => {
|
|
112
117
|
expect(findMatch(["dd", "if=/dev/sda", "of=/dev/null"])).toBe(
|
|
113
118
|
"disk write operation",
|
|
114
119
|
);
|
|
@@ -269,6 +274,18 @@ describe("container matcher (docker/podman)", () => {
|
|
|
269
274
|
);
|
|
270
275
|
});
|
|
271
276
|
|
|
277
|
+
it("matches docker run --uts=host", () => {
|
|
278
|
+
expect(findMatch(["docker", "run", "--uts=host", "alpine"])).toBe(
|
|
279
|
+
"container with host UTS namespace",
|
|
280
|
+
);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("matches docker run --ipc=host", () => {
|
|
284
|
+
expect(findMatch(["docker", "run", "--ipc=host", "alpine"])).toBe(
|
|
285
|
+
"container with host IPC",
|
|
286
|
+
);
|
|
287
|
+
});
|
|
288
|
+
|
|
272
289
|
it("matches docker run with root mount", () => {
|
|
273
290
|
expect(findMatch(["docker", "run", "-v/:/host", "alpine"])).toBe(
|
|
274
291
|
"container with root filesystem mount",
|
|
@@ -315,6 +332,121 @@ describe("container matcher (docker/podman)", () => {
|
|
|
315
332
|
});
|
|
316
333
|
});
|
|
317
334
|
|
|
335
|
+
describe("checkDangerousCommand", () => {
|
|
336
|
+
it("matches built-in dangerous commands structurally", () => {
|
|
337
|
+
const result = checkDangerousCommand({
|
|
338
|
+
command: "rm -rf /tmp/example",
|
|
339
|
+
patterns: compileCommandPatterns([
|
|
340
|
+
{ pattern: "rm -rf", description: "recursive force delete" },
|
|
341
|
+
]),
|
|
342
|
+
useBuiltinMatchers: true,
|
|
343
|
+
fallbackPatterns: [
|
|
344
|
+
{ pattern: "rm -rf", description: "recursive force delete" },
|
|
345
|
+
],
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
expect(result).toEqual({
|
|
349
|
+
description: "recursive force delete",
|
|
350
|
+
pattern: "(structural)",
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it("skips built-in substring matches after a successful parse", () => {
|
|
355
|
+
const result = checkDangerousCommand({
|
|
356
|
+
command: "echo 'rm -rf /tmp/example'",
|
|
357
|
+
patterns: compileCommandPatterns([
|
|
358
|
+
{ pattern: "rm -rf", description: "recursive force delete" },
|
|
359
|
+
]),
|
|
360
|
+
useBuiltinMatchers: true,
|
|
361
|
+
fallbackPatterns: [
|
|
362
|
+
{ pattern: "rm -rf", description: "recursive force delete" },
|
|
363
|
+
],
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
expect(result).toBeUndefined();
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it("uses configured regex patterns", () => {
|
|
370
|
+
const result = checkDangerousCommand({
|
|
371
|
+
command: "terraform apply -auto-approve",
|
|
372
|
+
patterns: compileCommandPatterns([
|
|
373
|
+
{
|
|
374
|
+
pattern: "terraform\\s+apply",
|
|
375
|
+
description: "terraform apply",
|
|
376
|
+
regex: true,
|
|
377
|
+
},
|
|
378
|
+
]),
|
|
379
|
+
useBuiltinMatchers: false,
|
|
380
|
+
fallbackPatterns: [],
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
expect(result).toEqual({
|
|
384
|
+
description: "terraform apply",
|
|
385
|
+
pattern: "terraform\\s+apply",
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it("ignores invalid regex patterns", () => {
|
|
390
|
+
const result = checkDangerousCommand({
|
|
391
|
+
command: "anything",
|
|
392
|
+
patterns: compileCommandPatterns([
|
|
393
|
+
{ pattern: "[", description: "invalid", regex: true },
|
|
394
|
+
]),
|
|
395
|
+
useBuiltinMatchers: false,
|
|
396
|
+
fallbackPatterns: [],
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
expect(result).toBeUndefined();
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it("uses configured patterns when built-in matchers are disabled", () => {
|
|
403
|
+
const result = checkDangerousCommand({
|
|
404
|
+
command: "deploy production",
|
|
405
|
+
patterns: compileCommandPatterns([
|
|
406
|
+
{ pattern: "deploy production", description: "production deploy" },
|
|
407
|
+
]),
|
|
408
|
+
useBuiltinMatchers: false,
|
|
409
|
+
fallbackPatterns: [],
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
expect(result).toEqual({
|
|
413
|
+
description: "production deploy",
|
|
414
|
+
pattern: "deploy production",
|
|
415
|
+
});
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it("falls back to raw patterns when parsing fails", () => {
|
|
419
|
+
const result = checkDangerousCommand({
|
|
420
|
+
command: "if then rm -rf /tmp/example",
|
|
421
|
+
patterns: [],
|
|
422
|
+
useBuiltinMatchers: true,
|
|
423
|
+
fallbackPatterns: [
|
|
424
|
+
{ pattern: "rm -rf", description: "recursive force delete" },
|
|
425
|
+
],
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
expect(result).toEqual({
|
|
429
|
+
description: "recursive force delete",
|
|
430
|
+
pattern: "rm -rf",
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it.each([
|
|
435
|
+
["logical command", "echo ok && sudo true", "superuser command"],
|
|
436
|
+
["pipeline", "echo ok | sudo tee /tmp/out", "superuser command"],
|
|
437
|
+
["subshell", "(sudo true)", "superuser command"],
|
|
438
|
+
])("matches dangerous commands nested in a %s", (_label, command, description) => {
|
|
439
|
+
const result = checkDangerousCommand({
|
|
440
|
+
command,
|
|
441
|
+
patterns: [],
|
|
442
|
+
useBuiltinMatchers: true,
|
|
443
|
+
fallbackPatterns: [],
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
expect(result).toEqual({ description, pattern: "(structural)" });
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
|
|
318
450
|
describe("matchDangerousCommand", () => {
|
|
319
451
|
it("returns description and pattern for dangerous commands", () => {
|
|
320
452
|
const result = matchDangerousCommand(["sudo", "apt", "update"]);
|