Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 142 additions & 0 deletions .agents/skills/building-guardrails-extensions/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
---
name: building-guardrails-extensions
description: "Builds new pi-guardrails feature extensions using the core/shared split. Use when adding guardrails features such as zones, policy engines, path controls, permission gates, or new Pi hooks in this repository."
---

# Building Guardrails Extensions

Use this when adding a new feature extension to `@aliou/pi-guardrails`.

## Architecture rules

- Put Pi-free primitives in `src/core` only when they are generic and reusable.
- Put shared Pi-extension infrastructure in `src/shared` only when multiple extensions need it.
- Put feature-specific rules, metadata, UI, prompts, and config interpretation in `extensions/<feature>`.
- Keep runtime code on the current config shape only. Legacy shapes belong in migrations.
- Keep event payload compatibility unless explicitly changing the public event contract.
- All split extensions share one config file: `guardrails.json` via `src/shared/config/loader.ts`.

## Extension shape

Create:

```text
extensions/<feature>/
index.ts # Pi adapter: load config, register hooks/events/commands
rules.ts # Rule<TMeta> factories and feature-specific metadata
targets.ts # Tool input -> Action targets, if needed
prompt.ts # UI prompt, if needed
grants.ts # Persisted/session grants, if needed
```

Do not add feature-specific metadata to `src/shared`.

## Core rule pattern

Use core typed rules:

```ts
import type { Action, Rule } from "../../src/core";

export type ZonesMeta = {
zoneId: string;
path: string;
};

export function createZonesRule(): Rule<ZonesMeta> {
return {
key: "zones.access",
check(action: Action) {
if (action.kind !== "file") return { kind: "pass" };
return {
kind: "match",
reason: "Zone policy blocks this access.",
metadata: { zoneId: "workspace", path: action.path },
};
},
};
}
```

Rules must return `{ kind: "pass" }` or `{ kind: "match", reason, metadata }`. Metadata and reason are required. Use `TMeta = null` only when there is truly no metadata.

## Adapter pattern

In `extensions/<feature>/index.ts`:

1. Read `configLoader.getConfig()` inside hooks, not once at startup.
2. Exit early when `!config.enabled` or feature flag is false.
3. Convert Pi events/tool inputs into core `Action`s.
4. Call `checkAction()` with feature rules.
5. Emit existing shared events if blocking/dangerous behavior matches current contracts.
6. Register loaded feature status through shared events when useful for settings UI.

## Config pattern

For a new feature such as issue #29 zones:

- Add current config types in `src/shared/config/types.ts`.
- Add defaults in `src/shared/config/defaults.ts`.
- Add `features.<feature>` using `GuardrailsFeatureId` if it is user-toggleable.
- Add a migration only if persisted config keys or old shapes need conversion.
- Do not add runtime branches for old config shapes.

Hypothetical zones config should stay feature-owned at runtime:

```json
{
"features": { "zones": true },
"zones": [
{
"id": "workspace",
"path": "~/workspace",
"bash": "safe-only",
"files": "readOnly"
}
],
"zonesDefault": { "bash": "block", "files": "noAccess" }
}
```

For zones, keep CWD priority semantics in the zones feature, not in shared path helpers, unless it becomes generally reusable.

## Target extraction

Reuse existing helpers before adding new parsing:

- Bash/path candidates: `src/shared/paths`.
- Shell AST helpers: `src/core/shell`.
- Path normalization/access primitives: `src/core/paths`.
- Matching helpers: `src/shared/matching`.

If a feature must inspect bash paths, add a feature `targets.ts` that converts tool calls into file actions or feature-specific targets.

## Settings and commands

- Primary guardrails settings live under `extensions/guardrails/commands/settings`.
- Feature UI belongs with its feature unless it is a cross-feature settings command.
- Use `registerSettingsCommand` for settings screens only.
- Use direct `pi.registerCommand` plus `Wizard` for guided flows.
- Do not put example/preset workflows in settings tabs unless they are truly settings.

## Tests

Add focused tests next to the feature:

```text
extensions/<feature>/rules.test.ts
extensions/<feature>/targets.test.ts
extensions/<feature>/grants.test.ts # if grants exist
```

Prefer pure rule/target tests over full extension harness tests. Use hook-level tests only when Pi event integration is the behavior under test.

## Documentation

When adding or changing defaults, permission patterns, or presets, update:

- `docs/defaults.md`
- `docs/examples.md` if presets change
- `README.md` if commands, feature flags, or public behavior changes

Add a changeset for user-facing behavior before release.
5 changes: 5 additions & 0 deletions .changeset/remove-command-explainer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@aliou/pi-guardrails": minor
---

Remove the permission gate command explainer and its subagent runtime.
5 changes: 5 additions & 0 deletions .changeset/shared-config-migrations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@aliou/pi-guardrails": patch
---

Move config migrations into shared modules and only show onboarding when no guardrails config exists.
5 changes: 5 additions & 0 deletions .changeset/update-pi-utils-settings.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@aliou/pi-guardrails": patch
---

Update settings utilities to the latest version.
7 changes: 5 additions & 2 deletions .pi/settings.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
{
"packages": ["npm:@378labs/pi-oss"]
}
"packages": [
"npm:@378labs/pi-oss",
"npm:@agents/vitest"
]
}
16 changes: 8 additions & 8 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,15 @@ pnpm changeset # Create changeset for versioning

```
src/
index.ts # Extension entry, registers hooks and commands
config.ts # Configuration loading, schema, defaults, merge logic
hooks/ # Event hooks (policies + permission gate)
commands/ # Slash commands (settings UI, add-policy)
components/ # UI components (pattern editor)
lib/ # Vendored subagent executor core (Phase 1)
utils/ # Helpers (matching, glob expansion, migration, shell AST)
core/ # Pure guardrail primitives, checks, path rules, shell parsing helpers
shared/ # Pi-extension shared infra and adapters (config, events, matching, filesystem-backed helpers)
extensions/
guardrails/ # Legacy Pi extension entry, hooks, commands, config, UI components
hooks/ # Event hooks (policies, path access, permission gate)
commands/ # Slash commands (settings UI, onboarding)
components/ # UI components (pattern editor)
tests/
utils/ # Test harness utilities (adapted from pi-harness)
pi-context.ts # Spy-based ExtensionContext / UI context builders
pi-test-harness.ts # Full extension loader with emitEvent() for hook testing
load-extension.ts # Wrapper for Pi internal extension loader
matchers.ts # Custom vitest matchers (toHaveRegisteredTool, etc.)
Expand All @@ -51,6 +50,7 @@ tests/
- Built-in dangerous command matching uses AST parsing via `@aliou/sh`; user-configured patterns use substring/regex matching
- File protection is policy-based (`features.policies`, `policies.rules`), not legacy `envFiles`
- Config migrations are predicate-based (`shouldRun`) using structural checks; do not rely on lexicographic version string comparisons
- Runtime code must only handle current config/core shapes. Old config shapes belong exclusively in migrations; do not add runtime compatibility branches for legacy config.
- `config.version` is a schema marker for debugging/inspection, not the package version
- Events emitted on the pi event bus for inter-extension communication (`guardrails:blocked`, `guardrails:dangerous`)

Expand Down
34 changes: 4 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,13 @@ pi install git:github.com/aliou/pi-guardrails
## Documentation

- [Default configuration](docs/defaults.md) — built-in policy rules and permission gate patterns
- [Example presets](docs/examples.md) — pre-configured presets available in settings
- [Example presets](docs/examples.md) — pre-configured presets available with `/guardrails:examples`

## What it does

- **policies**: named file-protection rules with per-rule protection levels.
- **permission-gate**: detects dangerous bash commands and asks for confirmation.
- **path-access**: restricts tool access to the current working directory with allow/ask/block modes.
- **optional command explainer**: can call a small LLM to explain a dangerous command inline in the confirmation dialog.

## Config locations

Expand All @@ -42,7 +41,7 @@ Guardrails reads and merges config from:

Priority: `memory > local > global > defaults`.

Use `/guardrails:settings` to edit config interactively.
Use `/guardrails:settings` to edit config interactively. Use `/guardrails:examples` to apply example policy and command presets.

## Current schema

Expand Down Expand Up @@ -91,10 +90,7 @@ Use `/guardrails:settings` to edit config interactively.
"customPatterns": [],
"requireConfirmation": true,
"allowedPatterns": [],
"autoDenyPatterns": [],
"explainCommands": false,
"explainModel": null,
"explainTimeout": 5000
"autoDenyPatterns": []
}
}
```
Expand All @@ -119,16 +115,6 @@ Each rule has:
When multiple rules match the same file, strongest protection wins:
`noAccess > readOnly > none`.

### Add rule with AI

Use:

```text
/guardrails:add-policy
```

This starts a subagent that helps build and save one policy rule.

## Path access

Restrict tool access to the current working directory. When enabled, any tool call targeting a path outside `cwd` is checked against the configured mode:
Expand Down Expand Up @@ -162,25 +148,13 @@ Built-in dangerous patterns are matched structurally (AST-based) for better accu

- `rm -rf`
- `sudo`
- `dd if=`
- `dd of=`
- `mkfs.`
- `chmod -R 777`
- `chown -R`

You can also add custom dangerous patterns.

### Explain commands (opt-in)

If enabled, guardrails calls an LLM before showing the confirmation dialog and displays a short explanation.

Config fields:

- `permissionGate.explainCommands` (boolean)
- `permissionGate.explainModel` (`provider/model-id`)
- `permissionGate.explainTimeout` (ms)

Failures/timeouts degrade gracefully: dialog still shows without explanation.

## Migration notes

Legacy fields are auto-migrated:
Expand Down
3 changes: 3 additions & 0 deletions __mocks__/fs.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const { fs } = require("memfs");

module.exports = fs;
3 changes: 3 additions & 0 deletions __mocks__/fs/promises.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const { fs } = require("memfs");

module.exports = fs.promises;
2 changes: 1 addition & 1 deletion biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"useIgnoreFile": true
},
"files": {
"includes": ["**/*.ts", "**/*.json"],
"includes": ["src/**/*.ts", "extensions/**/*.ts", "*.json"],
"ignoreUnknown": true
},
"assist": {
Expand Down
Loading