@saifymatteo/pi-extensions 0.0.0-stage → 0.1.1
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/LICENSE +661 -0
- package/NOTICE.md +20 -0
- package/README.md +151 -2
- package/inventory.json +73 -0
- package/package.json +74 -4
- package/packages/pi-bg-bash/LICENSE +661 -0
- package/packages/pi-bg-bash/NOTICE.md +3 -0
- package/packages/pi-bg-bash/README.md +41 -0
- package/packages/pi-bg-bash/examples/settings.example.json +6 -0
- package/packages/pi-bg-bash/package.json +38 -0
- package/packages/pi-bg-bash/src/index.ts +969 -0
- package/packages/pi-bg-bash/src/lib/async-work.ts +27 -0
- package/packages/pi-bg-bash/test/bg-bash.test.mjs +34 -0
- package/packages/pi-claude-usage/LICENSE +661 -0
- package/packages/pi-claude-usage/NOTICE.md +3 -0
- package/packages/pi-claude-usage/README.md +27 -0
- package/packages/pi-claude-usage/package.json +35 -0
- package/packages/pi-claude-usage/src/index.ts +130 -0
- package/packages/pi-clipboard-image/LICENSE +661 -0
- package/packages/pi-clipboard-image/NOTICE.md +3 -0
- package/packages/pi-clipboard-image/README.md +27 -0
- package/packages/pi-clipboard-image/package.json +36 -0
- package/packages/pi-clipboard-image/src/index.ts +527 -0
- package/packages/pi-codeblock-borders/LICENSE +661 -0
- package/packages/pi-codeblock-borders/NOTICE.md +3 -0
- package/packages/pi-codeblock-borders/README.md +30 -0
- package/packages/pi-codeblock-borders/package.json +35 -0
- package/packages/pi-codeblock-borders/src/index.ts +302 -0
- package/packages/pi-codegraph/LICENSE +661 -0
- package/packages/pi-codegraph/NOTICE.md +5 -0
- package/packages/pi-codegraph/README.md +35 -0
- package/packages/pi-codegraph/examples/codegraph.example.json +4 -0
- package/packages/pi-codegraph/package.json +37 -0
- package/packages/pi-codegraph/src/index.ts +573 -0
- package/packages/pi-codegraph/test/codegraph.test.mjs +21 -0
- package/packages/pi-compact-tools/LICENSE +661 -0
- package/packages/pi-compact-tools/NOTICE.md +3 -0
- package/packages/pi-compact-tools/README.md +27 -0
- package/packages/pi-compact-tools/package.json +36 -0
- package/packages/pi-compact-tools/src/index.ts +1161 -0
- package/packages/pi-decisions/LICENSE +661 -0
- package/packages/pi-decisions/NOTICE.md +3 -0
- package/packages/pi-decisions/README.md +37 -0
- package/packages/pi-decisions/examples/service-models.example.json +27 -0
- package/packages/pi-decisions/package.json +37 -0
- package/packages/pi-decisions/src/index.ts +364 -0
- package/packages/pi-decisions/src/lib/service-models.ts +95 -0
- package/packages/pi-decisions/test/service-models.test.mjs +71 -0
- package/packages/pi-load-pi-dotenv/LICENSE +661 -0
- package/packages/pi-load-pi-dotenv/NOTICE.md +3 -0
- package/packages/pi-load-pi-dotenv/README.md +27 -0
- package/packages/pi-load-pi-dotenv/package.json +32 -0
- package/packages/pi-load-pi-dotenv/src/index.ts +66 -0
- package/packages/pi-model-go/LICENSE +661 -0
- package/packages/pi-model-go/NOTICE.md +5 -0
- package/packages/pi-model-go/README.md +33 -0
- package/packages/pi-model-go/package.json +36 -0
- package/packages/pi-model-go/src/index.ts +171 -0
- package/packages/pi-model-go/src/lib/dialog.ts +31 -0
- package/packages/pi-model-go/src/lib/model-go.ts +239 -0
- package/packages/pi-model-go/test/behavior.test.mjs +17 -0
- package/packages/pi-prestart-working/LICENSE +661 -0
- package/packages/pi-prestart-working/NOTICE.md +3 -0
- package/packages/pi-prestart-working/README.md +25 -0
- package/packages/pi-prestart-working/package.json +35 -0
- package/packages/pi-prestart-working/src/index.ts +86 -0
- package/packages/pi-prestart-working/test/behavior.test.mjs +56 -0
- package/packages/pi-root-guard/LICENSE +661 -0
- package/packages/pi-root-guard/NOTICE.md +3 -0
- package/packages/pi-root-guard/README.md +27 -0
- package/packages/pi-root-guard/package.json +32 -0
- package/packages/pi-root-guard/src/index.ts +202 -0
- package/packages/pi-session-finder/LICENSE +661 -0
- package/packages/pi-session-finder/NOTICE.md +3 -0
- package/packages/pi-session-finder/README.md +33 -0
- package/packages/pi-session-finder/package.json +36 -0
- package/packages/pi-session-finder/src/index.ts +306 -0
- package/packages/pi-session-finder/src/lib/dialog.ts +31 -0
- package/packages/pi-session-finder/test/behavior.test.mjs +18 -0
- package/packages/pi-session-id/LICENSE +661 -0
- package/packages/pi-session-id/NOTICE.md +3 -0
- package/packages/pi-session-id/README.md +31 -0
- package/packages/pi-session-id/package.json +35 -0
- package/packages/pi-session-id/src/index.ts +20 -0
- package/packages/pi-session-id/test/behavior.test.mjs +25 -0
- package/packages/pi-session-nav/LICENSE +661 -0
- package/packages/pi-session-nav/NOTICE.md +3 -0
- package/packages/pi-session-nav/README.md +32 -0
- package/packages/pi-session-nav/package.json +36 -0
- package/packages/pi-session-nav/src/index.ts +203 -0
- package/packages/pi-session-nav/src/lib/dialog.ts +31 -0
- package/packages/pi-session-nav/test/behavior.test.mjs +36 -0
- package/packages/pi-session-rename/LICENSE +661 -0
- package/packages/pi-session-rename/NOTICE.md +3 -0
- package/packages/pi-session-rename/README.md +31 -0
- package/packages/pi-session-rename/package.json +35 -0
- package/packages/pi-session-rename/src/index.ts +456 -0
- package/packages/pi-session-rename/test/behavior.test.mjs +10 -0
- package/packages/pi-session-retention/LICENSE +661 -0
- package/packages/pi-session-retention/NOTICE.md +3 -0
- package/packages/pi-session-retention/README.md +62 -0
- package/packages/pi-session-retention/package.json +36 -0
- package/packages/pi-session-retention/src/README.md +42 -0
- package/packages/pi-session-retention/src/core.ts +333 -0
- package/packages/pi-session-retention/src/dialog.ts +118 -0
- package/packages/pi-session-retention/src/index.ts +311 -0
- package/packages/pi-session-retention/src/lib/dialog.ts +31 -0
- package/packages/pi-session-retention/src/safety.ts +50 -0
- package/packages/pi-session-retention/test/behavior.test.mjs +42 -0
- package/packages/pi-skill-usage-log/LICENSE +661 -0
- package/packages/pi-skill-usage-log/NOTICE.md +3 -0
- package/packages/pi-skill-usage-log/README.md +32 -0
- package/packages/pi-skill-usage-log/package.json +36 -0
- package/packages/pi-skill-usage-log/src/index.ts +316 -0
- package/packages/pi-skill-usage-log/src/lib/dialog.ts +31 -0
- package/packages/pi-skill-usage-log/src/lib/usage-log.ts +62 -0
- package/packages/pi-skill-usage-log/test/behavior.test.mjs +34 -0
- package/packages/pi-tool-usage-log/LICENSE +661 -0
- package/packages/pi-tool-usage-log/NOTICE.md +5 -0
- package/packages/pi-tool-usage-log/README.md +32 -0
- package/packages/pi-tool-usage-log/package.json +36 -0
- package/packages/pi-tool-usage-log/src/index.ts +233 -0
- package/packages/pi-tool-usage-log/src/lib/dialog.ts +31 -0
- package/packages/pi-tool-usage-log/src/lib/usage-log.ts +62 -0
- package/packages/pi-tool-usage-log/src/skill-usage-log.ts +316 -0
- package/packages/pi-tool-usage-log/test/behavior.test.mjs +23 -0
- package/packages/pi-tts/LICENSE +661 -0
- package/packages/pi-tts/NOTICE.md +3 -0
- package/packages/pi-tts/README.md +37 -0
- package/packages/pi-tts/examples/service-models.example.json +27 -0
- package/packages/pi-tts/package.json +37 -0
- package/packages/pi-tts/src/index.ts +135 -0
- package/packages/pi-tts/src/lib/service-models.ts +95 -0
- package/packages/pi-tts/test/tts-output.test.mjs +47 -0
- package/packages/pi-tts/test/tts.test.mjs +57 -0
- package/packages/pi-user-image-previews/LICENSE +661 -0
- package/packages/pi-user-image-previews/NOTICE.md +3 -0
- package/packages/pi-user-image-previews/README.md +27 -0
- package/packages/pi-user-image-previews/package.json +36 -0
- package/packages/pi-user-image-previews/src/index.ts +168 -0
- package/packages/pi-vision/LICENSE +661 -0
- package/packages/pi-vision/NOTICE.md +5 -0
- package/packages/pi-vision/README.md +43 -0
- package/packages/pi-vision/examples/vision-toolkit.example.json +12 -0
- package/packages/pi-vision/package.json +38 -0
- package/packages/pi-vision/skills/vision-tools/LICENSE +21 -0
- package/packages/pi-vision/skills/vision-tools/SKILL.md +134 -0
- package/packages/pi-vision/skills/vision-tools/agents/openai.yaml +4 -0
- package/packages/pi-vision/skills/vision-tools/bin/crop +94 -0
- package/packages/pi-vision/skills/vision-tools/bin/detect +18 -0
- package/packages/pi-vision/skills/vision-tools/bin/glance +98 -0
- package/packages/pi-vision/skills/vision-tools/bin/ground +18 -0
- package/packages/pi-vision/skills/vision-tools/bin/trace +134 -0
- package/packages/pi-vision/skills/vision-tools/bin/vision-doctor +168 -0
- package/packages/pi-vision/skills/vision-tools/detect.py +56 -0
- package/packages/pi-vision/skills/vision-tools/ground.py +216 -0
- package/packages/pi-vision/skills/vision-tools/references/gui.md +93 -0
- package/packages/pi-vision/skills/vision-tools/references/long-screenshot-ocr.md +103 -0
- package/packages/pi-vision/skills/vision-tools/references/restore-graphic.md +92 -0
- package/packages/pi-vision/skills/vision-tools/references/restore-structure.md +47 -0
- package/packages/pi-vision/skills/vision-tools/references/restore-ui.md +200 -0
- package/packages/pi-vision/skills/vision-tools/scripts/dominant_colors.py +224 -0
- package/packages/pi-vision/skills/vision-tools/scripts/extract_fg.py +278 -0
- package/packages/pi-vision/skills/vision-tools/scripts/html_shot.py +111 -0
- package/packages/pi-vision/skills/vision-tools/scripts/long_screenshot_ocr.py +1245 -0
- package/packages/pi-vision/skills/vision-tools/scripts/pixel_diff.py +88 -0
- package/packages/pi-vision/skills/vision-tools/vision_client.py +581 -0
- package/packages/pi-vision/src/index.ts +728 -0
- package/packages/pi-vision/test/vision.test.mjs +30 -0
- package/packages/pi-wait/LICENSE +661 -0
- package/packages/pi-wait/NOTICE.md +3 -0
- package/packages/pi-wait/README.md +37 -0
- package/packages/pi-wait/package.json +37 -0
- package/packages/pi-wait/src/index.ts +88 -0
- package/packages/pi-wait/src/lib/async-work.ts +27 -0
- package/packages/pi-wait/test/wait.test.mjs +43 -0
- package/packages/pi-zvec/LICENSE +661 -0
- package/packages/pi-zvec/NOTICE.md +3 -0
- package/packages/pi-zvec/README.md +37 -0
- package/packages/pi-zvec/examples/zvec.example.json +5 -0
- package/packages/pi-zvec/package.json +37 -0
- package/packages/pi-zvec/src/index.ts +754 -0
- package/packages/pi-zvec/test/zvec.test.mjs +19 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Agent configuration root guard
|
|
2
|
+
|
|
3
|
+
Blocks selected write/edit and simple bash writes into Pi and Claude configuration roots.
|
|
4
|
+
|
|
5
|
+
## Try locally
|
|
6
|
+
|
|
7
|
+
From the repository root:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
pi -e ./packages/pi-root-guard
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
This uses your normal personal resources unless you isolate the agent home. Follow the [root README](../../README.md#isolated-sessions) to avoid loading an existing copy of the extension. Nothing is installed globally.
|
|
14
|
+
|
|
15
|
+
## Configuration and requirements
|
|
16
|
+
|
|
17
|
+
Uses PI_CODING_AGENT_DIR, CLAUDE_CONFIG_DIR, and optional PI_GUARDRAIL_TEMP. Allows managed Pi subdirectories (agents, extensions, skills, npm, git, temp) and selected Claude working directories.
|
|
18
|
+
|
|
19
|
+
## Compatibility and safety
|
|
20
|
+
|
|
21
|
+
A guardrail, NOT a filesystem sandbox. Shell parsing is heuristic; arbitrary subprocess writes, aliases/symlinks, and unhandled tools may bypass it. The preserved source also has false positives: `echo "not a redirection > settings.json"` inside a guarded root is treated as a possible write, despite the quoted `>`. Existing error handling is preserved. Use OS permissions/sandboxing for a security boundary.
|
|
22
|
+
|
|
23
|
+
Imported host APIs are declared as peer dependencies and supplied by the Pi host. A `*` peer range describes host resolution, not a guarantee of compatibility with every release. See root verification notes for what was actually exercised.
|
|
24
|
+
|
|
25
|
+
## Status
|
|
26
|
+
|
|
27
|
+
Part of the [pi-extensions](../../README.md) collection. No personal settings or credentials are included; see [NOTICE.md](NOTICE.md) for licensing and provenance.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@saifymatteo/pi-root-guard",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Blocks selected write/edit and simple bash writes into Pi and Claude configuration roots.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "AGPL-3.0-only",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"pi-package"
|
|
9
|
+
],
|
|
10
|
+
"files": [
|
|
11
|
+
"src",
|
|
12
|
+
"README.md",
|
|
13
|
+
"NOTICE.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"pi": {
|
|
17
|
+
"extensions": [
|
|
18
|
+
"./src/index.ts"
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=24"
|
|
23
|
+
},
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/saifymatteo/pi-extensions.git",
|
|
30
|
+
"directory": "packages/pi-root-guard"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-root-guard.ts — stop the agent from writing into the two agent config
|
|
3
|
+
* roots: the Pi root (PI_CODING_AGENT_DIR, default ~/.pi/agent) and the Claude
|
|
4
|
+
* Code root (CLAUDE_CONFIG_DIR, default ~/.claude). The Pi-root rule is what
|
|
5
|
+
* caused earlier research clutter (pkgextract.py, *.html, *.tgz,
|
|
6
|
+
* repo_tree.json, ...) in D:\Pi.
|
|
7
|
+
*
|
|
8
|
+
* Rules (these two roots only; project folders are not guarded):
|
|
9
|
+
* - File writes inside the Pi root are BLOCKED except under the managed
|
|
10
|
+
* agents/, extensions/, skills/, npm/, git/, and temp/ subdirectories.
|
|
11
|
+
* Refused with a reason directing the agent to a temp dir.
|
|
12
|
+
* - File writes inside the Claude root are BLOCKED except where Claude Code
|
|
13
|
+
* itself writes without asking (plans/, jobs/<id>/tmp/, memory *.md), so
|
|
14
|
+
* Pi can stand in for Claude Code as a harness. Claude Code prompts for
|
|
15
|
+
* the rest; here they are always refused (its dontAsk behaviour) — asking
|
|
16
|
+
* the user is pi-guardrails' job.
|
|
17
|
+
*
|
|
18
|
+
* Intercepts tool_call for `write`/`edit` (exact `path`) plus a conservative
|
|
19
|
+
* `bash` heuristic (redirects, tee, curl -o, cp/mv dst, git clone, tar -x -C).
|
|
20
|
+
* ponytail: bash parsing is heuristic, not a full shell parser — subprocess
|
|
21
|
+
* writes to arbitrary computed paths can slip through. The deterministic
|
|
22
|
+
* fix for those is agent temp-dir discipline, not a shell parser.
|
|
23
|
+
*
|
|
24
|
+
* Cwd independence: the roots are resolved to an ABSOLUTE path (env var expanded,
|
|
25
|
+
* tilde expanded, homedir fallback), so the guard protects the Pi root no
|
|
26
|
+
* matter where the session cwd is. Candidate paths get the same tilde/env
|
|
27
|
+
* expansion, and relative paths resolve against the session cwd (ctx.cwd)
|
|
28
|
+
* rather than the launcher's process.cwd().
|
|
29
|
+
*/
|
|
30
|
+
import { homedir } from 'node:os'
|
|
31
|
+
import { join, resolve } from 'node:path'
|
|
32
|
+
|
|
33
|
+
const norm = (p: string): string => p.replace(/\\/g, '/')
|
|
34
|
+
const lower = (p: string): string => norm(p).toLowerCase()
|
|
35
|
+
|
|
36
|
+
const isUnder = (p: string, dir: string): boolean =>
|
|
37
|
+
p === dir || p.startsWith(dir.endsWith('/') ? dir : dir + '/')
|
|
38
|
+
|
|
39
|
+
/** Normalize git-bash `/d/...` (and `D:\`) to drive form `D:/...`. */
|
|
40
|
+
const drivePath = (raw: string): string =>
|
|
41
|
+
raw.replace(/\\/g, '/').replace(/^\/([a-zA-Z])\//, (_m, c: string) => c.toUpperCase() + ':/')
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Expand `~/...` to the real home dir and defined environment variables
|
|
45
|
+
* ($VAR, ${VAR} — bash style) in a candidate path. Undefined variables are
|
|
46
|
+
* left untouched: this mirrors what bash would actually write, and keeps the
|
|
47
|
+
* guard conservative without inventing paths the shell would never produce.
|
|
48
|
+
*/
|
|
49
|
+
function expandPath(raw: string): string {
|
|
50
|
+
let p = raw
|
|
51
|
+
if (p === '~' || p.startsWith('~/') || p.startsWith('~\\')) {
|
|
52
|
+
p = join(homedir(), p.slice(1))
|
|
53
|
+
}
|
|
54
|
+
p = p.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g, (m, braced, plain) => {
|
|
55
|
+
const v = process.env[braced ?? plain]
|
|
56
|
+
return v === undefined ? m : v
|
|
57
|
+
})
|
|
58
|
+
return p
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Absolute, normalized root from an env override, else ~/<fallback...>. */
|
|
62
|
+
function absRoot(override: string | undefined, ...fallback: string[]): string {
|
|
63
|
+
const raw = override?.trim()
|
|
64
|
+
const r = drivePath(raw ? expandPath(raw) : join(homedir(), ...fallback))
|
|
65
|
+
return norm(resolve(homedir(), r)) // normalize absolute paths too; relative overrides anchor at home
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Absolute, normalized Pi root: PI_CODING_AGENT_DIR if set, else ~/.pi/agent. */
|
|
69
|
+
export function envRoot(): string {
|
|
70
|
+
return absRoot(process.env.PI_CODING_AGENT_DIR, '.pi', 'agent')
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Absolute, normalized Claude Code root: CLAUDE_CONFIG_DIR if set, else ~/.claude. */
|
|
74
|
+
export function claudeRoot(): string {
|
|
75
|
+
return absRoot(process.env.CLAUDE_CONFIG_DIR, '.claude')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const GUARD_ROOT = envRoot()
|
|
79
|
+
const CLAUDE_ROOT = claudeRoot()
|
|
80
|
+
const TEMP = process.env.PI_GUARDRAIL_TEMP || (GUARD_ROOT ? GUARD_ROOT + '/temp' : 'temp')
|
|
81
|
+
// temp/ is the guard's own escape hatch.
|
|
82
|
+
const PI_ALLOW = ['agents', 'extensions', 'skills', 'npm', 'git', 'temp']
|
|
83
|
+
|
|
84
|
+
// Claude root paths Claude Code writes without asking, copied from
|
|
85
|
+
// https://code.claude.com/docs/en/permission-modes#protected-paths (2026-10-26).
|
|
86
|
+
const CLAUDE_EXCEPTIONS = [
|
|
87
|
+
/^plans\//,
|
|
88
|
+
/^jobs\/[^/]+\/tmp\//,
|
|
89
|
+
/^projects\/[^/]+\/memory\/.+\.md$/,
|
|
90
|
+
/^agent-memory\/.+\.md$/,
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
/** true when an absolute path is inside the Claude root and not one of Claude Code's exceptions. */
|
|
94
|
+
export function protectedPath(absPath: string, root = CLAUDE_ROOT): boolean {
|
|
95
|
+
if (!root) return false
|
|
96
|
+
const a = lower(resolve(drivePath(absPath)))
|
|
97
|
+
const r = lower(resolve(drivePath(root)))
|
|
98
|
+
if (!isUnder(a, r)) return false
|
|
99
|
+
const rel = a.slice(r.length + 1)
|
|
100
|
+
return !CLAUDE_EXCEPTIONS.some((re) => re.test(rel))
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** true when an absolute path is inside the guarded root and NOT allowed. */
|
|
104
|
+
export function blocked(absPath: string, root = GUARD_ROOT, allow = PI_ALLOW): boolean {
|
|
105
|
+
if (!root) return false
|
|
106
|
+
const a = lower(resolve(drivePath(absPath)))
|
|
107
|
+
const r = lower(resolve(drivePath(root)))
|
|
108
|
+
if (!isUnder(a, r)) return false
|
|
109
|
+
return !allow.some((dir) => isUnder(a, lower(join(r, dir))))
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Resolve a possibly-relative path against a base and return abs path iff blocked. */
|
|
113
|
+
function blockedResolved(raw: string, base: string): string | null {
|
|
114
|
+
try {
|
|
115
|
+
const d = drivePath(expandPath(raw))
|
|
116
|
+
const abs = resolve(drivePath(base), d)
|
|
117
|
+
return blocked(abs) || protectedPath(abs) ? abs : null
|
|
118
|
+
} catch {
|
|
119
|
+
return null
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Cwd after a leading `cd <dir>`; quoted occurrences inside other commands do not count. */
|
|
124
|
+
function baseDir(cmd: string, fallback: string): string {
|
|
125
|
+
const cd = /^\s*cd\s+(?:--\s+)?(?:"([^"]+)"|'([^']+)'|(\S+))/.exec(cmd)
|
|
126
|
+
if (!cd) return fallback
|
|
127
|
+
try {
|
|
128
|
+
return resolve(drivePath(fallback), drivePath(expandPath(cd[1] ?? cd[2] ?? cd[3])))
|
|
129
|
+
} catch {
|
|
130
|
+
return fallback
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Scan simple commands in order; this remains a heuristic, not a shell parser. */
|
|
135
|
+
export function blockedBash(cmd: string, base: string = process.cwd()): string | null {
|
|
136
|
+
// Preserve quoted/escaped separators instead of treating them as command boundaries.
|
|
137
|
+
const commands = cmd.match(/(?:[^"';&|()\r\n\\]|\\[\s\S]|"(?:\\[\s\S]|[^"\\])*"|'[^']*')+/g) ?? []
|
|
138
|
+
let cwd = base
|
|
139
|
+
for (const command of commands) {
|
|
140
|
+
const ups: string[] = [] // candidate paths for this command's cwd only
|
|
141
|
+
const pushRaw = (r: string | undefined): void => {
|
|
142
|
+
if (r) ups.push(r)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Match >> greedily so its second > cannot become the captured destination.
|
|
146
|
+
const redir = /(?:^|\s)[12]?>>?\s*(?:"([^"]+)"|'([^']+)'|(\S+))/g
|
|
147
|
+
let m: RegExpExecArray | null
|
|
148
|
+
while ((m = redir.exec(command))) pushRaw(m[1] ?? m[2] ?? m[3])
|
|
149
|
+
|
|
150
|
+
// curl/wget -o/--output and curl -O (URL basename in cwd).
|
|
151
|
+
const curl = /(?:curl|wget)\s+(?:.*\s)?(?:-o|--output)\s+"?([^\s"]+)"?/gi
|
|
152
|
+
while ((m = curl.exec(command))) pushRaw(m[1])
|
|
153
|
+
if (/(?:curl|wget)\s+-O\b/i.test(command)) ups.push('.')
|
|
154
|
+
|
|
155
|
+
// Destinations are the last argument of this command, not of the whole script.
|
|
156
|
+
const lastArg = (verbs: RegExp): string | null => {
|
|
157
|
+
const idx = command.search(verbs)
|
|
158
|
+
if (idx < 0) return null
|
|
159
|
+
const args = command.slice(idx).match(/(?:"([^"]+)"|'([^']+)'|(\S+))/g) ?? []
|
|
160
|
+
return args.length ? args[args.length - 1].replace(/^["']|["']$/g, '') : null
|
|
161
|
+
}
|
|
162
|
+
pushRaw(lastArg(/\bcp\s+/) ?? '')
|
|
163
|
+
pushRaw(lastArg(/\bmv\s+/) ?? '')
|
|
164
|
+
pushRaw(lastArg(/\btee\s+/) ?? '')
|
|
165
|
+
pushRaw(lastArg(/\bgit\s+clone\s+/) ?? '')
|
|
166
|
+
pushRaw(lastArg(/\btar\s+.*\s+-C\s+/) ?? '')
|
|
167
|
+
|
|
168
|
+
for (const u of ups) {
|
|
169
|
+
const hit = blockedResolved(u, cwd)
|
|
170
|
+
if (hit) return hit
|
|
171
|
+
}
|
|
172
|
+
// Redirections occur before cd changes cwd; following commands use the new cwd.
|
|
173
|
+
cwd = baseDir(command, cwd)
|
|
174
|
+
}
|
|
175
|
+
return null
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export default function register(pi: any): void {
|
|
179
|
+
const piReason = `writes to the Pi root are blocked by the pi-root-guard guardrail — put work artifacts in the temp dir (${TEMP}) instead.`
|
|
180
|
+
const claudeReason = `writes to the Claude root (${CLAUDE_ROOT}) are blocked by the pi-root-guard guardrail — ask the user to make this change.`
|
|
181
|
+
const reason = (hit: string): string => `[pi-root-guard] ${blocked(hit) ? piReason : claudeReason}`
|
|
182
|
+
pi.on('tool_call', (event: any, ctx: any) => {
|
|
183
|
+
try {
|
|
184
|
+
const t = event?.toolName
|
|
185
|
+
const inp = event?.input ?? {}
|
|
186
|
+
// Session cwd (tools execute there) with process.cwd() as a fallback.
|
|
187
|
+
const sessionCwd: string =
|
|
188
|
+
ctx && typeof ctx.cwd === 'string' && ctx.cwd ? ctx.cwd : process.cwd()
|
|
189
|
+
if (t === 'write' || t === 'edit') {
|
|
190
|
+
const hit = typeof inp.path === 'string' ? blockedResolved(inp.path, sessionCwd) : null
|
|
191
|
+
if (hit) return { block: true, reason: `${reason(hit)} (blocked path: ${hit})` }
|
|
192
|
+
} else if (t === 'bash') {
|
|
193
|
+
const cmd: string = typeof inp.command === 'string' ? inp.command : ''
|
|
194
|
+
if (!cmd) return
|
|
195
|
+
const hit = blockedBash(cmd, sessionCwd)
|
|
196
|
+
if (hit) return { block: true, reason: `${reason(hit)} (blocked path: ${hit}, cmd: ${cmd.slice(0, 120)})` }
|
|
197
|
+
}
|
|
198
|
+
} catch {
|
|
199
|
+
/* never break a turn over the guard itself */
|
|
200
|
+
}
|
|
201
|
+
})
|
|
202
|
+
}
|