pi-safety-guards 0.1.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 ADDED
@@ -0,0 +1,114 @@
1
+ # pi-safety-guards
2
+
3
+ Bash safety rules for Pi, with selectable presets, per-rule actions and custom matchers.
4
+
5
+ [中文文档](./README.zh-CN.md)
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pi install npm:pi-safety-guards
11
+ ```
12
+
13
+ Run `/reload` after installation or configuration changes.
14
+
15
+ ## Presets
16
+
17
+ By default, `destructive-operations` asks for confirmation before these operations:
18
+
19
+ | Preset | Rule ID | Match | Action |
20
+ | --- | --- | --- | --- |
21
+ | `destructive-operations` | `filesystem.delete` | Executed `rm` or `rmdir` | confirm |
22
+ | `destructive-operations` | `filesystem.format` | Executed `mkfs` or `mkfs.*` | confirm |
23
+ | `destructive-operations` | `filesystem.ownership` | Executed `chown` | confirm |
24
+ | `destructive-operations` | `shell.fork-bomb` | Supported colon-function fork-bomb syntax | confirm |
25
+ | `workspace-boundary` (opt-in) | `paths.workspace` | Explicit Bash paths outside `.` | block |
26
+
27
+ Matching uses parsed commands, including supported wrappers, literal nested shells, substitutions and redirects. `echo 'rm file'` and `git rm` do not count as executing `rm`.
28
+
29
+ ## Configuration
30
+
31
+ File: `<pi-agent-dir>/extensions/pi-safety-guards/config.json`. The agent directory respects `PI_CODING_AGENT_DIR`.
32
+
33
+ ```json
34
+ {
35
+ "presets": ["destructive-operations"],
36
+ "rules": [
37
+ { "id": "filesystem.delete", "action": "block" },
38
+ { "id": "filesystem.ownership", "enabled": false },
39
+ { "id": "example.command", "action": "confirm", "match": { "commands": ["example-command"] } }
40
+ ]
41
+ }
42
+ ```
43
+
44
+ Replace `example-command` with the command to match. See also [config.example.json](./config.example.json) and [custom-rules.json](./examples/custom-rules.json).
45
+
46
+ - `presets`: omitted selects the default preset; `[]` selects none.
47
+ - `rules`: override selected preset rules by `id` or add new ones. IDs must be unique within this list.
48
+ - Existing rules can override `action`, `match` and `message`, or use `enabled: false`.
49
+ - New enabled rules require `id`, `action` and `match`.
50
+ - `message`: optional non-empty text or an object containing `zh-CN` and `en-US`. If omitted, feedback shows the rule ID and action.
51
+ - Empty presets and rules disable checking and report that no protection is active.
52
+
53
+ Use `/config:safety-guards` to open the normal TUI preset menu, and use `reset` to restore the default preset. Run `/reload` after saving.
54
+
55
+ ### Actions
56
+
57
+ | Action | Behavior |
58
+ | --- | --- |
59
+ | `warn` | Allow and append a warning to the corresponding tool result, including without a UI. |
60
+ | `confirm` | Ask once for the matching rules. Block if prompting is unavailable or the user does not confirm. |
61
+ | `block` | Reject without prompting. |
62
+
63
+ When multiple rules match, priority is `block > confirm > warn`. Feedback includes all matching IDs.
64
+
65
+ ### Matchers
66
+
67
+ Use exactly one matcher per rule:
68
+
69
+ | `match` | Meaning |
70
+ | --- | --- |
71
+ | `{ "commands": ["example-command"] }` | Exact executed command basenames |
72
+ | `{ "detector": "disk-format" }` | `mkfs` or `mkfs.*` |
73
+ | `{ "detector": "fork-bomb" }` | Supported colon-function fork-bomb syntax |
74
+ | `{ "detector": "in-place-edit" }` | `sed` in-place flags |
75
+ | `{ "detector": "home-root" }` | An unquoted standalone `~` argument |
76
+ | `{ "detector": "root-search" }` | A `find` argument equal to `/` |
77
+ | `{ "outsideRoots": [".", "../shared"] }` | Explicit paths outside the configured roots |
78
+ | `{ "module": "./rules/deploy.mjs" }` | A trusted local matcher module |
79
+
80
+ ### Directory rules
81
+
82
+ Relative roots resolve against Pi's current working directory; absolute paths and `~/` are supported. Only listed roots are allowed. The preset uses `.` for the working directory. When `pi-add-dir` is installed, its active directory authorization is also honored; after `session_squash`, the guard restores the authorization from the squashed source branch. Add extra directories and device paths such as `/dev/null` when needed.
83
+
84
+ Custom matchers can use the exported `findOutOfScopeBashPaths(command, cwd, roots)` helper to supply their own roots.
85
+
86
+ ### Custom modules
87
+
88
+ Module paths resolve against the directory containing `config.json`. A JavaScript ES module must default-export a matcher:
89
+
90
+ ```js
91
+ export default ({ commands }) => commands.some(
92
+ ({ name, args }) => name === "example-deploy" && args.includes("--production"),
93
+ );
94
+ ```
95
+
96
+ The matcher receives a frozen `{ command, cwd, commands: [{ name, args }] }` summary and returns a boolean or `Promise<boolean>`. `RuleContext` and `RuleMatcher` types are exported by the package.
97
+
98
+ Only enabled modules are loaded. Loading failures, matcher errors, non-boolean results and asynchronous work exceeding five seconds block the operation. Use `/reload` after editing a module.
99
+
100
+ Modules run with full process permissions. The timeout cannot interrupt synchronous loops or roll back side effects; load only trusted code.
101
+
102
+ ## Limits and errors
103
+
104
+ The extension checks Pi's `bash` tool, not direct user shell commands, other tools or program-internal operations. It does not cover every dangerous command, simulate every working-directory change or resolve arbitrary variable-generated paths. Path checks handle statically identifiable references and symlinks, not OS-level access control.
105
+
106
+ A missing config file uses the default preset. Invalid configuration, malformed Bash and enabled-rule failures block Bash until the relevant error is fixed. Configuration changes require `/reload`.
107
+
108
+ Runtime messages are available in Chinese and English.
109
+
110
+ ## Development
111
+
112
+ ```bash
113
+ npm run check
114
+ ```
@@ -0,0 +1,114 @@
1
+ # pi-safety-guards
2
+
3
+ Pi 的 Bash 安全规则插件,支持选择预设、设置规则动作和扩展匹配器。
4
+
5
+ [English](./README.md)
6
+
7
+ ## 安装
8
+
9
+ ```bash
10
+ pi install npm:pi-safety-guards
11
+ ```
12
+
13
+ 安装或修改配置后执行 `/reload`。
14
+
15
+ ## 预设
16
+
17
+ 默认启用 `destructive-operations`,执行下列操作前要求确认:
18
+
19
+ | 预设 | 规则 ID | 匹配范围 | 动作 |
20
+ | --- | --- | --- | --- |
21
+ | `destructive-operations` | `filesystem.delete` | 实际执行的 rm/rmdir | confirm |
22
+ | `destructive-operations` | `filesystem.format` | 实际执行的 mkfs/mkfs.* | confirm |
23
+ | `destructive-operations` | `filesystem.ownership` | 实际执行的 chown | confirm |
24
+ | `destructive-operations` | `shell.fork-bomb` | 支持的冒号函数 fork bomb 形式 | confirm |
25
+ | `workspace-boundary`(可选) | `paths.workspace` | Bash 显式路径超出 `.` | block |
26
+
27
+ 匹配基于解析后的命令,覆盖支持的 wrapper、字面量嵌套 shell、命令替换和重定向。`echo 'rm file'` 和 `git rm` 不视为实际执行 rm。
28
+
29
+ ## 配置
30
+
31
+ 文件:`<pi-agent-dir>/extensions/pi-safety-guards/config.json`。agent 目录遵守 `PI_CODING_AGENT_DIR`。
32
+
33
+ ```json
34
+ {
35
+ "presets": ["destructive-operations"],
36
+ "rules": [
37
+ { "id": "filesystem.delete", "action": "block" },
38
+ { "id": "filesystem.ownership", "enabled": false },
39
+ { "id": "example.command", "action": "confirm", "match": { "commands": ["example-command"] } }
40
+ ]
41
+ }
42
+ ```
43
+
44
+ 将 `example-command` 替换为需要匹配的命令。另见 [config.example.json](./config.example.json) 和 [custom-rules.json](./examples/custom-rules.json)。
45
+
46
+ - `presets`:缺省使用默认预设,`[]` 表示不选择预设。
47
+ - `rules`:按 ID 覆盖所选预设中的规则,或添加新规则;列表内 ID 不能重复。
48
+ - 已有规则可覆盖 `action`、`match`、`message`,或设置 `enabled: false`。
49
+ - 新启用规则必须提供 `id`、`action` 和 `match`。
50
+ - `message`:可选的非空文本,或包含 `zh-CN`、`en-US` 的对象。不填写时反馈规则 ID 和动作。
51
+ - 预设和规则都为空时关闭检查,并提示当前没有启用保护。
52
+
53
+ 使用 `/config:safety-guards` 打开常规 TUI 预设菜单,输入 `reset` 恢复默认预设;保存后执行 `/reload`。
54
+
55
+ ### 动作
56
+
57
+ | 动作 | 行为 |
58
+ | --- | --- |
59
+ | `warn` | 放行,将提醒附加到对应工具结果,无 UI 模式也可见。 |
60
+ | `confirm` | 合并命中的规则后询问一次;无法交互或用户未确认时阻断。 |
61
+ | `block` | 不询问,直接拒绝。 |
62
+
63
+ 多条规则命中时,优先级为 `block > confirm > warn`,反馈包含所有命中的规则 ID。
64
+
65
+ ### 匹配器
66
+
67
+ 每条规则选择一种匹配方式:
68
+
69
+ | `match` | 含义 |
70
+ | --- | --- |
71
+ | `{ "commands": ["example-command"] }` | 实际执行程序的 basename 精确匹配 |
72
+ | `{ "detector": "disk-format" }` | mkfs 或 mkfs.* |
73
+ | `{ "detector": "fork-bomb" }` | 支持的冒号函数 fork bomb 形式 |
74
+ | `{ "detector": "in-place-edit" }` | sed 原地编辑选项 |
75
+ | `{ "detector": "home-root" }` | 未加引号的独立 `~` 参数 |
76
+ | `{ "detector": "root-search" }` | find 参数为 `/` |
77
+ | `{ "outsideRoots": [".", "../shared"] }` | 显式路径超出配置的允许根 |
78
+ | `{ "module": "./rules/deploy.mjs" }` | 可信的本地匹配器模块 |
79
+
80
+ ### 目录规则
81
+
82
+ 相对根路径以 Pi 当前工作目录为基准,也支持绝对路径和 `~/`。只有列出的根才被允许,预设中的 `.` 表示工作目录。安装 `pi-add-dir` 时会同时尊重其当前会话目录授权;`session_squash` 后会从被压缩的源分支恢复这份授权。需要时添加额外目录或 `/dev/null` 等设备路径。
83
+
84
+ 自定义匹配器可使用导出的 `findOutOfScopeBashPaths(command, cwd, roots)`,提供自行计算的目录列表。
85
+
86
+ ### 自定义模块
87
+
88
+ 模块路径以配置文件所在目录为基准。JavaScript ES 模块必须默认导出匹配函数:
89
+
90
+ ```js
91
+ export default ({ commands }) => commands.some(
92
+ ({ name, args }) => name === "example-deploy" && args.includes("--production"),
93
+ );
94
+ ```
95
+
96
+ 函数接收冻结的 `{ command, cwd, commands: [{ name, args }] }` 摘要,返回布尔值或 `Promise<boolean>`。包内导出了 `RuleContext` 和 `RuleMatcher` 类型。
97
+
98
+ 只加载启用的模块。加载失败、匹配异常、非布尔返回或异步工作超过 5 秒,均会阻断操作。修改模块后执行 `/reload`。
99
+
100
+ 模块拥有进程权限,超时无法中断同步死循环或撤销副作用,因此只能加载可信代码。
101
+
102
+ ## 限制与错误
103
+
104
+ 本包检查 Pi 的 `bash` 工具,不覆盖用户直接执行的 shell、其他工具或程序内部操作。不保证检测所有危险命令,不模拟所有工作目录变化,也不解析任意变量计算的路径。目录检查处理可静态识别的引用和符号链接,不是操作系统访问控制。
105
+
106
+ 缺少配置文件时使用默认预设。配置损坏、Bash 解析失败或启用规则异常时阻断 Bash,修复相应错误后恢复;修改配置需要 `/reload`。
107
+
108
+ 运行时文案支持中英文。
109
+
110
+ ## 开发
111
+
112
+ ```bash
113
+ npm run check
114
+ ```
package/SKILL.md ADDED
@@ -0,0 +1,41 @@
1
+ ---
2
+ name: configure-pi-safety-guards
3
+ description: "配置安全预设、规则动作和自定义匹配器。Use when configuring safety presets, rule actions or custom matchers."
4
+ ---
5
+
6
+ # 配置安全规则 / Configure safety rules
7
+
8
+ ## 配置位置 / Location
9
+
10
+ `<pi-agent-dir>/extensions/pi-safety-guards/config.json`
11
+
12
+ 遵守 `PI_CODING_AGENT_DIR`,可以使用 `/config:safety-guards` 打开 TUI 预设菜单,输入 `reset` 恢复默认预设,保存后执行 `/reload`。
13
+ Respect `PI_CODING_AGENT_DIR`; use `/config:safety-guards` to open the TUI preset menu, use `reset` for the default preset, and reload after saving.
14
+
15
+ ## 选择规则 / Select rules
16
+
17
+ 默认 `destructive-operations` 对支持的删除、格式化、所有权修改和 fork bomb 操作要求确认。需要限制目录时选择 `workspace-boundary`。
18
+
19
+ The default `destructive-operations` preset asks for confirmation for supported deletion, formatting, ownership and fork-bomb operations. Select `workspace-boundary` for directory restrictions.
20
+
21
+ - `presets`:选择预设;`[]` 不选择预设。Select presets; `[]` selects none.
22
+ - `rules`:按 ID 覆盖或添加;`enabled: false` 禁用已有规则。Override or add by ID; use `enabled: false` to disable a rule.
23
+ - `action`:`warn`、`confirm`、`block`,优先级递增。Actions in increasing priority.
24
+ - `match`:`commands`、`detector`、`outsideRoots`、`module`,选择一种。Choose one matcher.
25
+ - `message`:可选说明,支持文本或中英文对象。Optional text or bilingual message object.
26
+
27
+ 字段说明和示例见 [README](./README.zh-CN.md) 与 [配置示例](./config.example.json)。
28
+ See the [English README](./README.md) and [configuration example](./config.example.json) for details.
29
+
30
+ ## 自定义模块 / Custom modules
31
+
32
+ 使用显式指定的可信本地 ES 模块,默认导出函数并返回布尔值。模块接收冻结的命令摘要;加载和异步匹配 5 秒超时。同进程代码不是沙箱,不能通过超时中止同步死循环。
33
+
34
+ Use a trusted local ES module that default-exports a boolean matcher over the frozen command summary. Loading and asynchronous matching have a five-second deadline. Same-process modules are not sandboxed; the deadline cannot stop synchronous loops.
35
+
36
+ ## 验证与排错 / Verify and troubleshoot
37
+
38
+ - 配置后检查预设和规则 ID、动作及目录根是否符合预期。Check selected presets, IDs, actions and directory roots.
39
+ - 无 UI 时 confirm 会阻断;warn 附加到对应工具结果。Without a UI, confirm blocks; warn appears in the corresponding tool result.
40
+ - 配置、解析或规则失败时查看错误原因,修复后重试;修改配置或模块后 reload。Inspect failures, fix the cause and retry; reload after configuration or module changes.
41
+ - 运行 `npm run check` 验证代码;单元测试不替代真实 Pi 交互确认。Run code checks; unit tests do not replace interactive Pi verification.
@@ -0,0 +1,6 @@
1
+ {
2
+ "presets": [
3
+ "destructive-operations"
4
+ ],
5
+ "rules": []
6
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "presets": ["destructive-operations"],
3
+ "rules": [
4
+ {
5
+ "id": "filesystem.delete",
6
+ "action": "warn"
7
+ },
8
+ {
9
+ "id": "filesystem.ownership",
10
+ "enabled": false
11
+ },
12
+ {
13
+ "id": "example.command",
14
+ "action": "block",
15
+ "match": { "commands": ["example-command"] }
16
+ }
17
+ ]
18
+ }
package/index.ts ADDED
@@ -0,0 +1,167 @@
1
+ import type {
2
+ ExtensionAPI,
3
+ ExtensionCommandContext,
4
+ } from "@earendil-works/pi-coding-agent";
5
+ import { dirname } from "node:path";
6
+ import { configPath, loadConfig, loadConfigDocument, saveConfigDocument } from "./src/config.ts";
7
+ import { DEFAULT_PRESETS, PRESETS } from "./src/presets.ts";
8
+ import { compileRules, evaluateRules, type ModuleLoader } from "./src/engine.ts";
9
+ import { addedDirectoryPathsFromSession } from "./src/bash-directory-scope-utils.ts";
10
+ import { i18n } from "./src/i18n.ts";
11
+ import type { SafetyConfig, SafetyRule } from "./src/types.ts";
12
+
13
+ const BASH_TOOL = "bash";
14
+ const ERROR_LEVEL = "error";
15
+ const INFO_LEVEL = "info";
16
+ const WARN_LEVEL = "warning";
17
+ const BLOCK_ACTION = "block";
18
+ const CONFIRM_ACTION = "confirm";
19
+ const WARN_ACTION = "warn";
20
+ const CONFIG_COMMAND_ALIASES = ["config:safety-guards", "safety-guards-config", "pi-safety-guards-config"] as const;
21
+ const CONFIG_RESET_COMMAND = "reset";
22
+ const CONFIG_PRESET_NAMES = Object.keys(PRESETS);
23
+
24
+ /** 注册配置命令,通过 TUI 菜单选择安全预设并保留自定义规则。 */
25
+ function registerConfigCommand(pi: ExtensionAPI): void {
26
+ const command = {
27
+ description: i18n.t("configCommandDescription"),
28
+ getArgumentCompletions: () => [{ value: CONFIG_RESET_COMMAND, label: CONFIG_RESET_COMMAND }],
29
+ /** Handles preset selection and persists each change immediately. */
30
+ handler: async (args: string, ctx: ExtensionCommandContext): Promise<void> => {
31
+ const argument = args.trim();
32
+ if (argument && argument !== CONFIG_RESET_COMMAND) {
33
+ ctx.ui.notify(i18n.t("configCommandUsage"), WARN_LEVEL);
34
+ return;
35
+ }
36
+ if (argument === CONFIG_RESET_COMMAND) {
37
+ try {
38
+ const path = saveConfigDocument({ presets: [...DEFAULT_PRESETS], rules: [] });
39
+ ctx.ui.notify(i18n.t("configCommandSaved", { path }), INFO_LEVEL);
40
+ } catch (error) {
41
+ ctx.ui.notify(i18n.t("configCommandInvalid", {
42
+ error: error instanceof Error ? error.message : String(error),
43
+ }), ERROR_LEVEL);
44
+ }
45
+ return;
46
+ }
47
+ if (!ctx.hasUI) {
48
+ ctx.ui.notify(i18n.t("configCommandInteractiveOnly"), WARN_LEVEL);
49
+ return;
50
+ }
51
+
52
+ let document: { presets: string[]; rules: unknown[] };
53
+ try {
54
+ document = loadConfigDocument();
55
+ } catch (error) {
56
+ ctx.ui.notify(i18n.t("configCommandInvalid", {
57
+ error: error instanceof Error ? error.message : String(error),
58
+ }), ERROR_LEVEL);
59
+ return;
60
+ }
61
+ while (true) {
62
+ const choices = CONFIG_PRESET_NAMES.map((name) => i18n.t("configPreset", {
63
+ name,
64
+ value: document.presets.includes(name) ? i18n.t("configOn") : i18n.t("configOff"),
65
+ }));
66
+ choices.push(i18n.t("configCustomRules", { count: document.rules.length }), i18n.t("configDone"));
67
+ const selected = await ctx.ui.select(i18n.t("configMenuTitle"), choices);
68
+ if (selected === undefined || selected === choices[choices.length - 1]) return;
69
+ const selectedIndex = choices.indexOf(selected);
70
+ if (selectedIndex === CONFIG_PRESET_NAMES.length) {
71
+ ctx.ui.notify(i18n.t("configCustomRulesHint"), INFO_LEVEL);
72
+ continue;
73
+ }
74
+ const preset = CONFIG_PRESET_NAMES[selectedIndex];
75
+ if (!preset) continue;
76
+ const presets = document.presets.includes(preset)
77
+ ? document.presets.filter((name) => name !== preset)
78
+ : [...document.presets, preset];
79
+ try {
80
+ const path = saveConfigDocument({ presets, rules: document.rules });
81
+ document = { ...document, presets };
82
+ ctx.ui.notify(i18n.t("configCommandSaved", { path }), INFO_LEVEL);
83
+ } catch (error) {
84
+ ctx.ui.notify(i18n.t("configCommandInvalid", {
85
+ error: error instanceof Error ? error.message : String(error),
86
+ }), ERROR_LEVEL);
87
+ }
88
+ }
89
+ },
90
+ };
91
+ for (const name of CONFIG_COMMAND_ALIASES) pi.registerCommand(name, command);
92
+ }
93
+
94
+ /** 将规则 ID 和用户选择的说明一起展示,不自动执行替代命令。 */
95
+ function describeRule(rule: SafetyRule): string {
96
+ const message = typeof rule.message === "string" ? rule.message : rule.message?.[i18n.locale()];
97
+ return message ? `[${rule.id}] ${message}` : i18n.t("ruleMatched", { id: rule.id });
98
+ }
99
+
100
+ /** 先加载规则再注册统一执行入口,禁用规则不会执行模块。 */
101
+ export async function registerSafetyGuards(
102
+ pi: ExtensionAPI,
103
+ config: SafetyConfig,
104
+ options: { configDirectory?: string; loader?: ModuleLoader } = {},
105
+ ): Promise<void> {
106
+ const rules = await compileRules(config, options.configDirectory ?? dirname(configPath()), options.loader);
107
+ if (rules.length === 0) {
108
+ pi.on("session_start", (_event, ctx) => ctx.ui.notify(i18n.t("noRules"), INFO_LEVEL));
109
+ return;
110
+ }
111
+ const warnings = new Map<string, string>();
112
+ pi.on("session_shutdown", () => warnings.clear());
113
+ pi.on("turn_end", () => warnings.clear());
114
+ pi.on("tool_call", async (event, ctx) => {
115
+ if (event.toolName !== BASH_TOOL) return;
116
+ const command = String(event.input.command ?? "");
117
+ try {
118
+ const additionalRoots = addedDirectoryPathsFromSession(
119
+ ctx.sessionManager.getEntries(),
120
+ ctx.sessionManager.getBranch(),
121
+ );
122
+ const decision = await evaluateRules(rules, command, ctx.cwd, additionalRoots);
123
+ if (!decision) return;
124
+ const details = decision.matches.map(describeRule).join("\n");
125
+ if (decision.action === BLOCK_ACTION) return { block: true, reason: i18n.t("blocked", { details }) };
126
+ if (decision.action === CONFIRM_ACTION) {
127
+ if (!ctx.hasUI) return { block: true, reason: i18n.t("confirmationUnavailable", { details }) };
128
+ const accepted = await ctx.ui.confirm(i18n.t("confirmTitle"), i18n.t("confirmBody", { details, command }));
129
+ if (!accepted) return { block: true, reason: i18n.t("confirmationRejected", { details }) };
130
+ }
131
+ const warned = decision.matches.filter((rule) => rule.action === WARN_ACTION);
132
+ if (warned.length) {
133
+ warnings.set(event.toolCallId, i18n.t("warning", { details: warned.map(describeRule).join("\n") }));
134
+ }
135
+ } catch (error) {
136
+ return { block: true, reason: error instanceof Error ? error.message : String(error) };
137
+ }
138
+ });
139
+ // warn 必须对没有 UI 的调用者同样可见,且只附加到对应工具结果。
140
+ pi.on("tool_result", (event) => {
141
+ const warning = warnings.get(event.toolCallId);
142
+ warnings.delete(event.toolCallId);
143
+ if (!warning) return;
144
+ return { content: [...event.content, { type: "text", text: warning }] };
145
+ });
146
+ }
147
+
148
+ /** 配置或启用规则加载失败时阻断 Bash,避免把失败当作关闭保护。 */
149
+ export default async function piSafetyGuards(pi: ExtensionAPI): Promise<void> {
150
+ registerConfigCommand(pi);
151
+ try {
152
+ await registerSafetyGuards(pi, loadConfig());
153
+ } catch (error) {
154
+ const reason = i18n.t("configLoadFailed", { error: error instanceof Error ? error.message : String(error) });
155
+ pi.on("session_start", (_event, ctx) => ctx.ui.notify(reason, ERROR_LEVEL));
156
+ pi.on("tool_call", (event) => {
157
+ if (event.toolName === BASH_TOOL) return { block: true, reason };
158
+ });
159
+ }
160
+ }
161
+
162
+ export { configPath, loadConfig, parseConfig, saveConfig } from "./src/config.ts";
163
+ export type { RuleContext, RuleMatcher } from "./src/types.ts";
164
+ export {
165
+ addedDirectoryPathsFromSession,
166
+ findOutOfScopeBashPaths,
167
+ } from "./src/bash-directory-scope-utils.ts";
@@ -0,0 +1,110 @@
1
+ {
2
+ "configInvalidField": {
3
+ "zh-CN": "安全规则配置字段无效:{field}",
4
+ "en-US": "Invalid safety rule configuration field: {field}"
5
+ },
6
+ "configLoadFailed": {
7
+ "zh-CN": "安全规则加载失败,Bash 已阻断。请修复配置并 /reload:{error}",
8
+ "en-US": "Safety rules failed to load; Bash is blocked. Fix the configuration and /reload: {error}"
9
+ },
10
+ "noRules": {
11
+ "zh-CN": "尚未启用任何安全规则,本插件不会阻断操作。",
12
+ "en-US": "No safety rules are enabled; this extension will not block operations."
13
+ },
14
+ "ruleMatched": {
15
+ "zh-CN": "命中规则 [{id}]",
16
+ "en-US": "Matched rule [{id}]"
17
+ },
18
+ "blocked": {
19
+ "zh-CN": "安全规则已阻断操作:\n{details}",
20
+ "en-US": "Safety rules blocked the operation:\n{details}"
21
+ },
22
+ "confirmTitle": {
23
+ "zh-CN": "确认执行命中的危险操作?",
24
+ "en-US": "Confirm the matched dangerous operation?"
25
+ },
26
+ "confirmBody": {
27
+ "zh-CN": "{details}\n\n命令:\n{command}",
28
+ "en-US": "{details}\n\nCommand:\n{command}"
29
+ },
30
+ "confirmationUnavailable": {
31
+ "zh-CN": "此操作需要用户确认,但当前模式无法交互,已阻断:\n{details}",
32
+ "en-US": "User confirmation is required but this mode cannot prompt; operation blocked:\n{details}"
33
+ },
34
+ "confirmationRejected": {
35
+ "zh-CN": "用户未确认,操作已阻断:\n{details}",
36
+ "en-US": "The user did not confirm; operation blocked:\n{details}"
37
+ },
38
+ "warning": {
39
+ "zh-CN": "安全规则提醒(本规则不阻断操作):\n{details}",
40
+ "en-US": "Safety rule warning (operation was not blocked by this rule):\n{details}"
41
+ },
42
+ "shellParseBlocked": {
43
+ "zh-CN": "无法完整解析 Bash 命令,已阻止执行。",
44
+ "en-US": "The Bash command could not be parsed completely and was blocked."
45
+ },
46
+ "ruleFailed": {
47
+ "zh-CN": "安全规则 [{id}] 执行失败,已阻止操作:{error}",
48
+ "en-US": "Safety rule [{id}] failed; operation blocked: {error}"
49
+ },
50
+ "moduleMustExportMatcher": {
51
+ "zh-CN": "规则模块必须默认导出匹配函数。",
52
+ "en-US": "A rule module must default-export a matcher function."
53
+ },
54
+ "matcherMustReturnBoolean": {
55
+ "zh-CN": "规则匹配函数必须返回布尔值。",
56
+ "en-US": "A rule matcher must return a boolean."
57
+ },
58
+ "moduleTimeout": {
59
+ "zh-CN": "规则模块加载或匹配超时。",
60
+ "en-US": "Rule module loading or matching timed out."
61
+ },
62
+ "configCommandDescription": {
63
+ "zh-CN": "配置安全规则",
64
+ "en-US": "Configure safety rules"
65
+ },
66
+ "configCommandUsage": {
67
+ "zh-CN": "用法:/config:safety-guards(打开配置菜单)或 /config:safety-guards reset(恢复默认预设)",
68
+ "en-US": "Usage: /config:safety-guards (open settings) or /config:safety-guards reset (restore defaults)"
69
+ },
70
+ "configMenuTitle": {
71
+ "zh-CN": "安全规则配置",
72
+ "en-US": "Safety rule settings"
73
+ },
74
+ "configPreset": {
75
+ "zh-CN": "预设 {name}:{value}",
76
+ "en-US": "Preset {name}: {value}"
77
+ },
78
+ "configCustomRules": {
79
+ "zh-CN": "自定义规则:{count} 条",
80
+ "en-US": "Custom rules: {count}"
81
+ },
82
+ "configCustomRulesHint": {
83
+ "zh-CN": "自定义规则仍保留在配置文件中;当前 TUI 可以切换内置预设。",
84
+ "en-US": "Custom rules remain in the configuration file; the TUI currently manages built-in presets."
85
+ },
86
+ "configOn": {
87
+ "zh-CN": "开",
88
+ "en-US": "on"
89
+ },
90
+ "configOff": {
91
+ "zh-CN": "关",
92
+ "en-US": "off"
93
+ },
94
+ "configDone": {
95
+ "zh-CN": "完成",
96
+ "en-US": "Done"
97
+ },
98
+ "configCommandInteractiveOnly": {
99
+ "zh-CN": "安全规则配置需要在 TUI 中修改;请在交互式 Pi 会话中执行命令。",
100
+ "en-US": "Safety configuration requires the TUI; run this command in an interactive Pi session."
101
+ },
102
+ "configCommandSaved": {
103
+ "zh-CN": "安全规则配置已保存:{path}。请执行 /reload 使配置生效。",
104
+ "en-US": "Safety configuration saved to {path}. Run /reload to apply it."
105
+ },
106
+ "configCommandInvalid": {
107
+ "zh-CN": "安全规则配置无效:{error}",
108
+ "en-US": "Invalid safety configuration: {error}"
109
+ }
110
+ }
package/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "pi-safety-guards",
3
+ "version": "0.1.0",
4
+ "description": "Configurable Bash safety rules with selectable presets and custom matchers for Pi",
5
+ "type": "module",
6
+ "main": "./index.ts",
7
+ "exports": {
8
+ ".": "./index.ts"
9
+ },
10
+ "files": [
11
+ "index.ts",
12
+ "src",
13
+ "locales",
14
+ "config.example.json",
15
+ "README.md",
16
+ "README.zh-CN.md",
17
+ "SKILL.md",
18
+ "tsconfig.json",
19
+ "examples"
20
+ ],
21
+ "scripts": {
22
+ "test": "tsx --test tests/*.test.ts",
23
+ "typecheck": "tsc --noEmit --pretty false",
24
+ "build": "npm run typecheck",
25
+ "check": "npm run typecheck && npm test && npm pack --dry-run --json > /dev/null"
26
+ },
27
+ "pi": {
28
+ "extensions": [
29
+ "./index.ts",
30
+ "../pi-extensions-i18n/index.ts"
31
+ ],
32
+ "skills": [
33
+ "./SKILL.md"
34
+ ]
35
+ },
36
+ "engines": {
37
+ "node": ">=22"
38
+ },
39
+ "license": "MIT",
40
+ "author": "maplezzk",
41
+ "homepage": "https://github.com/maplezzk/pi-extensions/tree/main/packages/pi-safety-guards",
42
+ "bugs": "https://github.com/maplezzk/pi-extensions/issues",
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "git+https://github.com/maplezzk/pi-extensions.git",
46
+ "directory": "packages/pi-safety-guards"
47
+ },
48
+ "publishConfig": {
49
+ "access": "public",
50
+ "registry": "https://registry.npmjs.org"
51
+ },
52
+ "keywords": [
53
+ "pi-package",
54
+ "pi",
55
+ "pi-extension",
56
+ "coding-agent",
57
+ "safety",
58
+ "bash",
59
+ "presets",
60
+ "rules"
61
+ ],
62
+ "peerDependencies": {
63
+ "@earendil-works/pi-coding-agent": ">=0.80.0 <0.81.0"
64
+ },
65
+ "dependencies": {
66
+ "pi-extensions-i18n": "^0.4.0",
67
+ "unbash": "^4.0.10"
68
+ },
69
+ "devDependencies": {
70
+ "@earendil-works/pi-coding-agent": "0.80.10",
71
+ "@types/node": "24.12.4",
72
+ "tsx": "4.23.1",
73
+ "typescript": "5.9.3"
74
+ }
75
+ }