@aliou/pi-guardrails 0.9.4 → 0.10.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 +5 -0
- package/docs/defaults.md +115 -0
- package/docs/examples.md +170 -0
- package/package.json +10 -9
- package/src/commands/onboarding-command.ts +59 -0
- package/src/commands/onboarding.ts +274 -0
- package/src/commands/settings-command.ts +129 -3
- package/src/config.ts +58 -2
- package/src/hooks/permission-gate.ts +248 -105
- package/src/hooks/policies.ts +20 -4
- package/src/index.ts +62 -3
- package/src/utils/migration.ts +55 -1
- package/src/utils/path.ts +18 -0
package/README.md
CHANGED
|
@@ -18,6 +18,11 @@ Or from git:
|
|
|
18
18
|
pi install git:github.com/aliou/pi-guardrails
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
+
## Documentation
|
|
22
|
+
|
|
23
|
+
- [Default configuration](docs/defaults.md) — built-in policy rules and permission gate patterns
|
|
24
|
+
- [Example presets](docs/examples.md) — pre-configured presets available in settings
|
|
25
|
+
|
|
21
26
|
## What it does
|
|
22
27
|
|
|
23
28
|
- **policies**: named file-protection rules with per-rule protection levels.
|
package/docs/defaults.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Default Configuration
|
|
2
|
+
|
|
3
|
+
These are the built-in defaults that ship with guardrails. Rules marked as disabled are included but inactive by default — enable them in your config or via `/guardrails:settings`.
|
|
4
|
+
|
|
5
|
+
Source: [`src/config.ts`](../src/config.ts)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
Home-directory defaults use `~` in patterns. During policy evaluation, guardrails expands `~` to the current user's home directory before checking whether a file exists or should be blocked.
|
|
9
|
+
## Default Policy Rules
|
|
10
|
+
|
|
11
|
+
### `secret-files` — Files containing secrets
|
|
12
|
+
|
|
13
|
+
Blocks access to dotenv files and similar secret-bearing files.
|
|
14
|
+
|
|
15
|
+
| Protection | Only if exists |
|
|
16
|
+
|------------|---------------|
|
|
17
|
+
| `noAccess` | yes |
|
|
18
|
+
|
|
19
|
+
**Patterns:**
|
|
20
|
+
|
|
21
|
+
| Pattern | Type |
|
|
22
|
+
|--------------------|------|
|
|
23
|
+
| `.env` | glob |
|
|
24
|
+
| `.env.local` | glob |
|
|
25
|
+
| `.env.production` | glob |
|
|
26
|
+
| `.env.prod` | glob |
|
|
27
|
+
| `.dev.vars` | glob |
|
|
28
|
+
|
|
29
|
+
**Allowed exceptions:**
|
|
30
|
+
|
|
31
|
+
| Pattern | Type |
|
|
32
|
+
|--------------------|------|
|
|
33
|
+
| `*.example.env` | glob |
|
|
34
|
+
| `*.sample.env` | glob |
|
|
35
|
+
| `*.test.env` | glob |
|
|
36
|
+
| `.env.example` | glob |
|
|
37
|
+
| `.env.sample` | glob |
|
|
38
|
+
| `.env.test` | glob |
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
### `home-ssh` — SSH directory and keys
|
|
43
|
+
|
|
44
|
+
Blocks access to SSH configuration, private keys, and related files. Disabled by default.
|
|
45
|
+
|
|
46
|
+
| Protection | Only if exists | Enabled by default |
|
|
47
|
+
|------------|---------------|-------------------|
|
|
48
|
+
| `noAccess` | yes | no |
|
|
49
|
+
|
|
50
|
+
**Patterns:**
|
|
51
|
+
|
|
52
|
+
| Pattern | Type |
|
|
53
|
+
|-----------------------|------|
|
|
54
|
+
| `~/.ssh/**` | glob |
|
|
55
|
+
| `~/.ssh/*_rsa` | glob |
|
|
56
|
+
| `~/.ssh/*_ed25519` | glob |
|
|
57
|
+
| `~/.ssh/*.pem` | glob |
|
|
58
|
+
|
|
59
|
+
**Allowed exceptions:**
|
|
60
|
+
|
|
61
|
+
| Pattern | Type |
|
|
62
|
+
|----------|------|
|
|
63
|
+
| `~/.ssh/*.pub` | glob |
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
### `home-config` — Sensitive user configuration directories
|
|
68
|
+
|
|
69
|
+
Blocks access to a small set of known sensitive config directories that commonly store credentials, tokens, or encrypted material. Disabled by default — enable it if these tools are installed and you want to protect them.
|
|
70
|
+
|
|
71
|
+
| Protection | Only if exists | Enabled by default |
|
|
72
|
+
|------------|---------------|-------------------|
|
|
73
|
+
| `noAccess` | yes | no |
|
|
74
|
+
|
|
75
|
+
**Patterns:**
|
|
76
|
+
|
|
77
|
+
| Pattern | Type |
|
|
78
|
+
|-----------------------|------|
|
|
79
|
+
| `~/.config/gh/**` | glob |
|
|
80
|
+
| `~/.config/gcloud/**` | glob |
|
|
81
|
+
| `~/.config/op/**` | glob |
|
|
82
|
+
| `~/.config/sops/**` | glob |
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
### `home-gpg` — GPG keys and configuration
|
|
87
|
+
|
|
88
|
+
Blocks access to GPG/GnuPG private keys, keyrings, and configuration. Disabled by default.
|
|
89
|
+
|
|
90
|
+
| Protection | Only if exists | Enabled by default |
|
|
91
|
+
|------------|---------------|-------------------|
|
|
92
|
+
| `noAccess` | yes | no |
|
|
93
|
+
|
|
94
|
+
**Patterns:**
|
|
95
|
+
|
|
96
|
+
| Pattern | Type |
|
|
97
|
+
|--------------------|------|
|
|
98
|
+
| `~/.gnupg/**` | glob |
|
|
99
|
+
| `~/*.gpg` | glob |
|
|
100
|
+
| `~/.gpg-agent.conf` | glob |
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Default Permission Gate Patterns
|
|
105
|
+
|
|
106
|
+
These commands are detected using AST-based structural matching for accuracy.
|
|
107
|
+
|
|
108
|
+
| Pattern | Description |
|
|
109
|
+
|-----------------|--------------------------------|
|
|
110
|
+
| `rm -rf` | Recursive force delete |
|
|
111
|
+
| `sudo` | Superuser command |
|
|
112
|
+
| `dd if=` | Disk write operation |
|
|
113
|
+
| `mkfs.` | Filesystem format |
|
|
114
|
+
| `chmod -R 777` | Insecure recursive permissions |
|
|
115
|
+
| `chown -R` | Recursive ownership change |
|
package/docs/examples.md
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# Example Presets
|
|
2
|
+
|
|
3
|
+
Pre-configured presets available in the `/guardrails:settings` Examples tab. These can be applied to any config scope (global, local, or memory).
|
|
4
|
+
|
|
5
|
+
Source: [`src/commands/settings-command.ts`](../src/commands/settings-command.ts)
|
|
6
|
+
|
|
7
|
+
## File Policy Presets
|
|
8
|
+
|
|
9
|
+
### Secrets (.env)
|
|
10
|
+
|
|
11
|
+
Block dotenv-like files using glob patterns.
|
|
12
|
+
|
|
13
|
+
| Field | Value |
|
|
14
|
+
|------------|------------------------------------|
|
|
15
|
+
| ID | `example-secret-env-files` |
|
|
16
|
+
| Protection | `noAccess` |
|
|
17
|
+
| Patterns | `.env`, `.env.*` |
|
|
18
|
+
| Exceptions | `.env.example`, `*.sample.env` |
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
### Logs (*.log)
|
|
23
|
+
|
|
24
|
+
Mark log files as read-only to prevent accidental modification.
|
|
25
|
+
|
|
26
|
+
| Field | Value |
|
|
27
|
+
|------------|---------------------------|
|
|
28
|
+
| ID | `example-log-files` |
|
|
29
|
+
| Protection | `readOnly` |
|
|
30
|
+
| Patterns | `*.log`, `*.out` |
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
### Regex env
|
|
35
|
+
|
|
36
|
+
Regex-based matching for `.env` and `.env.*` files. Demonstrates regex mode.
|
|
37
|
+
|
|
38
|
+
| Field | Value |
|
|
39
|
+
|------------|------------------------------------------|
|
|
40
|
+
| ID | `example-regex-env` |
|
|
41
|
+
| Protection | `noAccess` |
|
|
42
|
+
| Patterns | `^\.env(\..+)?$` (regex) |
|
|
43
|
+
| Exceptions | `^\.env\.example$` (regex) |
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
### SSH keys
|
|
48
|
+
|
|
49
|
+
Block access to SSH private key files.
|
|
50
|
+
|
|
51
|
+
| Field | Value |
|
|
52
|
+
|------------|--------------------------------------|
|
|
53
|
+
| ID | `example-ssh-keys` |
|
|
54
|
+
| Protection | `noAccess` |
|
|
55
|
+
| Patterns | `*.pem`, `*_rsa`, `*_ed25519` |
|
|
56
|
+
| Exceptions | `*.pub` |
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
### AWS credentials
|
|
61
|
+
|
|
62
|
+
Block AWS CLI credentials and config files.
|
|
63
|
+
|
|
64
|
+
| Field | Value |
|
|
65
|
+
|------------|----------------------------------------|
|
|
66
|
+
| ID | `example-aws-credentials` |
|
|
67
|
+
| Protection | `noAccess` |
|
|
68
|
+
| Patterns | `.aws/credentials`, `.aws/config` |
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
### Database files
|
|
73
|
+
|
|
74
|
+
Mark SQLite and database files as read-only.
|
|
75
|
+
|
|
76
|
+
| Field | Value |
|
|
77
|
+
|------------|----------------------------------------|
|
|
78
|
+
| ID | `example-database-files` |
|
|
79
|
+
| Protection | `readOnly` |
|
|
80
|
+
| Patterns | `*.db`, `*.sqlite`, `*.sqlite3` |
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
### Kubernetes secrets
|
|
85
|
+
|
|
86
|
+
Block kubeconfig and Kubernetes secret files.
|
|
87
|
+
|
|
88
|
+
| Field | Value |
|
|
89
|
+
|------------|----------------------------------------|
|
|
90
|
+
| ID | `example-k8s-secrets` |
|
|
91
|
+
| Protection | `noAccess` |
|
|
92
|
+
| Patterns | `.kube/config`, `*kubeconfig*` |
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
### Certificates
|
|
97
|
+
|
|
98
|
+
Block SSL/TLS certificate and key files.
|
|
99
|
+
|
|
100
|
+
| Field | Value |
|
|
101
|
+
|------------|----------------------------------------|
|
|
102
|
+
| ID | `example-certificates` |
|
|
103
|
+
| Protection | `noAccess` |
|
|
104
|
+
| Patterns | `*.crt`, `*.key`, `*.p12` |
|
|
105
|
+
| Exceptions | `*.csr` |
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Dangerous Command Presets
|
|
110
|
+
|
|
111
|
+
### General
|
|
112
|
+
|
|
113
|
+
| Label | Pattern | Description |
|
|
114
|
+
|--------------------|----------------------|----------------------------------------|
|
|
115
|
+
| Homebrew | `brew` | Homebrew package manager |
|
|
116
|
+
| git push --force | `git push --force` | Git force push |
|
|
117
|
+
| npm publish | `npm publish` | NPM package publishing |
|
|
118
|
+
| yarn publish | `yarn publish` | Yarn package publishing |
|
|
119
|
+
| pnpm publish | `pnpm publish` | PNPM package publishing |
|
|
120
|
+
| drop database | `DROP DATABASE` | SQL database drop |
|
|
121
|
+
| drop table | `DROP TABLE` | SQL table drop |
|
|
122
|
+
|
|
123
|
+
### dbt
|
|
124
|
+
|
|
125
|
+
| Label | Pattern | Description |
|
|
126
|
+
|----------|------------|------------------------|
|
|
127
|
+
| dbt run | `dbt run` | dbt model execution |
|
|
128
|
+
| dbt seed | `dbt seed` | dbt seed data loading |
|
|
129
|
+
|
|
130
|
+
### AWS
|
|
131
|
+
|
|
132
|
+
| Label | Pattern | Description |
|
|
133
|
+
|----------------------|--------------------------------|------------------------------|
|
|
134
|
+
| aws s3 rm | `aws s3 rm` | AWS S3 object deletion |
|
|
135
|
+
| aws iam | `aws iam` | AWS IAM permission changes |
|
|
136
|
+
| aws ec2 terminate | `aws ec2 terminate-instances` | AWS EC2 instance termination |
|
|
137
|
+
|
|
138
|
+
### Kubernetes
|
|
139
|
+
|
|
140
|
+
| Label | Pattern | Description |
|
|
141
|
+
|----------------|------------------|--------------------------------|
|
|
142
|
+
| kubectl delete | `kubectl delete` | Kubernetes resource deletion |
|
|
143
|
+
| kubectl apply | `kubectl apply` | Kubernetes resource application|
|
|
144
|
+
| kubectl scale | `kubectl scale` | Kubernetes scaling operation |
|
|
145
|
+
|
|
146
|
+
### Docker
|
|
147
|
+
|
|
148
|
+
| Label | Pattern | Description |
|
|
149
|
+
|----------------------|------------------------|------------------------------------------|
|
|
150
|
+
| Docker secrets | `docker inspect` | Docker inspect (may expose env vars) |
|
|
151
|
+
| docker rm | `docker rm` | Docker container removal |
|
|
152
|
+
| docker rmi | `docker rmi` | Docker image removal |
|
|
153
|
+
| docker system prune | `docker system prune` | Docker system cleanup |
|
|
154
|
+
| docker compose down | `docker compose down` | Docker Compose service teardown |
|
|
155
|
+
|
|
156
|
+
### Terraform
|
|
157
|
+
|
|
158
|
+
| Label | Pattern | Description |
|
|
159
|
+
|--------------------|----------------------|------------------------------------|
|
|
160
|
+
| Terraform apply | `terraform apply` | Terraform infrastructure changes |
|
|
161
|
+
| Terraform destroy | `terraform destroy` | Terraform infrastructure destruction|
|
|
162
|
+
| terraform import | `terraform import` | Terraform resource import |
|
|
163
|
+
|
|
164
|
+
### Google Cloud
|
|
165
|
+
|
|
166
|
+
| Label | Pattern | Description |
|
|
167
|
+
|------------------------|------------------------------------|----------------------------------|
|
|
168
|
+
| gcloud compute delete | `gcloud compute instances delete` | GCP compute instance deletion |
|
|
169
|
+
| gcloud iam | `gcloud iam` | GCP IAM permission changes |
|
|
170
|
+
| gcloud sql delete | `gcloud sql instances delete` | GCP Cloud SQL instance deletion |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aliou/pi-guardrails",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -26,25 +26,26 @@
|
|
|
26
26
|
},
|
|
27
27
|
"files": [
|
|
28
28
|
"src",
|
|
29
|
+
"docs",
|
|
29
30
|
"README.md"
|
|
30
31
|
],
|
|
31
32
|
"dependencies": {
|
|
32
|
-
"@aliou/pi-utils-settings": "^0.
|
|
33
|
+
"@aliou/pi-utils-settings": "^0.11.2",
|
|
33
34
|
"@aliou/sh": "^0.1.0"
|
|
34
35
|
},
|
|
35
36
|
"peerDependencies": {
|
|
36
|
-
"@mariozechner/pi-agent-core": "
|
|
37
|
-
"@mariozechner/pi-ai": "
|
|
38
|
-
"@mariozechner/pi-coding-agent": "
|
|
39
|
-
"@mariozechner/pi-tui": "
|
|
37
|
+
"@mariozechner/pi-agent-core": "0.61.0",
|
|
38
|
+
"@mariozechner/pi-ai": "0.61.0",
|
|
39
|
+
"@mariozechner/pi-coding-agent": "0.61.0",
|
|
40
|
+
"@mariozechner/pi-tui": "0.61.0"
|
|
40
41
|
},
|
|
41
42
|
"devDependencies": {
|
|
42
43
|
"@aliou/biome-plugins": "^0.3.2",
|
|
43
44
|
"@biomejs/biome": "^2.3.13",
|
|
44
45
|
"@changesets/cli": "^2.27.11",
|
|
45
|
-
"@mariozechner/pi-agent-core": "0.
|
|
46
|
-
"@mariozechner/pi-ai": "0.
|
|
47
|
-
"@mariozechner/pi-coding-agent": "0.
|
|
46
|
+
"@mariozechner/pi-agent-core": "0.61.0",
|
|
47
|
+
"@mariozechner/pi-ai": "0.61.0",
|
|
48
|
+
"@mariozechner/pi-coding-agent": "0.61.0",
|
|
48
49
|
"@sinclair/typebox": "^0.34.48",
|
|
49
50
|
"@types/node": "^25.0.10",
|
|
50
51
|
"husky": "^9.1.7",
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { configLoader, type GuardrailsConfig } from "../config";
|
|
3
|
+
import {
|
|
4
|
+
buildOnboardedConfig,
|
|
5
|
+
createOnboardingWizard,
|
|
6
|
+
isOnboardingPending,
|
|
7
|
+
type OnboardingResult,
|
|
8
|
+
} from "./onboarding";
|
|
9
|
+
|
|
10
|
+
function mergeOnboarding(
|
|
11
|
+
base: GuardrailsConfig | null,
|
|
12
|
+
applyBuiltinDefaults: boolean,
|
|
13
|
+
): GuardrailsConfig {
|
|
14
|
+
const next = structuredClone(base ?? {});
|
|
15
|
+
const onboarded = buildOnboardedConfig(applyBuiltinDefaults);
|
|
16
|
+
next.applyBuiltinDefaults = onboarded.applyBuiltinDefaults;
|
|
17
|
+
next.version = onboarded.version;
|
|
18
|
+
next.onboarding = onboarded.onboarding;
|
|
19
|
+
return next;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function registerGuardrailsOnboardingCommand(
|
|
23
|
+
pi: ExtensionAPI,
|
|
24
|
+
onCompleted?: () => void,
|
|
25
|
+
): void {
|
|
26
|
+
pi.registerCommand("guardrails:onboarding", {
|
|
27
|
+
description: "Run guardrails onboarding",
|
|
28
|
+
handler: async (_args, ctx) => {
|
|
29
|
+
if (!ctx.hasUI) return;
|
|
30
|
+
|
|
31
|
+
const globalConfig = configLoader.getRawConfig("global");
|
|
32
|
+
if (!isOnboardingPending(globalConfig)) {
|
|
33
|
+
ctx.ui.notify(
|
|
34
|
+
"[Guardrails] onboarding already completed. Use /guardrails:settings to update behavior.",
|
|
35
|
+
"info",
|
|
36
|
+
);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const result = await ctx.ui.custom<OnboardingResult>(
|
|
41
|
+
(_tui, theme, _keybindings, done) =>
|
|
42
|
+
createOnboardingWizard(theme, done),
|
|
43
|
+
{ overlay: true },
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
if (!result.completed || result.applyBuiltinDefaults === null) {
|
|
47
|
+
ctx.ui.notify("[Guardrails] onboarding cancelled.", "warning");
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const merged = mergeOnboarding(globalConfig, result.applyBuiltinDefaults);
|
|
52
|
+
await configLoader.save("global", merged);
|
|
53
|
+
await configLoader.load();
|
|
54
|
+
|
|
55
|
+
onCompleted?.();
|
|
56
|
+
ctx.ui.notify("[Guardrails] onboarding completed.", "info");
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getSettingsTheme,
|
|
3
|
+
type SettingsTheme,
|
|
4
|
+
Wizard,
|
|
5
|
+
} from "@aliou/pi-utils-settings";
|
|
6
|
+
import { getMarkdownTheme, type Theme } from "@mariozechner/pi-coding-agent";
|
|
7
|
+
import type { Component } from "@mariozechner/pi-tui";
|
|
8
|
+
import { Box, Key, Markdown, matchesKey, Text } from "@mariozechner/pi-tui";
|
|
9
|
+
import type { GuardrailsConfig } from "../config";
|
|
10
|
+
import { CURRENT_VERSION } from "../utils/migration";
|
|
11
|
+
|
|
12
|
+
interface OnboardingState {
|
|
13
|
+
applyBuiltinDefaults: boolean | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface OnboardingResult {
|
|
17
|
+
completed: boolean;
|
|
18
|
+
applyBuiltinDefaults: boolean | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
class IntroStep implements Component {
|
|
22
|
+
private readonly introText = new Text("", 2, 0);
|
|
23
|
+
|
|
24
|
+
constructor(private readonly onNext: () => void) {}
|
|
25
|
+
|
|
26
|
+
invalidate() {
|
|
27
|
+
this.introText.invalidate();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
render(width: number): string[] {
|
|
31
|
+
this.introText.setText(
|
|
32
|
+
"Guardrails helps prevent accidental exposure of secrets and risky actions.\n\nIt gives you two protections:\n- Policies: file access rules (`noAccess` or `readOnly`)\n- Permission gate: confirmation before dangerous commands run\n\nYou are choosing the starting defaults now. You can change them later in `/guardrails:settings`.",
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
return [
|
|
36
|
+
" Welcome to Guardrails",
|
|
37
|
+
"",
|
|
38
|
+
...this.introText.render(Math.max(1, width)),
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
handleInput(data: string): void {
|
|
43
|
+
if (matchesKey(data, Key.enter)) {
|
|
44
|
+
this.onNext();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
class DefaultsChoiceStep implements Component {
|
|
50
|
+
private selectedIndex = 0;
|
|
51
|
+
private readonly settingsTheme: SettingsTheme;
|
|
52
|
+
|
|
53
|
+
constructor(
|
|
54
|
+
private readonly theme: Theme,
|
|
55
|
+
private readonly state: OnboardingState,
|
|
56
|
+
private readonly onSelect: () => void,
|
|
57
|
+
) {
|
|
58
|
+
this.settingsTheme = getSettingsTheme(theme);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
invalidate() {}
|
|
62
|
+
|
|
63
|
+
render(width: number): string[] {
|
|
64
|
+
const options = ["Recommended defaults", "Minimal setup"];
|
|
65
|
+
const explanations = [
|
|
66
|
+
[
|
|
67
|
+
"Use built-ins for common safety needs:",
|
|
68
|
+
"",
|
|
69
|
+
"- Protect secret files like `.env`, `.env.local`, `.env.production`, and `.dev.vars`",
|
|
70
|
+
"- Keep safe exceptions like `.env.example` and `*.sample.env`",
|
|
71
|
+
"- Require confirmation before running dangerous commands like `rm -rf`, `sudo`, and `dd if=`",
|
|
72
|
+
].join("\n"),
|
|
73
|
+
[
|
|
74
|
+
"Start with no built-in file policy defaults.",
|
|
75
|
+
"",
|
|
76
|
+
"- Configure your own policies in `/guardrails:settings`",
|
|
77
|
+
"- Browse policy and command examples in `/guardrails:settings`",
|
|
78
|
+
].join("\n"),
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
const lines: string[] = [
|
|
82
|
+
" Pick how much built-in protection to start with.",
|
|
83
|
+
"",
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
for (let i = 0; i < options.length; i++) {
|
|
87
|
+
const option = options[i];
|
|
88
|
+
if (!option) continue;
|
|
89
|
+
const selected = i === this.selectedIndex;
|
|
90
|
+
const prefix = selected ? this.settingsTheme.cursor : " ";
|
|
91
|
+
const label = this.settingsTheme.value(` ${option}`, selected);
|
|
92
|
+
lines.push(`${prefix}${label}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
lines.push("");
|
|
96
|
+
|
|
97
|
+
const explanationBox = new Box(1, 0, (s: string) => s);
|
|
98
|
+
explanationBox.addChild(
|
|
99
|
+
new Markdown(
|
|
100
|
+
explanations[this.selectedIndex] ?? "",
|
|
101
|
+
0,
|
|
102
|
+
0,
|
|
103
|
+
getMarkdownTheme(),
|
|
104
|
+
{
|
|
105
|
+
color: (s: string) => this.theme.fg("text", s),
|
|
106
|
+
},
|
|
107
|
+
),
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
lines.push(...explanationBox.render(Math.max(1, width)));
|
|
111
|
+
|
|
112
|
+
return lines;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
handleInput(data: string): void {
|
|
116
|
+
if (matchesKey(data, Key.up) || data === "k") {
|
|
117
|
+
this.selectedIndex = this.selectedIndex === 0 ? 1 : 0;
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (matchesKey(data, Key.down) || data === "j") {
|
|
121
|
+
this.selectedIndex = this.selectedIndex === 1 ? 0 : 1;
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (matchesKey(data, Key.enter)) {
|
|
126
|
+
this.state.applyBuiltinDefaults = this.selectedIndex === 0;
|
|
127
|
+
this.onSelect();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
class FinishStep implements Component {
|
|
133
|
+
private readonly recapMarkdown = new Markdown("", 2, 0, getMarkdownTheme());
|
|
134
|
+
|
|
135
|
+
constructor(
|
|
136
|
+
private readonly state: OnboardingState,
|
|
137
|
+
private readonly onFinish: () => void,
|
|
138
|
+
) {}
|
|
139
|
+
|
|
140
|
+
invalidate() {
|
|
141
|
+
this.recapMarkdown.invalidate();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
render(width: number): string[] {
|
|
145
|
+
const content =
|
|
146
|
+
this.state.applyBuiltinDefaults === true
|
|
147
|
+
? [
|
|
148
|
+
"You selected **Recommended defaults**.",
|
|
149
|
+
"",
|
|
150
|
+
"Guardrails will start with built-in protection, including:",
|
|
151
|
+
"- secret files like `.env`, `.env.local`, `.env.production`, `.dev.vars`",
|
|
152
|
+
"- safe exceptions like `.env.example` and `*.sample.env`",
|
|
153
|
+
"- confirmation before running dangerous commands like `rm -rf`, `sudo`, `dd if=`",
|
|
154
|
+
].join("\n")
|
|
155
|
+
: [
|
|
156
|
+
"You selected **Minimal setup**.",
|
|
157
|
+
"",
|
|
158
|
+
"No built-in file policy defaults will be applied.",
|
|
159
|
+
"",
|
|
160
|
+
"You can configure policies later with `/guardrails:settings`.",
|
|
161
|
+
].join("\n");
|
|
162
|
+
|
|
163
|
+
this.recapMarkdown.setText(content);
|
|
164
|
+
return [...this.recapMarkdown.render(Math.max(1, width)), ""];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
handleInput(data: string): void {
|
|
168
|
+
if (matchesKey(data, Key.enter)) {
|
|
169
|
+
this.onFinish();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function createOnboardingWizard(
|
|
175
|
+
theme: Theme,
|
|
176
|
+
done: (result: OnboardingResult) => void,
|
|
177
|
+
): Component {
|
|
178
|
+
const state: OnboardingState = {
|
|
179
|
+
applyBuiltinDefaults: null,
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
let markWelcomeComplete: (() => void) | null = null;
|
|
183
|
+
let settled = false;
|
|
184
|
+
|
|
185
|
+
const finalize = (result: OnboardingResult) => {
|
|
186
|
+
if (settled) return;
|
|
187
|
+
settled = true;
|
|
188
|
+
done(result);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const wizard = new Wizard({
|
|
192
|
+
title: "Guardrails onboarding",
|
|
193
|
+
theme,
|
|
194
|
+
steps: [
|
|
195
|
+
{
|
|
196
|
+
label: "Welcome",
|
|
197
|
+
build: (ctx) => {
|
|
198
|
+
markWelcomeComplete = ctx.markComplete;
|
|
199
|
+
return new IntroStep(() => {
|
|
200
|
+
ctx.markComplete();
|
|
201
|
+
ctx.goNext();
|
|
202
|
+
});
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
label: "Defaults",
|
|
207
|
+
build: (ctx) =>
|
|
208
|
+
new DefaultsChoiceStep(theme, state, () => {
|
|
209
|
+
ctx.markComplete();
|
|
210
|
+
ctx.goNext();
|
|
211
|
+
}),
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
label: "Recap",
|
|
215
|
+
build: (ctx) =>
|
|
216
|
+
new FinishStep(state, () => {
|
|
217
|
+
if (state.applyBuiltinDefaults === null) return;
|
|
218
|
+
ctx.markComplete();
|
|
219
|
+
finalize({
|
|
220
|
+
completed: true,
|
|
221
|
+
applyBuiltinDefaults: state.applyBuiltinDefaults,
|
|
222
|
+
});
|
|
223
|
+
}),
|
|
224
|
+
},
|
|
225
|
+
],
|
|
226
|
+
onComplete: () => {
|
|
227
|
+
if (state.applyBuiltinDefaults === null) {
|
|
228
|
+
finalize({ completed: false, applyBuiltinDefaults: null });
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
finalize({
|
|
232
|
+
completed: true,
|
|
233
|
+
applyBuiltinDefaults: state.applyBuiltinDefaults,
|
|
234
|
+
});
|
|
235
|
+
},
|
|
236
|
+
onCancel: () => finalize({ completed: false, applyBuiltinDefaults: null }),
|
|
237
|
+
hintSuffix: "Enter select/continue",
|
|
238
|
+
minContentHeight: 12,
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
render: (width) => wizard.render(width),
|
|
243
|
+
invalidate: () => wizard.invalidate(),
|
|
244
|
+
handleInput: (data: string) => {
|
|
245
|
+
if (
|
|
246
|
+
matchesKey(data, Key.tab) &&
|
|
247
|
+
wizard.getActiveIndex() === 0 &&
|
|
248
|
+
markWelcomeComplete
|
|
249
|
+
) {
|
|
250
|
+
markWelcomeComplete();
|
|
251
|
+
}
|
|
252
|
+
wizard.handleInput(data);
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function buildOnboardedConfig(
|
|
258
|
+
applyBuiltinDefaults: boolean,
|
|
259
|
+
): GuardrailsConfig {
|
|
260
|
+
return {
|
|
261
|
+
version: CURRENT_VERSION,
|
|
262
|
+
applyBuiltinDefaults,
|
|
263
|
+
onboarding: {
|
|
264
|
+
completed: true,
|
|
265
|
+
completedAt: new Date().toISOString(),
|
|
266
|
+
version: CURRENT_VERSION,
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function isOnboardingPending(config: GuardrailsConfig | null): boolean {
|
|
272
|
+
if (!config) return true;
|
|
273
|
+
return config.onboarding?.completed !== true;
|
|
274
|
+
}
|