diff --git a/agent/automode.json b/agent/automode.json new file mode 100644 index 0000000..0cedd5b --- /dev/null +++ b/agent/automode.json @@ -0,0 +1,54 @@ +{ + "autoMode": { + "enabled": true, + "environment": [ + "$defaults" + ], + "allow": [ + "$defaults", + "Using standard credentials with their intended configured providers is allowed only when secret values remain hidden from the agent, command arguments, and command output. Secret injection through op run is allowed under this condition, but never with --no-masking." + ], + "protectedPaths": [ + "$defaults" + ], + "soft_deny": [ + "$defaults", + "Block every operation that changes state in an external or remote system unless the latest user instruction directly identifies the target and requests that exact state change. This includes cloud services, APIs, databases, infrastructure, clusters, ticketing and collaboration systems, communications, CI/CD, package registries, and remote hosts. The built-in allowance for pushing the current non-default working branch or a new task branch remains permitted.", + "Block package publication unless the user explicitly identifies the package, version, registry, and asks to publish it.", + "Block destructive database statements, including DROP DATABASE and DROP TABLE, unless the user explicitly identifies the target database or table and confirms the destructive action.", + "Block Terraform or OpenTofu apply and destroy unless the user explicitly identifies the environment and approves the exact infrastructure change after reviewing a plan.", + "Block Kubernetes mutations, remote execution, port-forwarding, and proxying unless the user explicitly identifies the cluster and namespace and authorizes that exact action.", + "Block Docker system prune unless the user explicitly authorizes cleanup of Docker resources.", + "Block AWS resource or S3-object deletion unless the user explicitly identifies the account, resource or bucket, scope, and deletion intent.", + "Block Jira and GitLab write actions unless the user explicitly identifies the target and requested state change." + ], + "hard_deny": [ + "$defaults", + "Never retrieve, reveal, print, log, decode, search for, or transmit secret values, credentials, tokens, private keys, authentication material, or sensitive configuration. Using op run is permitted only when injected values remain hidden from the agent, command arguments, and command output.", + "Never send repository contents to third-party code-review APIs." + ] + }, + "permissions": { + "deny": [ + "grep(*)", + "read(*.env*)", + "read(*.netrc)", + "read(*.npmrc)", + "read(*.pypirc)", + "read(*.git-credentials)", + "read(*.aws/credentials)", + "read(*.config/gh/hosts.yml)", + "read(*.docker/config.json)", + "read(*.kube/config)", + "read(*id_rsa*)", + "read(*id_ed25519*)", + "read(*.pem)", + "read(*.key)", + "read(*.p12)", + "read(*.pfx)", + "read(*credentials.json)", + "read(*service-account*.json)", + "read(*auth.json)" + ] + } +} diff --git a/agent/extensions/guardrails.json b/agent/extensions/guardrails.json index 916aa8d..5c9e762 100644 --- a/agent/extensions/guardrails.json +++ b/agent/extensions/guardrails.json @@ -1,182 +1,3 @@ { - "$schema": "https://unpkg.com/@aliou/pi-guardrails@0.15.0/schema.json", - "applyBuiltinDefaults": true, - "version": "0.13.0-20260619", - "onboarding": { - "completed": true, - "completedAt": "2026-06-27T00:47:19.051Z", - "version": "0.13.0-20260619" - }, - "features": { - "policies": true, - "permissionGate": true, - "pathAccess": true - }, - "permissionGate": { - "requireConfirmation": true, - "patterns": [ - { - "pattern": "(?:^|[|&;]\\s*)opr?(?:\\s|$)", - "regex": true, - "description": "1Password CLI (op) accesses your vault; run sparingly and only with explicit approval." - }, - { - "pattern": "(?:^|[|&;\\n]\\s*)acli\\s+jira\\s+(?![^|&;\\n]*\\s--help(?=\\s|$|[|&;\\n]))(?:(?:auth\\s+(?:login|logout|switch))|(?:board\\s+(?:create|delete))|(?:field\\s+(?:cancel-delete|create|delete|restore|update))|(?:filter\\s+(?:add-favourite|change-owner|reset-columns|update))|(?:project\\s+(?:archive|create|delete|restore|update))|(?:sprint\\s+(?:create|delete|update))|(?:workitem\\s+(?:archive|assign|attachment\\s+delete|clone|comment\\s+(?:create|delete|update)|create(?:-bulk)?|delete|edit|link\\s+(?:create|delete)|transition|unarchive|watcher\\s+remove)))\\b", - "regex": true, - "description": "Jira write action via acli" - }, - { - "pattern": "(?:^|[|&;\\n]\\s*)glab\\s+(?![^|&;\\n]*\\s--help(?=\\s|$|[|&;\\n]))(?:(?:alias\\s+(?:delete|set))|(?:auth\\s+(?:configure-docker|login|logout))|(?:ci\\s+(?:cancel|delete|retry|run(?:-trig)?|trigger))|(?:cluster\\s+agent\\s+(?:bootstrap|get-token|update-kubeconfig|token\\s+revoke|token-cache\\s+clear))|(?:config\\s+(?:edit|set))|(?:container-registry\\s+(?:repository|tag)\\s+delete)|(?:deploy-key\\s+(?:add|delete))|(?:gpg-key\\s+(?:add|delete))|(?:incident\\s+(?:close|note|reopen|subscribe|unsubscribe))|(?:issue\\s+(?:board\\s+create|close|create|delete|note|reopen|subscribe|unsubscribe|update))|(?:label\\s+(?:create|delete|edit))|(?:milestone\\s+(?:create|delete|edit))|(?:mr\\s+(?:approve|close|create|delete|for|merge|note\\s+(?:create|delete|reopen|resolve|update)|rebase|reopen|revoke|subscribe|todo|unsubscribe|update))|(?:opentofu\\s+state\\s+(?:delete|lock|unlock))|(?:packages\\s+upload)|(?:release\\s+(?:create|delete|upload))|(?:repo\\s+(?:create|delete|fork|members\\s+(?:add|remove)|mirror|prune|publish\\s+catalog|remote\\s+add|transfer|update))|(?:runner\\s+(?:assign|delete|unassign|update))|(?:runner-controller\\s+(?:create|delete|scope\\s+(?:create|delete)|token\\s+(?:create|revoke|rotate)|update))|(?:schedule\\s+(?:create|delete|run|update))|(?:securefile\\s+(?:create|remove))|(?:skills\\s+(?:install|update))|(?:snippet\\s+create)|(?:ssh-key\\s+(?:add|delete))|(?:stack\\s+(?:amend|create|infer|reorder|save|sync))|(?:todo\\s+done)|(?:token\\s+(?:create|revoke|rotate))|(?:variable\\s+(?:delete|set|update))|(?:work-items\\s+(?:create|delete|update)))\\b", - "regex": true, - "description": "GitLab write action via glab" - }, - { - "pattern": "(?:^|[|&;\\n]\\s*)glab\\s+api\\b(?![^|&;\\n]*\\s--help(?=\\s|$|[|&;\\n]))(?![^|&;\\n]*\\bgraphql\\b)(?=[^|&;\\n]*(?:(?:-X|--method)(?:\\s+|=)?(?:[Pp][Oo][Ss][Tt]|[Pp][Uu][Tt]|[Pp][Aa][Tt][Cc][Hh]|[Dd][Ee][Ll][Ee][Tt][Ee])\\b|(?:-F|--field|-f|--raw-field|--form|--input)(?:\\s|=)))", - "regex": true, - "description": "GitLab API write action via glab" - }, - { - "pattern": "(?:^|[|&;\\n]\\s*)glab\\s+api\\s+graphql\\b(?![^|&;\\n]*\\s--help(?=\\s|$|[|&;\\n]))(?=[^|&;\\n]*\\bmutation\\b)", - "regex": true, - "description": "GitLab GraphQL mutation via glab" - }, - { - "pattern": "git push .*(-f\\b|--force(?!-with-lease)|--force-with-lease)", - "regex": true, - "description": "Git force push (--force, -f, or --force-with-lease)" - }, - { - "pattern": "(?:npm|yarn|pnpm)\\s+publish", - "regex": true, - "description": "Package publishing (npm/yarn/pnpm)" - }, - { - "pattern": "DROP\\s+(?:DATABASE|TABLE)", - "regex": true, - "description": "SQL DROP DATABASE/TABLE" - }, - { - "pattern": "terraform\\s+(?:apply|destroy)", - "regex": true, - "description": "Terraform apply/destroy (infra changes)" - }, - { - "pattern": "kubectl delete", - "description": "Kubernetes resource deletion" - }, - { - "pattern": "(?:^|[|&;\\n]\\s*)kubectl\\b(?=[^|&;\\n]*\\b(?:apply|create|delete|patch|replace|edit|scale|rollout\\s+(?:restart|undo)|drain|cordon|uncordon|taint|label|annotate|set|exec|cp|debug|run|attach|port-forward|proxy)\\b)", - "regex": true, - "description": "Kubernetes cluster action (including remote command execution); require explicit approval before changing or executing in a cluster." - }, - { - "pattern": "docker system prune", - "description": "Docker system cleanup (removes images/containers/volumes)" - }, - { - "pattern": "aws s3 rm", - "description": "AWS S3 object deletion" - }, - { - "pattern": "aws ec2 terminate-instances", - "description": "AWS EC2 instance termination" - } - ] - }, - "policies": { - "rules": [ - { - "id": "sensitive-files-deny-list", - "name": "Sensitive files deny list", - "description": "Files and paths that may contain secrets, credentials, tokens, or sensitive shell history", - "patterns": [ - { - "pattern": "**/.env*" - }, - { - "pattern": "~/.zsh_history" - }, - { - "pattern": "~/.ssh/**" - }, - { - "pattern": "**/.secrets" - }, - { - "pattern": "*credentials*" - }, - { - "pattern": "~/.npmrc*" - }, - { - "pattern": "~/.local/state/glean-cli/*" - }, - { - "pattern": "~/.pi/agent/auth.json" - }, - { - "pattern": "~/.pi/agent/mcp.json" - }, - { - "pattern": "~/.aws/config" - }, - { - "pattern": "~/.kube/config" - }, - { - "pattern": "~/.config/glab-cli/config.yaml" - }, - { - "pattern": "~/.claude.json" - }, - { - "pattern": "~/.claude/backups/.claude.json.backup*" - }, - { - "pattern": "**/.mcp.json" - } - ], - "protection": "noAccess", - "onlyIfExists": true, - "blockMessage": "Accessing {file} is not allowed. This path may contain secrets, credentials, tokens, or sensitive local data. Ask the user if you need access." - } - ] - }, - "pathAccess": { - "mode": "ask", - "allowedPaths": [ - { - "kind": "directory", - "path": "/tmp" - }, - { - "kind": "file", - "path": "/dev/null" - }, - { - "kind": "directory", - "path": "~/.pi/agent/skills" - }, - { - "kind": "directory", - "path": "~/.pi/agent/extensions" - }, - { - "kind": "file", - "path": "~/.pi/agent/settings" - }, - { - "kind": "directory", - "path": "~/.pi" - }, - { - "kind": "directory", - "path": "~/projects/zapier" - }, - { - "kind": "directory", - "path": "~/dotfiles" - } - ] - } + "$schema": "https://unpkg.com/@aliou/pi-guardrails@0.15.0/schema.json" } diff --git a/agent/extensions/guardrails.test.mts b/agent/extensions/guardrails.test.mts deleted file mode 100644 index bbfc453..0000000 --- a/agent/extensions/guardrails.test.mts +++ /dev/null @@ -1,310 +0,0 @@ -import { readFileSync } from "node:fs"; -import { dirname, join } from "node:path"; -import { fileURLToPath } from "node:url"; -import assert from "node:assert/strict"; -import test from "node:test"; - -type GuardrailPattern = { - pattern: string; - regex?: boolean; - description?: string; -}; - -type GuardrailsConfig = { - permissionGate?: { - patterns?: GuardrailPattern[]; - }; -}; - -type CompiledPattern = GuardrailPattern & { - test(command: string): boolean; -}; - -const configPath = join(dirname(fileURLToPath(import.meta.url)), "guardrails.json"); -const config = JSON.parse(readFileSync(configPath, "utf8")) as GuardrailsConfig; - -function compilePattern(pattern: GuardrailPattern): CompiledPattern { - if (pattern.regex) { - const regex = new RegExp(pattern.pattern); - return { - ...pattern, - test: (command: string) => regex.test(command), - }; - } - - return { - ...pattern, - test: (command: string) => command.includes(pattern.pattern), - }; -} - -const patterns = (config.permissionGate?.patterns ?? []).map(compilePattern); - -function matchingDescriptions(command: string): string[] { - return patterns - .filter((pattern) => pattern.test(command)) - .map((pattern) => pattern.description ?? pattern.pattern); -} - -test("guardrails command patterns compile", () => { - assert.ok(patterns.length > 0, "expected at least one permissionGate pattern"); -}); - -test("read-style acli/glab commands are not flagged", () => { - const allowedCommands = [ - "acli jira auth status", - "acli jira board search --name ENG", - "acli jira dashboard search --query team", - "acli jira field view customfield_12345", - "acli jira filter list", - "acli jira filter view 12345", - "acli jira project list", - "acli jira project view ZDEV", - "acli jira sprint view 123", - "acli jira workitem view ZDEV-123", - "acli jira workitem search --jql \"project = ZDEV and summary ~ create\"", - "acli jira workitem comment list ZDEV-123", - "acli jira workitem attachment list ZDEV-123", - "acli jira workitem link list ZDEV-123", - - "glab auth status", - "glab ci list", - "glab ci status", - "glab issue list --search create", - "glab issue view 123", - "glab label list", - "glab milestone list", - "glab mr list", - "glab mr view 123", - "glab mr diff 123", - "glab repo list", - "glab repo view zapier/zdev-cli", - "glab runner list", - "glab schedule list", - "glab securefile list", - "glab ssh-key list", - "glab variable list", - "glab work-items list", - "glab api projects/:fullpath/issues", - "glab api graphql -f query='query { currentUser { username } }'", - ]; - - for (const command of allowedCommands) { - assert.deepEqual(matchingDescriptions(command), [], command); - } -}); - -test("write-style acli commands are flagged", () => { - const writeCommands = [ - "acli jira auth login", - "acli jira board create --name Test", - "acli jira board delete 123", - "acli jira field create --name Foo", - "acli jira field delete customfield_12345", - "acli jira field update customfield_12345 --name Bar", - "acli jira filter add-favourite 12345", - "acli jira filter change-owner 12345 --account-id abc", - "acli jira filter reset-columns 12345", - "acli jira filter update 12345 --name Bar", - "acli jira project archive ZDEV", - "acli jira project create --key TEST --name Test", - "acli jira project delete TEST", - "acli jira project restore TEST", - "acli jira project update TEST --name Test2", - "acli jira sprint create --board 1 --name Sprint", - "acli jira sprint delete 123", - "acli jira sprint update 123 --name Sprint2", - "acli jira workitem archive ZDEV-123", - "acli jira workitem assign ZDEV-123 --assignee me", - "acli jira workitem attachment delete 999", - "acli jira workitem clone ZDEV-123", - "acli jira workitem comment create ZDEV-123 --body hi", - "acli jira workitem comment delete ZDEV-123 10000", - "acli jira workitem comment update ZDEV-123 10000 --body hi", - "acli jira workitem create --project ZDEV --summary Test", - "acli jira workitem create-bulk --file issues.json", - "acli jira workitem delete ZDEV-123", - "acli jira workitem edit ZDEV-123 --summary Test", - "acli jira workitem link create ZDEV-123 ZDEV-456", - "acli jira workitem link delete 10000", - "acli jira workitem transition --key ZDEV-123 --status Done", - "acli jira workitem unarchive ZDEV-123", - "acli jira workitem watcher remove ZDEV-123 --account-id abc", - "echo ok; acli jira sprint update 123 --name Sprint2", - ]; - - for (const command of writeCommands) { - assert.ok( - matchingDescriptions(command).includes("Jira write action via acli"), - command, - ); - } -}); - -test("write-style glab commands are flagged", () => { - const writeCommands = [ - "glab alias set co 'mr checkout'", - "glab auth login", - "glab ci cancel 123", - "glab ci delete 123", - "glab ci retry 123", - "glab ci run", - "glab ci trigger deploy", - "glab cluster agent bootstrap agent-name", - "glab cluster agent token revoke 1 2", - "glab config set editor vim", - "glab container-registry repository delete 123", - "glab deploy-key add ~/.ssh/id_rsa.pub", - "glab gpg-key delete 123", - "glab incident close 123", - "glab issue board create --name Board", - "glab issue close 123", - "glab issue create --title Test", - "glab issue delete 123", - "glab issue note 123 --message hi", - "glab issue reopen 123", - "glab issue subscribe 123", - "glab issue update 123 --title Test", - "glab label create bug", - "glab label delete bug", - "glab milestone create --title M1", - "glab mr approve 123", - "glab mr close 123", - "glab mr create --title Test", - "glab mr delete 123", - "glab mr merge 123", - "glab mr note create 123 --message hi", - "glab mr rebase 123", - "glab mr revoke 123", - "glab mr todo 123", - "glab mr update 123 --title Test", - "glab opentofu state lock prod", - "glab packages upload dist.tgz --name pkg --version 1.0.0", - "glab release create v1.0.0", - "glab release delete v1.0.0", - "glab release upload v1.0.0 dist.tgz", - "glab repo create test-project", - "glab repo delete test-project", - "glab repo fork zapier/zdev-cli", - "glab repo members add --username user", - "glab repo mirror 123", - "glab repo prune", - "glab repo remote add zapier/zdev-cli", - "glab repo transfer zapier/new-group", - "glab runner assign 123", - "glab runner delete 123", - "glab runner update 123", - "glab schedule create --cron '* * * * *'", - "glab schedule delete 123", - "glab schedule run 123", - "glab securefile create secret.txt ./secret.txt", - "glab securefile remove 123", - "glab skills install foo", - "glab snippet create -t Test file.txt", - "glab ssh-key add ~/.ssh/id_rsa.pub", - "glab ssh-key delete 123", - "glab stack save", - "glab stack sync", - "glab todo done 123", - "glab token create test-token", - "glab token revoke 123", - "glab variable delete FOO", - "glab variable set FOO bar", - "glab variable update FOO bar", - "glab work-items create --title Test", - "glab work-items delete 123", - "glab work-items update 123 --title Test", - "echo x && glab variable set FOO bar", - ]; - - for (const command of writeCommands) { - assert.ok( - matchingDescriptions(command).includes("GitLab write action via glab"), - command, - ); - } -}); - -test("glab api mutation-style commands are flagged", () => { - const apiWriteCommands = [ - "glab api projects/:fullpath/issues -f title=Test", - "glab api projects/:fullpath/issues --field title=Test", - "glab api projects/:fullpath/issues --raw-field title=Test", - "glab api projects/:fullpath/wikis/attachments --form file=@./image.png", - "glab api projects/:fullpath/issues --input body.json", - "glab api -X POST projects/:fullpath/issues", - "glab api --method=PUT projects/:fullpath/issues/1", - "glab api --method PATCH projects/:fullpath/protected_branches/main", - "glab api -X DELETE projects/:fullpath/issues/1", - "glab api graphql -f query='mutation { issueCreate(input:{}) { issue { id } } }'", - ]; - - for (const command of apiWriteCommands) { - assert.notDeepEqual(matchingDescriptions(command), [], command); - } -}); - -test("1Password CLI (op) is flagged, including after a pipe", () => { - const flagged = [ - "op read", - "op signin", - "echo x && op get item foo", - "true || op item delete xxx", - ]; - for (const command of flagged) { - assert.ok( - matchingDescriptions(command).some((d) => d.includes("1Password CLI")), - command, - ); - } - - // "op" inside other words must not trigger. - const benign = ['echo "stop now"', "open index.html", "git rebase --continue"]; - for (const command of benign) { - assert.deepEqual(matchingDescriptions(command), [], command); - } -}); - -test("generic destructive commands are flagged", () => { - const flagged = [ - "git push --force origin main", - "git push origin main -f", - "git push --force-with-lease origin main", - "npm publish", - "pnpm publish", - "yarn publish", - "DROP TABLE users;", - "DROP DATABASE prod;", - "terraform apply", - "terraform destroy", - "kubectl delete pod foo", - "docker system prune -af", - "aws s3 rm s3://bucket/key", - "aws ec2 terminate-instances --instance-ids i-123", - ]; - for (const command of flagged) { - assert.ok(matchingDescriptions(command).length > 0, command); - } -}); - -test("benign lookalikes are not flagged", () => { - const benign = [ - "git push origin main", - "git push origin feature/update", - "npm install", - "npm run build", - "terraform plan", - "terraform validate", - "kubectl get pods", - "kubectl describe pod foo", - "docker ps", - "docker system df", - "aws s3 ls", - "aws s3 cp x.txt s3://bucket/", - "aws ec2 describe-instances", - 'psql -c "SELECT 1"', - ]; - for (const command of benign) { - assert.deepEqual(matchingDescriptions(command), [], command); - } -}); diff --git a/agent/settings.json b/agent/settings.json index 56de090..ddf6f9e 100644 --- a/agent/settings.json +++ b/agent/settings.json @@ -5,7 +5,8 @@ "git:github.com/burneikis/pi-vim", "git:github.com/burneikis/pi-fzfp", "git:github.com/nicobailon/pi-web-access", - "git:github.com/nicobailon/pi-mcp-adapter" + "git:github.com/nicobailon/pi-mcp-adapter", + "git:https://github.com/czottmann/pi-automode" ], "shellCommandPrefix": "export PATH=\"/opt/homebrew/bin:/opt/homebrew/sbin:$PATH\"" } diff --git a/agent/sync-settings.sh b/agent/sync-settings.sh index 47f73d2..ef72fc9 100755 --- a/agent/sync-settings.sh +++ b/agent/sync-settings.sh @@ -2,17 +2,65 @@ set -euo pipefail -script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" -source_settings="$script_dir/settings.json" -target_settings="$HOME/.pi/agent/settings.json" +script_dir="${1:-$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)}" +target_dir="$HOME/.pi/agent" -mkdir -p "$(dirname "$target_settings")" +merge_settings() { + local name="$1" + local base="$script_dir/$name.json" + local local_settings="$target_dir/$name.local.json" + local target="$target_dir/$name.json" + local existing="$target" + local tmp -if [ ! -f "$target_settings" ]; then - cp "$source_settings" "$target_settings" - exit 0 -fi + mkdir -p "$(dirname "$target")" + tmp="$(mktemp "${target}.XXXXXX")" + if [ ! -f "$existing" ]; then + existing="$base" + fi + if [ -f "$local_settings" ]; then + jq -s ' + def unique_preserving: + reduce .[] as $item ([]; if index($item) == null then . + [$item] else . end); + def merge($base; $local): + if ($base | type) == "object" and ($local | type) == "object" then + reduce (((($base | keys_unsorted) + ($local | keys_unsorted)) | unique)[]) as $key + ({}; .[$key] = + if ($base | has($key)) and ($local | has($key)) then merge($base[$key]; $local[$key]) + elif $local | has($key) then $local[$key] + else $base[$key] + end) + elif ($base | type) == "array" and ($local | type) == "array" then + ($base + $local | unique_preserving) + else $local + end; + merge(merge(.[0]; .[1]); .[2]) + ' "$existing" "$base" "$local_settings" > "$tmp" + elif [ -f "$target" ]; then + jq -s ' + def unique_preserving: + reduce .[] as $item ([]; if index($item) == null then . + [$item] else . end); + def merge($existing; $base): + if ($existing | type) == "object" and ($base | type) == "object" then + reduce (((($existing | keys_unsorted) + ($base | keys_unsorted)) | unique)[]) as $key + ({}; .[$key] = + if ($existing | has($key)) and ($base | has($key)) then merge($existing[$key]; $base[$key]) + elif $base | has($key) then $base[$key] + else $existing[$key] + end) + elif ($existing | type) == "array" and ($base | type) == "array" then + ($existing + $base | unique_preserving) + else $base + end; + merge(.[0]; .[1]) + ' "$target" "$base" > "$tmp" + else + cp "$base" "$tmp" + fi + mv "$tmp" "$target" +} -tmp="$(mktemp)" -jq -s '.[0] * .[1]' "$target_settings" "$source_settings" > "$tmp" -mv "$tmp" "$target_settings" +mkdir -p "$target_dir" +merge_settings settings +merge_settings automode +merge_settings extensions/guardrails diff --git a/sync.sh b/sync.sh index 00d41f9..c635e15 100755 --- a/sync.sh +++ b/sync.sh @@ -25,8 +25,6 @@ function link_configs { mappings["agent/AGENTS.md"]="$HOME/.pi/agent/AGENTS.md" mappings["agent/extensions/notify.ts"]="$HOME/.pi/agent/extensions/notify.ts" mappings["agent/extensions/notify-focus.sh"]="$HOME/.pi/agent/extensions/notify-focus.sh" - mappings["agent/extensions/guardrails.json"]="$HOME/.pi/agent/extensions/guardrails.json" - mappings["agent/extensions/guardrails.test.mts"]="$HOME/.pi/agent/extensions/guardrails.test.mts" for key in "${!mappings[@]}"; do source="${key}"