@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
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { addPendingWarning } from "../../warnings";
|
|
2
|
+
import type { GuardrailsConfig } from "../types";
|
|
3
|
+
import { CURRENT_VERSION } from "./version";
|
|
4
|
+
|
|
5
|
+
const REMOVED_PERMISSION_GATE_KEYS = [
|
|
6
|
+
"explainCommands",
|
|
7
|
+
"explainModel",
|
|
8
|
+
"explainTimeout",
|
|
9
|
+
] as const;
|
|
10
|
+
|
|
11
|
+
export function shouldRun(config: GuardrailsConfig): boolean {
|
|
12
|
+
const raw = config as Record<string, unknown>;
|
|
13
|
+
const permissionGate = raw.permissionGate as
|
|
14
|
+
| Record<string, unknown>
|
|
15
|
+
| undefined;
|
|
16
|
+
if (!permissionGate) return false;
|
|
17
|
+
|
|
18
|
+
for (const key of REMOVED_PERMISSION_GATE_KEYS) {
|
|
19
|
+
if (key in permissionGate) return true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function run(config: GuardrailsConfig): GuardrailsConfig {
|
|
26
|
+
addPendingWarning(
|
|
27
|
+
"[guardrails] permissionGate.explainCommands, explainModel, and explainTimeout " +
|
|
28
|
+
"have been removed. These fields will be stripped from your config.",
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const cleaned = structuredClone(config) as Record<string, unknown>;
|
|
32
|
+
const permissionGate = cleaned.permissionGate as
|
|
33
|
+
| Record<string, unknown>
|
|
34
|
+
| undefined;
|
|
35
|
+
if (permissionGate) {
|
|
36
|
+
for (const key of REMOVED_PERMISSION_GATE_KEYS) {
|
|
37
|
+
delete permissionGate[key];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
cleaned.version = CURRENT_VERSION;
|
|
41
|
+
return cleaned as GuardrailsConfig;
|
|
42
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { addPendingWarning } from "../../warnings";
|
|
2
|
+
import type { GuardrailsConfig } from "../types";
|
|
3
|
+
import { CURRENT_VERSION } from "./version";
|
|
4
|
+
|
|
5
|
+
export function shouldRun(config: GuardrailsConfig): boolean {
|
|
6
|
+
const raw = config as Record<string, unknown>;
|
|
7
|
+
if (raw.envFiles !== undefined) return true;
|
|
8
|
+
|
|
9
|
+
const features = raw.features as Record<string, unknown> | undefined;
|
|
10
|
+
return features?.protectEnvFiles !== undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function run(config: GuardrailsConfig): GuardrailsConfig {
|
|
14
|
+
const migrated = structuredClone(config);
|
|
15
|
+
const raw = migrated as Record<string, unknown>;
|
|
16
|
+
const features = raw.features as Record<string, unknown> | undefined;
|
|
17
|
+
const envFiles = raw.envFiles as Record<string, unknown> | undefined;
|
|
18
|
+
|
|
19
|
+
if (features?.protectEnvFiles !== undefined) {
|
|
20
|
+
features.policies = features.protectEnvFiles;
|
|
21
|
+
delete features.protectEnvFiles;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (envFiles) {
|
|
25
|
+
const rule: Record<string, unknown> = {
|
|
26
|
+
id: "secret-files",
|
|
27
|
+
description: "Files containing secrets (migrated from envFiles)",
|
|
28
|
+
protection: "noAccess",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
if (envFiles.protectedPatterns) rule.patterns = envFiles.protectedPatterns;
|
|
32
|
+
if (envFiles.allowedPatterns)
|
|
33
|
+
rule.allowedPatterns = envFiles.allowedPatterns;
|
|
34
|
+
if (envFiles.onlyBlockIfExists !== undefined) {
|
|
35
|
+
rule.onlyIfExists = envFiles.onlyBlockIfExists;
|
|
36
|
+
}
|
|
37
|
+
if (typeof envFiles.blockMessage === "string") {
|
|
38
|
+
rule.blockMessage = envFiles.blockMessage;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (Array.isArray(envFiles.protectedDirectories)) {
|
|
42
|
+
const dirs = envFiles.protectedDirectories as Array<
|
|
43
|
+
Record<string, unknown>
|
|
44
|
+
>;
|
|
45
|
+
const patterns = Array.isArray(rule.patterns)
|
|
46
|
+
? ([...rule.patterns] as Array<Record<string, unknown>>)
|
|
47
|
+
: [];
|
|
48
|
+
|
|
49
|
+
for (const dir of dirs) {
|
|
50
|
+
const dirPattern = dir.pattern;
|
|
51
|
+
if (typeof dirPattern !== "string" || dirPattern.trim() === "") {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const normalized = dirPattern.endsWith("/**")
|
|
56
|
+
? dirPattern
|
|
57
|
+
: `${dirPattern}/**`;
|
|
58
|
+
patterns.push({ pattern: normalized, regex: dir.regex });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (patterns.length > 0) rule.patterns = patterns;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (Array.isArray(envFiles.protectedTools)) {
|
|
65
|
+
addPendingWarning(
|
|
66
|
+
"[guardrails] envFiles.protectedTools is deprecated and has no direct policies equivalent. " +
|
|
67
|
+
"The migrated secret-files rule uses protection=noAccess.",
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!Array.isArray(rule.patterns) || rule.patterns.length === 0) {
|
|
72
|
+
rule.patterns = [
|
|
73
|
+
{ pattern: ".env" },
|
|
74
|
+
{ pattern: ".env.local" },
|
|
75
|
+
{ pattern: ".env.production" },
|
|
76
|
+
{ pattern: ".env.prod" },
|
|
77
|
+
{ pattern: ".dev.vars" },
|
|
78
|
+
];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
raw.policies = { rules: [rule] };
|
|
82
|
+
delete raw.envFiles;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
raw.version = CURRENT_VERSION;
|
|
86
|
+
return migrated as GuardrailsConfig;
|
|
87
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { addPendingWarning } from "../../warnings";
|
|
2
|
+
import type { GuardrailsConfig } from "../types";
|
|
3
|
+
import { CURRENT_VERSION } from "./version";
|
|
4
|
+
|
|
5
|
+
export function shouldRun(config: GuardrailsConfig): boolean {
|
|
6
|
+
const raw = config as Record<string, unknown>;
|
|
7
|
+
const pathAccess = raw.pathAccess as Record<string, unknown> | undefined;
|
|
8
|
+
if (!Array.isArray(pathAccess?.allowedPaths)) return false;
|
|
9
|
+
return pathAccess.allowedPaths.some((item) => typeof item !== "string");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function run(config: GuardrailsConfig): GuardrailsConfig {
|
|
13
|
+
const migrated = structuredClone(config) as Record<string, unknown>;
|
|
14
|
+
const pathAccess = migrated.pathAccess as Record<string, unknown> | undefined;
|
|
15
|
+
if (!pathAccess) return migrated as GuardrailsConfig;
|
|
16
|
+
|
|
17
|
+
pathAccess.allowedPaths = normalizeAllowedPaths(pathAccess.allowedPaths);
|
|
18
|
+
migrated.version = CURRENT_VERSION;
|
|
19
|
+
addPendingWarning(
|
|
20
|
+
"[guardrails] pathAccess.allowedPaths was migrated from pattern objects to path strings.",
|
|
21
|
+
);
|
|
22
|
+
return migrated as GuardrailsConfig;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function normalizeAllowedPaths(items: unknown): string[] {
|
|
26
|
+
if (!Array.isArray(items)) return [];
|
|
27
|
+
|
|
28
|
+
const paths = new Set<string>();
|
|
29
|
+
for (const item of items) {
|
|
30
|
+
let path: string | null = null;
|
|
31
|
+
if (typeof item === "string") {
|
|
32
|
+
path = item;
|
|
33
|
+
} else if (typeof item === "object" && item !== null) {
|
|
34
|
+
const pattern = (item as Record<string, unknown>).pattern;
|
|
35
|
+
if (typeof pattern === "string") path = pattern;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const normalized = path?.trim();
|
|
39
|
+
if (normalized) paths.add(normalized);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return [...paths];
|
|
43
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { addPendingWarning } from "../../warnings";
|
|
2
|
+
import type { GuardrailsConfig } from "../types";
|
|
3
|
+
import { CURRENT_VERSION } from "./version";
|
|
4
|
+
|
|
5
|
+
export function shouldRun(config: GuardrailsConfig): boolean {
|
|
6
|
+
return config.applyBuiltinDefaults === undefined;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function run(config: GuardrailsConfig): GuardrailsConfig {
|
|
10
|
+
const migrated = structuredClone(config);
|
|
11
|
+
migrated.applyBuiltinDefaults = true;
|
|
12
|
+
migrated.version = CURRENT_VERSION;
|
|
13
|
+
|
|
14
|
+
addPendingWarning(
|
|
15
|
+
"Guardrails config was migrated. `applyBuiltinDefaults` was set to `true` to preserve current behavior.",
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
return migrated;
|
|
19
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { addPendingWarning } from "../../warnings";
|
|
2
|
+
import type { GuardrailsConfig } from "../types";
|
|
3
|
+
import { CURRENT_VERSION } from "./version";
|
|
4
|
+
|
|
5
|
+
export function shouldRun(config: GuardrailsConfig): boolean {
|
|
6
|
+
return (
|
|
7
|
+
config.onboarding?.completed === undefined &&
|
|
8
|
+
config.applyBuiltinDefaults !== undefined
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function run(config: GuardrailsConfig): GuardrailsConfig {
|
|
13
|
+
const migrated = structuredClone(config);
|
|
14
|
+
addPendingWarning(
|
|
15
|
+
"Guardrails config was migrated. Existing setup marked as onboarding-complete.",
|
|
16
|
+
);
|
|
17
|
+
migrated.onboarding = {
|
|
18
|
+
...(migrated.onboarding ?? {}),
|
|
19
|
+
completed: true,
|
|
20
|
+
completedAt: migrated.onboarding?.completedAt ?? new Date().toISOString(),
|
|
21
|
+
version: migrated.onboarding?.version ?? CURRENT_VERSION,
|
|
22
|
+
};
|
|
23
|
+
migrated.version = CURRENT_VERSION;
|
|
24
|
+
return migrated;
|
|
25
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Migration } from "@aliou/pi-utils-settings";
|
|
2
|
+
import type { GuardrailsConfig } from "../types";
|
|
3
|
+
import * as v0FormatUpgrade from "./001-v0-format-upgrade";
|
|
4
|
+
import * as stripToolchainFields from "./002-strip-toolchain-fields";
|
|
5
|
+
import * as stripCommandExplainerFields from "./003-strip-command-explainer-fields";
|
|
6
|
+
import * as envFilesToPolicies from "./004-env-files-to-policies";
|
|
7
|
+
import * as normalizeAllowedPaths from "./005-normalize-allowed-paths";
|
|
8
|
+
import * as applyBuiltinDefaults from "./006-apply-builtin-defaults";
|
|
9
|
+
import * as markOnboardingDone from "./007-mark-onboarding-done";
|
|
10
|
+
|
|
11
|
+
export { CURRENT_VERSION } from "./version";
|
|
12
|
+
|
|
13
|
+
export const migrations: Migration<GuardrailsConfig>[] = [
|
|
14
|
+
{
|
|
15
|
+
name: "v0-format-upgrade",
|
|
16
|
+
shouldRun: v0FormatUpgrade.shouldRun,
|
|
17
|
+
run: v0FormatUpgrade.run,
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: "strip-toolchain-fields",
|
|
21
|
+
shouldRun: stripToolchainFields.shouldRun,
|
|
22
|
+
run: stripToolchainFields.run,
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: "strip-command-explainer-fields",
|
|
26
|
+
shouldRun: stripCommandExplainerFields.shouldRun,
|
|
27
|
+
run: stripCommandExplainerFields.run,
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: "envFiles-to-policies",
|
|
31
|
+
shouldRun: envFilesToPolicies.shouldRun,
|
|
32
|
+
run: envFilesToPolicies.run,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: "normalize-allowed-paths",
|
|
36
|
+
shouldRun: normalizeAllowedPaths.shouldRun,
|
|
37
|
+
run: normalizeAllowedPaths.run,
|
|
38
|
+
},
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
export const globalConfigMigrations = [
|
|
42
|
+
applyBuiltinDefaults,
|
|
43
|
+
markOnboardingDone,
|
|
44
|
+
] as const;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration schema for the guardrails extension.
|
|
3
|
+
*
|
|
4
|
+
* GuardrailsConfig is the user-facing schema (all fields optional).
|
|
5
|
+
* ResolvedConfig is the internal schema (all fields required, defaults applied).
|
|
6
|
+
*/
|
|
7
|
+
import type { GuardrailsFeatureId } from "../events";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A pattern with explicit matching mode.
|
|
11
|
+
* Default: glob for files, substring for commands.
|
|
12
|
+
* regex: true means full regex matching.
|
|
13
|
+
*/
|
|
14
|
+
export interface PatternConfig {
|
|
15
|
+
pattern: string;
|
|
16
|
+
/** Optional description surfaced to the agent when the pattern triggers (e.g. auto-deny reason). */
|
|
17
|
+
description?: string;
|
|
18
|
+
regex?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Permission gate pattern. When regex is false (default), the pattern
|
|
23
|
+
* is matched as substring against the raw command string.
|
|
24
|
+
* When regex is true, uses full regex against the raw string.
|
|
25
|
+
*/
|
|
26
|
+
export interface DangerousPattern extends PatternConfig {
|
|
27
|
+
description: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Protection level for a policy rule.
|
|
32
|
+
*/
|
|
33
|
+
export type Protection = "none" | "readOnly" | "noAccess";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A named policy rule. Matches files by patterns and enforces a protection level.
|
|
37
|
+
*/
|
|
38
|
+
export interface PolicyRule {
|
|
39
|
+
/** Stable identifier used for deduplication across scopes. */
|
|
40
|
+
id: string;
|
|
41
|
+
/** Optional display name for settings/UI. */
|
|
42
|
+
name?: string;
|
|
43
|
+
/** Human-readable description. */
|
|
44
|
+
description?: string;
|
|
45
|
+
/** File patterns to protect. */
|
|
46
|
+
patterns: PatternConfig[];
|
|
47
|
+
/** Optional exceptions. */
|
|
48
|
+
allowedPatterns?: PatternConfig[];
|
|
49
|
+
/** Protection level. */
|
|
50
|
+
protection: Protection;
|
|
51
|
+
/** Block only when file exists on disk. Default true. */
|
|
52
|
+
onlyIfExists?: boolean;
|
|
53
|
+
/** Message shown when blocked; supports {file} placeholder. */
|
|
54
|
+
blockMessage?: string;
|
|
55
|
+
/** Per-rule toggle. Default true. */
|
|
56
|
+
enabled?: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type PathAccessMode = "allow" | "ask" | "block";
|
|
60
|
+
|
|
61
|
+
export interface PathAccessConfig {
|
|
62
|
+
mode?: PathAccessMode;
|
|
63
|
+
allowedPaths?: string[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface GuardrailsConfig {
|
|
67
|
+
/** JSON Schema URL for editor autocomplete and validation. Added automatically when Guardrails writes the file. */
|
|
68
|
+
$schema?: string;
|
|
69
|
+
/** Internal config schema marker for migration/debugging. Not tied to the package version. */
|
|
70
|
+
version?: string;
|
|
71
|
+
/** Enable or disable all Guardrails checks. */
|
|
72
|
+
enabled?: boolean;
|
|
73
|
+
/** When true, include Guardrails built-in policy rules before user rules are merged. */
|
|
74
|
+
applyBuiltinDefaults?: boolean;
|
|
75
|
+
/** Tracks whether the setup wizard has been completed. Usually managed by Guardrails. */
|
|
76
|
+
onboarding?: {
|
|
77
|
+
/** Whether onboarding is complete. */
|
|
78
|
+
completed?: boolean;
|
|
79
|
+
/** ISO timestamp for when onboarding completed. */
|
|
80
|
+
completedAt?: string;
|
|
81
|
+
/** Guardrails config schema marker used when onboarding completed. */
|
|
82
|
+
version?: string;
|
|
83
|
+
};
|
|
84
|
+
/** Enable or disable individual Guardrails feature extensions. */
|
|
85
|
+
features?: Partial<Record<GuardrailsFeatureId, boolean>> & {
|
|
86
|
+
// Deprecated. Kept only for migration.
|
|
87
|
+
protectEnvFiles?: boolean;
|
|
88
|
+
};
|
|
89
|
+
/** File protection policies. */
|
|
90
|
+
policies?: {
|
|
91
|
+
/** Named policy rules. Rules with the same id override earlier rules across scopes. */
|
|
92
|
+
rules?: PolicyRule[];
|
|
93
|
+
};
|
|
94
|
+
/** Outside-workspace path access settings. */
|
|
95
|
+
pathAccess?: PathAccessConfig;
|
|
96
|
+
// Deprecated. Kept only for migration.
|
|
97
|
+
envFiles?: {
|
|
98
|
+
protectedPatterns?: PatternConfig[];
|
|
99
|
+
allowedPatterns?: PatternConfig[];
|
|
100
|
+
protectedDirectories?: PatternConfig[];
|
|
101
|
+
protectedTools?: string[];
|
|
102
|
+
onlyBlockIfExists?: boolean;
|
|
103
|
+
blockMessage?: string;
|
|
104
|
+
};
|
|
105
|
+
/** Dangerous bash command detection and confirmation settings. */
|
|
106
|
+
permissionGate?: {
|
|
107
|
+
/** Additional dangerous command patterns. */
|
|
108
|
+
patterns?: DangerousPattern[];
|
|
109
|
+
/** If set, replaces the default dangerous command patterns entirely. */
|
|
110
|
+
customPatterns?: DangerousPattern[];
|
|
111
|
+
/** When true, prompt before running dangerous commands. When false, only warn. */
|
|
112
|
+
requireConfirmation?: boolean;
|
|
113
|
+
/** Command patterns that bypass dangerous command prompts. */
|
|
114
|
+
allowedPatterns?: PatternConfig[];
|
|
115
|
+
/** Command patterns that are always blocked without prompting. */
|
|
116
|
+
autoDenyPatterns?: PatternConfig[];
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface ResolvedConfig {
|
|
121
|
+
version: string;
|
|
122
|
+
enabled: boolean;
|
|
123
|
+
applyBuiltinDefaults: boolean;
|
|
124
|
+
features: Record<GuardrailsFeatureId, boolean>;
|
|
125
|
+
policies: {
|
|
126
|
+
rules: PolicyRule[];
|
|
127
|
+
};
|
|
128
|
+
pathAccess: {
|
|
129
|
+
mode: PathAccessMode;
|
|
130
|
+
allowedPaths: string[];
|
|
131
|
+
};
|
|
132
|
+
permissionGate: {
|
|
133
|
+
patterns: DangerousPattern[];
|
|
134
|
+
/** When true, use hardcoded structural matchers for built-in patterns.
|
|
135
|
+
* Set to false when customPatterns replaces the defaults. */
|
|
136
|
+
useBuiltinMatchers: boolean;
|
|
137
|
+
requireConfirmation: boolean;
|
|
138
|
+
allowedPatterns: PatternConfig[];
|
|
139
|
+
autoDenyPatterns: PatternConfig[];
|
|
140
|
+
};
|
|
141
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { Action, Safety } from "../core/types";
|
|
3
|
+
|
|
4
|
+
export const GUARDRAILS_ACTION_BLOCKED_EVENT = "guardrails:action:blocked";
|
|
5
|
+
export const GUARDRAILS_RISK_DETECTED_EVENT = "guardrails:risk:detected";
|
|
6
|
+
export const GUARDRAILS_FEATURE_REQUEST_EVENT = "guardrails:feature:request";
|
|
7
|
+
export const GUARDRAILS_FEATURE_REGISTER_EVENT = "guardrails:feature:register";
|
|
8
|
+
|
|
9
|
+
export type GuardrailsFeatureId = "policies" | "permissionGate" | "pathAccess";
|
|
10
|
+
|
|
11
|
+
export interface GuardrailsEventBase {
|
|
12
|
+
source: "guardrails";
|
|
13
|
+
feature: GuardrailsFeatureId;
|
|
14
|
+
timestamp: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface GuardrailsFeatureRequestPayload {
|
|
18
|
+
source: "guardrails";
|
|
19
|
+
timestamp: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface GuardrailsFeatureRegisterPayload {
|
|
23
|
+
source: "guardrails";
|
|
24
|
+
timestamp: string;
|
|
25
|
+
feature: {
|
|
26
|
+
id: GuardrailsFeatureId;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type GuardrailsBlockSource =
|
|
31
|
+
| "policy"
|
|
32
|
+
| "permission"
|
|
33
|
+
| "user"
|
|
34
|
+
| "nonInteractive";
|
|
35
|
+
|
|
36
|
+
export type GuardrailsActionBlockedPayload<TMeta = unknown> =
|
|
37
|
+
GuardrailsEventBase & {
|
|
38
|
+
action: Action;
|
|
39
|
+
reason: string;
|
|
40
|
+
block: {
|
|
41
|
+
source: GuardrailsBlockSource;
|
|
42
|
+
metadata?: TMeta;
|
|
43
|
+
};
|
|
44
|
+
context?: {
|
|
45
|
+
toolName?: string;
|
|
46
|
+
input?: Record<string, unknown>;
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type GuardrailsRiskDetectedPayload<TMeta = unknown> =
|
|
51
|
+
GuardrailsEventBase & {
|
|
52
|
+
risk: Safety<TMeta> & { kind: "dangerous" };
|
|
53
|
+
context?: {
|
|
54
|
+
toolName?: string;
|
|
55
|
+
input?: Record<string, unknown>;
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
function timestamp(): string {
|
|
60
|
+
return new Date().toISOString();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function createFeatureRequestPayload(): GuardrailsFeatureRequestPayload {
|
|
64
|
+
return {
|
|
65
|
+
source: "guardrails",
|
|
66
|
+
timestamp: timestamp(),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function createFeatureRegisterPayload(
|
|
71
|
+
feature: GuardrailsFeatureId,
|
|
72
|
+
): GuardrailsFeatureRegisterPayload {
|
|
73
|
+
return {
|
|
74
|
+
source: "guardrails",
|
|
75
|
+
timestamp: timestamp(),
|
|
76
|
+
feature: { id: feature },
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function emitActionBlocked<TMeta = unknown>(
|
|
81
|
+
pi: ExtensionAPI,
|
|
82
|
+
event: Omit<GuardrailsActionBlockedPayload<TMeta>, "source" | "timestamp">,
|
|
83
|
+
): void {
|
|
84
|
+
pi.events.emit(GUARDRAILS_ACTION_BLOCKED_EVENT, {
|
|
85
|
+
source: "guardrails",
|
|
86
|
+
timestamp: timestamp(),
|
|
87
|
+
...event,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function emitRiskDetected<TMeta = unknown>(
|
|
92
|
+
pi: ExtensionAPI,
|
|
93
|
+
event: Omit<GuardrailsRiskDetectedPayload<TMeta>, "source" | "timestamp">,
|
|
94
|
+
): void {
|
|
95
|
+
pi.events.emit(GUARDRAILS_RISK_DETECTED_EVENT, {
|
|
96
|
+
source: "guardrails",
|
|
97
|
+
timestamp: timestamp(),
|
|
98
|
+
...event,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
compileCommandPattern,
|
|
4
|
+
compileFilePattern,
|
|
5
|
+
normalizeFilePath,
|
|
6
|
+
} from "./matching";
|
|
7
|
+
import { drainPendingWarnings } from "./warnings";
|
|
8
|
+
|
|
9
|
+
describe("normalizeFilePath", () => {
|
|
10
|
+
it.each([
|
|
11
|
+
["./src//file.ts", "src/file.ts"],
|
|
12
|
+
["src\\file.ts", "src/file.ts"],
|
|
13
|
+
["./foo\\bar//baz", "foo/bar/baz"],
|
|
14
|
+
])("normalizes %s", (input, expected) => {
|
|
15
|
+
expect(normalizeFilePath(input)).toBe(expected);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe("compileFilePattern", () => {
|
|
20
|
+
it("matches basename when the pattern has no slash", () => {
|
|
21
|
+
const pattern = compileFilePattern({ pattern: ".env" });
|
|
22
|
+
|
|
23
|
+
expect(pattern.test(".env")).toBe(true);
|
|
24
|
+
expect(pattern.test("config/.env")).toBe(true);
|
|
25
|
+
expect(pattern.test("config/.env.local")).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("matches full normalized paths when the pattern has a slash", () => {
|
|
29
|
+
const pattern = compileFilePattern({ pattern: "config/*.env" });
|
|
30
|
+
|
|
31
|
+
expect(pattern.test("config/app.env")).toBe(true);
|
|
32
|
+
expect(pattern.test("./config//app.env")).toBe(true);
|
|
33
|
+
expect(pattern.test("nested/config/app.env")).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("uses case-insensitive regex matching for file patterns", () => {
|
|
37
|
+
const pattern = compileFilePattern({
|
|
38
|
+
pattern: "SECRET\\.TXT$",
|
|
39
|
+
regex: true,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
expect(pattern.test("docs/secret.txt")).toBe(true);
|
|
43
|
+
expect(pattern.test("docs/public.txt")).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("records a warning and returns a non-matching pattern for invalid regex", () => {
|
|
47
|
+
drainPendingWarnings();
|
|
48
|
+
|
|
49
|
+
const pattern = compileFilePattern({ pattern: "[", regex: true });
|
|
50
|
+
|
|
51
|
+
expect(pattern.test("anything")).toBe(false);
|
|
52
|
+
expect(drainPendingWarnings()).toEqual([
|
|
53
|
+
"Invalid regex in guardrails config: [",
|
|
54
|
+
]);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("compileCommandPattern", () => {
|
|
59
|
+
it("uses substring matching by default", () => {
|
|
60
|
+
const pattern = compileCommandPattern({ pattern: "deploy production" });
|
|
61
|
+
|
|
62
|
+
expect(pattern.test("please deploy production now")).toBe(true);
|
|
63
|
+
expect(pattern.test("deploy staging")).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("uses regex matching when requested", () => {
|
|
67
|
+
const pattern = compileCommandPattern({
|
|
68
|
+
pattern: "terraform\\s+apply",
|
|
69
|
+
regex: true,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
expect(pattern.test("terraform apply -auto-approve")).toBe(true);
|
|
73
|
+
expect(pattern.test("terraform plan")).toBe(false);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("records a warning and returns a non-matching pattern for invalid regex", () => {
|
|
77
|
+
drainPendingWarnings();
|
|
78
|
+
|
|
79
|
+
const pattern = compileCommandPattern({ pattern: "[", regex: true });
|
|
80
|
+
|
|
81
|
+
expect(pattern.test("anything")).toBe(false);
|
|
82
|
+
expect(drainPendingWarnings()).toEqual([
|
|
83
|
+
"Invalid regex in guardrails config: [",
|
|
84
|
+
]);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { matchesGlob } from "node:path";
|
|
12
|
-
import type { PatternConfig } from "
|
|
13
|
-
import {
|
|
12
|
+
import type { PatternConfig } from "./config";
|
|
13
|
+
import { addPendingWarning } from "./warnings";
|
|
14
14
|
|
|
15
15
|
export interface CompiledPattern {
|
|
16
16
|
test: (input: string) => boolean;
|
|
@@ -47,7 +47,7 @@ export function compileFilePattern(config: PatternConfig): CompiledPattern {
|
|
|
47
47
|
source: config,
|
|
48
48
|
};
|
|
49
49
|
} catch {
|
|
50
|
-
|
|
50
|
+
addPendingWarning(
|
|
51
51
|
`Invalid regex in guardrails config: ${config.pattern}`,
|
|
52
52
|
);
|
|
53
53
|
return { test: () => false, source: config };
|
|
@@ -80,7 +80,7 @@ export function compileCommandPattern(config: PatternConfig): CompiledPattern {
|
|
|
80
80
|
const re = new RegExp(config.pattern);
|
|
81
81
|
return { test: (input) => re.test(input), source: config };
|
|
82
82
|
} catch {
|
|
83
|
-
|
|
83
|
+
addPendingWarning(
|
|
84
84
|
`Invalid regex in guardrails config: ${config.pattern}`,
|
|
85
85
|
);
|
|
86
86
|
return { test: () => false, source: config };
|
|
@@ -84,8 +84,8 @@ describe("extractBashPathCandidates", () => {
|
|
|
84
84
|
|
|
85
85
|
it("detects Windows-style paths", async () => {
|
|
86
86
|
const result = await extractBashPathCandidates("type C:\\foo\\bar", CWD);
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
|
|
88
|
+
expect(result).toHaveLength(1);
|
|
89
89
|
expect(result[0]).toContain("C:\\foo\\bar");
|
|
90
90
|
});
|
|
91
91
|
});
|
|
@@ -102,6 +102,15 @@ describe("extractBashPathCandidates", () => {
|
|
|
102
102
|
await extractBashPathCandidates("echo foo > /tmp/out", CWD),
|
|
103
103
|
).toEqual(["/tmp/out"]);
|
|
104
104
|
});
|
|
105
|
+
|
|
106
|
+
it("extracts paths from multiple commands and redirects", async () => {
|
|
107
|
+
expect(
|
|
108
|
+
await extractBashPathCandidates(
|
|
109
|
+
"cat ./input && grep needle /tmp/log > ./out",
|
|
110
|
+
CWD,
|
|
111
|
+
),
|
|
112
|
+
).toEqual(["/work/project/input", "/tmp/log", "/work/project/out"]);
|
|
113
|
+
});
|
|
105
114
|
});
|
|
106
115
|
|
|
107
116
|
describe("when command has no path-like tokens", () => {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import { parse } from "@aliou/sh";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
3
|
+
import { expandHomePath, maybePathLike } from "../../core/paths/path";
|
|
4
|
+
import { walkCommands, wordToString } from "../../core/shell/ast";
|
|
5
|
+
import { classifyCommandArgs } from "../../core/shell/command-args";
|
|
6
|
+
import { expandGlob, hasGlobChars } from "../glob";
|
|
7
7
|
|
|
8
8
|
async function expandCandidate(
|
|
9
9
|
candidate: string,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { extractBashPathCandidates } from "./bash-paths";
|