[READ-ONLY] Mirror of https://github.com/andrioid/pi-fence. Fence wrapper for Pi coding agent. Wraps bash calls and intercepts reads/writes according to fence config
0

Configure Feed

Select the types of activity you want to include in your feed.

Initial commit — pi-fence v0.1.0

Single-config sandbox extension for Pi, enforcing one shared
fence.jsonc across two layers:

1. Bash subprocesses, wrapped via the `fence` binary
(Seatbelt on macOS, bubblewrap+Landlock on Linux).
2. Pi's own file tools (read/write/edit/grep/find/ls), gated
by an in-process tool_call interceptor.

Both layers consume the same resolved config from `fence config show`
so they cannot disagree.

Features:
- Bash + user_bash wrapping with --no-fence escape hatch
- Footer status state machine (PLAN.md §12)
- Structured block payload back to the LLM (PLAN.md §6)
- Interactive grant prompt: [once / session / persist / deny]
with default-deny, realpath shown first, 500ms anti-fatfinger,
persist 2nd-confirmation
- Session grants persisted via pi.appendEntry, restored on
fork/resume
- Persist-to-disk grants edit-or-scaffold the project-root
fence.jsonc; jsonc-parser preserves comments, ajv 2020-12
validates, atomic write via tmp+rename, last-3 .bak rotation
- Mandatory denyWrite floor (PLAN.md §11) for ~/.pi/agent/{extensions,
skills,prompts}/**, ~/.config/fence/**, audit log, install dir
- Audit log at ~/.pi/agent/pi-fence.log with daily rotation,
7-day retention
- Per-turn rate limiter (3+ denials → batch confirm)
- Footgun lint: cwd===$HOME, secret canaries, top-level **,
brace expansion, bare ** segments
- Slash commands: /fence, /fence reload, /fence init, /fence log

See PLAN.md for the build contract and design decisions.
See README.md for usage and trade-offs.

author
Andri Óskarsson
date (May 10, 2026, 4:18 PM +0200) commit 1bc9faee
+7709
+18
.gitignore
··· 1 + node_modules/ 2 + 3 + # tsc smoke-test scratch dir (used during development) 4 + .smoke-build/ 5 + 6 + # fence.jsonc backups created by the persist flow 7 + fence.jsonc.bak.* 8 + fence.json.bak.* 9 + 10 + # editor + OS noise 11 + .DS_Store 12 + .idea/ 13 + .vscode/ 14 + *.swp 15 + *.swo 16 + 17 + # logs 18 + *.log
+820
PLAN.md
··· 1 + # pi-fence — design and build plan 2 + 3 + Status snapshot: 4 + 5 + - **Pass 0 (shipped):** bash + user_bash wrapping via the `fence` binary, 6 + startup detection, footer status, `--no-fence` flag. 7 + - **Pass 1 (shipped):** FS-policy layer driven by `fence.jsonc`, 8 + structured block messages to the LLM, `/fence` / `/fence reload` / 9 + `/fence init` commands, footgun lint, footer state machine, 10 + mandatory denyWrite floor (PLAN §11) on the Pi-tool layer. 11 + - **Pass 2 (shipped):** interactive grant prompt with default-deny + 12 + realpath display + 500ms anti-fatfinger + persist 2nd-confirm; 13 + session grants via `pi.appendEntry` (restored on fork/resume); 14 + persist-to-disk via jsonc-parser modify + ajv validate + atomic 15 + rename + 3-deep .bak rotation; audit log at `~/.pi/agent/pi-fence.log` 16 + with daily rotation + 7-day retention; per-turn rate limiter with 17 + batch confirm at N=3; `/fence log [N]` slash command. 18 + 19 + This document is the contract. If anything below is unclear or feels wrong 20 + when you pick this up, push back before writing code. 21 + 22 + --- 23 + 24 + ## 1. One-line description 25 + 26 + pi-fence is a single Pi extension that enforces one shared `fence.jsonc` 27 + policy across two surfaces: 28 + 29 + 1. **Bash subprocesses** — wrapped via the `fence` binary (kernel-level 30 + sandbox: Seatbelt on macOS, bubblewrap + Landlock on Linux). 31 + 2. **Pi's own file tools** (`read`, `write`, `edit`, `grep`, `find`, 32 + `ls`) — gated by an in-process `tool_call` interceptor that reads the 33 + same fence config. 34 + 35 + The two layers cannot disagree because they consume the *same resolved 36 + config* via `fence config show`. 37 + 38 + ## 2. Scope 39 + 40 + **In scope:** 41 + - Bash + user_bash → fence binary 42 + - Built-in file tools (`read`, `write`, `edit`, `grep`, `find`, `ls`) 43 + - `fence.jsonc` resolution + caching 44 + - Structured block messages to the LLM 45 + - Interactive grant prompts (TUI only) 46 + - Session-scoped grants via `pi.appendEntry` 47 + - Persist-to-disk grants (project-root file only, schema-validated) 48 + - Audit log with daily rotation 49 + - `/fence`, `/fence log`, `/fence reload`, `/fence init` commands 50 + - One-shot lint warnings for footgun configs 51 + 52 + **Out of scope (document loudly in README):** 53 + - MCP server tools (neither bash subprocess nor built-in tool — invisible 54 + to both layers) 55 + - Pi-internal reads (session storage, AGENTS.md, image attachments, 56 + extension files) — these are not `tool_call` events 57 + - Pi's own network calls (LLM API requests, etc.) 58 + - A proactive `request_permission` tool — revisit in v2 if the 59 + deny-then-grant loop is too friction-heavy 60 + - Wrapping non-bash extensions or remote-tool plugins 61 + 62 + ## 3. Locked design decisions 63 + 64 + | Question | Decision | 65 + |---|---| 66 + | Reads default | Mirror fence: default-allow unless `defaultDenyRead: true`. Recommended starter is `extends: "code"` (per fence's own agents.md). | 67 + | Layer alignment under `defaultDenyRead` | Pi-tool layer stays strict (no implicit system-path allowlist). Users who want both layers symmetric add `strictDenyRead: true`. We do **not** auto-mirror fence's implicit defaults — there's no documented API for them today (only `-d` debug-output parsing of Seatbelt/Landlock profiles, which isn't a stable contract). See PLAN §Layer alignment. | 68 + | Glob library | `picomatch` strict mode | 69 + | Fence binary missing | Symmetric fail-open: no enforcement on either side, one warning | 70 + | First run, no `fence.jsonc` | Notify, run unfenced, suggest `/fence init` | 71 + | Headless mode (`!ctx.hasUI`) | No prompts; deny is final; every block hits the audit log | 72 + | Block UX for the LLM | Structured payload (rule, path, suggestion) — see §6 | 73 + | Interactive grants | Yes, with `[once] [session] [persist] [deny]` | 74 + | Session grant storage | `pi.appendEntry` (survives fork/resume; cleared on `/new`) | 75 + | Persist target | **Project-root file only** (`<repo-root>/fence.jsonc`) — see §7 | 76 + | Persist format | `jsonc-parser` for editing, `ajv` to validate against vendored schema | 77 + | Audit log | `~/.pi/agent/pi-fence.log`, daily rotation, 7-day retention | 78 + | Lint warnings | Warn-only, never block — cwd === `$HOME`, broad globs, etc. | 79 + | MCP support | Out of scope, document the gap | 80 + | Proactive permission tool | Skip v1 | 81 + | Glob case sensitivity | Auto-detected from cwd's filesystem; `nocase: true` on macOS APFS by default | 82 + | Brace expansion in globs | Disabled (`nobrace: true`) — fence's matcher doesn't support it; lint warns on braces in patterns | 83 + | Mandatory denies | Non-overridable `denyWrite` for `~/.pi/**`, `~/.agents/skills/**`, `~/.config/fence/**`, audit log — see §11 | 84 + | Grant prompt default | `[deny]` (not `[once]`); shows realpath; `[persist]` requires a second confirmation; 500ms anti-fatfinger delay | 85 + 86 + ## 4. Architecture 87 + 88 + ``` 89 + pi-fence/ 90 + ├── index.ts ← entry; wires layers, owns footer status, registers /fence 91 + ├── bash.ts ← bash + user_bash wrappers (mostly unchanged from pass 0) 92 + ├── policy/ 93 + │ ├── resolve.ts ← `fence config show` + cache + chain parsing 94 + │ ├── match.ts ← picomatch wrapper + realpath-deepest-existing-ancestor 95 + │ ├── decide.ts ← block/allow logic, returns structured decision 96 + │ └── lint.ts ← one-shot footgun warnings 97 + ├── grants/ 98 + │ ├── prompt.ts ← ctx.ui.confirm dialog (TUI only) 99 + │ ├── store.ts ← session-scoped grants via pi.appendEntry 100 + │ └── persist.ts ← project-root edit-or-scaffold + ajv validation 101 + ├── audit.ts ← append + rotate 102 + ├── schema/ 103 + │ └── fence.schema.json ← vendored from upstream 104 + ├── package.json 105 + ├── tsconfig.json 106 + ├── PLAN.md (this file) 107 + └── README.md 108 + ``` 109 + 110 + ### Data flow on a tool_call 111 + 112 + ``` 113 + LLM calls read({ path }) 114 + 115 + tool_call event handler 116 + 117 + policy/match.ts: resolve → realpath → check against cached policy 118 + 119 + policy/decide.ts: returns 120 + { allow: true } | { block: true, rule, path, suggestion } 121 + 122 + on block: 123 + - audit.ts: append log entry 124 + - if TUI present: 125 + grants/prompt.ts: ask user [once / session / persist / deny] 126 + on grant: register in grants/store.ts (and grants/persist.ts if persist) 127 + on grant: re-emit allow, tool proceeds 128 + - else: 129 + return structured error to LLM 130 + ``` 131 + 132 + ## 5. Pass 1 (core) — implementation order 133 + 134 + Build in this order so each step is independently verifiable. 135 + 136 + 1. **`policy/resolve.ts`** 137 + - `execFile("fence", ["config", "show"], { cwd })` 138 + - Parse stdout JSON (resolved config) 139 + - Parse stderr lines for the resolution chain (file paths fence loaded) 140 + - **Detect case-insensitive filesystem:** `fs.statSync(cwd.toLowerCase())` 141 + and `.toUpperCase()` and compare inodes; if they match, we're on a 142 + case-insensitive FS (typical macOS APFS, Windows NTFS). Cache the 143 + boolean alongside the resolved policy. 144 + - **Apply mandatory denies (§11)** by augmenting the resolved policy's 145 + `filesystem.denyWrite` array. These are non-overridable — even if the 146 + user explicitly adds the same path to `allowWrite`, our deny stays. 147 + - Return `{ resolved: FenceConfig, chain: string[], available: boolean, 148 + caseInsensitive: boolean }` 149 + - Cache per-session in module state; refresh in `session_start` and on `/fence reload` 150 + - Handle: fence missing (return `{ available: false }`), invalid JSON, timeout 151 + 152 + 2. **`policy/match.ts`** 153 + - `canonicalize(rawPath, cwd)` — `path.resolve` then realpath the deepest 154 + existing ancestor (same logic we used in the deleted andridk-sandbox) 155 + - `matchesAny(absPath, patterns[], { caseInsensitive })` — picomatch with 156 + `{ dot: true, nocase: caseInsensitive, strictSlashes: true, 157 + nobrace: true }`. We disable brace expansion because fence's Go matcher 158 + doesn't support it; allowing it here would create silent disagreement 159 + between our Pi-tool layer and the bash layer. 160 + - Document picomatch ↔ Go filepath.Match divergences in README. Known: 161 + character classes, escape sequences, the literal-`{`/`}` interpretation. 162 + 163 + 3. **`policy/decide.ts`** 164 + - Pure function: `(toolName, absPath, policy) → Decision` 165 + - Read tools (`read`, `grep`, `find`, `ls`): 166 + - If `defaultDenyRead && !matchesAny(allowRead)`: block 167 + - If `matchesAny(denyRead)`: block (overrides allowRead) 168 + - Else: allow 169 + - Write tools (`write`, `edit`): 170 + - If `!matchesAny(allowWrite)`: block (writes are default-deny) 171 + - If `matchesAny(denyWrite)`: block 172 + - Else: allow 173 + - Decision shape: `{ allow: true } | { block: true, rule: string, 174 + path: string, suggestion: string }` 175 + 176 + 4. **`tool_call` wiring in `index.ts`** 177 + - Hook `tool_call` for the six file tools 178 + - Resolve path → decide → on block, return `{ block: true, reason }` with 179 + a serialized structured payload as the reason 180 + - Fence missing or no config: skip enforcement, log nothing 181 + 182 + 5. **`/fence` and `/fence reload`** 183 + - `/fence`: pretty-print resolved policy with per-field "applies to: 184 + bash / fs / both" annotations 185 + - `/fence reload`: re-call resolve, refresh status footer 186 + 187 + 6. **`policy/lint.ts`** 188 + - Run once at session_start after resolve 189 + - Warn if cwd === `$HOME` 190 + - Warn if any pattern in `allowRead`/`allowWrite` matches canary paths 191 + (`/etc/passwd`, `~/.ssh/id_rsa`, `~/.aws/credentials`) 192 + - Warn via `ctx.ui.notify` once per session 193 + 194 + **Verification gate for pass 1:** with a `fence.jsonc` containing 195 + `defaultDenyRead: true, allowRead: ["."]`, `read("/etc/passwd")` is blocked 196 + with a structured error, `read("./README.md")` works, and `bash("cat 197 + /etc/passwd")` is also blocked (by the existing fence binary). 198 + 199 + ## 6. Structured block message format 200 + 201 + The `reason` string we return from `tool_call`'s `{ block: true, reason }` 202 + should be valid JSON the LLM can parse, but readable as text if it doesn't. 203 + Format: 204 + 205 + ``` 206 + pi-fence: blocked 207 + { 208 + "tool": "read", 209 + "path": "/Users/andri/.ssh/id_rsa", 210 + "matched_rule": "filesystem.defaultDenyRead", 211 + "matched_pattern": null, 212 + "policy_source": "/Users/andri/repo/fence.jsonc", 213 + "suggestion": "If this access is legitimate, ask the user to grant it. They can use /fence or add the path to filesystem.allowRead in fence.jsonc." 214 + } 215 + ``` 216 + 217 + Rationale: 218 + - Leading human prefix so terse error rendering still makes sense 219 + - JSON body so the agent can self-correct or surface a clean ask to the user 220 + - `matched_pattern` filled when a deny rule matched (e.g. 221 + `denyRead: ["~/.ssh/**"]`); null when the implicit default-deny fired 222 + - `policy_source` points at the file the user should edit 223 + 224 + ## 7. Persist target — project root only 225 + 226 + ### Locating project root 227 + 228 + 1. `git rev-parse --show-toplevel` from cwd. If success → that's root. 229 + 2. Else: cwd itself. 230 + 231 + We do **not** walk upward looking for an existing `fence.jsonc`. Project 232 + root is a stable, predictable target. 233 + 234 + ### Edit target 235 + 236 + - `<root>/fence.jsonc` if present 237 + - else `<root>/fence.json` if present 238 + - else *proposed* `<root>/fence.jsonc` (does not yet exist) 239 + 240 + ### Persist flow — three branches 241 + 242 + **A) File exists at project root.** 243 + 244 + ``` 245 + 1. Read current contents (record mtime). 246 + 2. Parse via jsonc-parser → CST (preserves comments, formatting). 247 + 3. modify({ filesystem.allowRead | filesystem.allowWrite }, append op). 248 + 4. Apply edits, get new text. 249 + 5. JSON.parse the new text and validate against vendored schema (ajv). 250 + 6. mtime check: if file changed on disk since step 1, abort with a clean 251 + error. Tell user to retry. 252 + 7. Write to <file>.tmp, fsync, rename. 253 + 8. Backup: copy original to <file>.bak.<unix-ts> before rename. Keep last 254 + 3 backups, delete older. 255 + ``` 256 + 257 + **B) File missing at project root.** 258 + 259 + Confirm with user: 260 + ``` 261 + pi-fence: no fence.jsonc found in <project-root>. 262 + Create one with this grant? 263 + [y] yes, create <root>/fence.jsonc 264 + [n] no, just allow for the session 265 + ``` 266 + 267 + If yes, scaffold: 268 + ```jsonc 269 + { 270 + "$schema": "https://raw.githubusercontent.com/Use-Tusk/fence/main/docs/schema/fence.schema.json", 271 + "extends": "<inherit>", 272 + "filesystem": { 273 + "allowRead": ["<the-path>"] 274 + } 275 + } 276 + ``` 277 + 278 + `<inherit>` selection: 279 + - `"@base"` if any non-project fence config exists in the resolved chain 280 + (e.g., `~/.config/fence/fence.jsonc`). Layers project file on top of 281 + user's global config — fence's own documented pattern. 282 + - `"code"` if no global config exists. Sensible default for coding agents. 283 + 284 + Validate the scaffolded JSON against the schema, then atomic-write. 285 + 286 + **C) File only exists outside project root** (e.g., only at 287 + `~/.config/fence/`). 288 + 289 + Treated as case (B). We do **not** edit the global file. The scaffolded 290 + project-root file uses `extends: "@base"` to inherit the global. Notify 291 + user: 292 + 293 + ``` 294 + pi-fence: persist requires a project-local fence.jsonc. 295 + Your existing config at <path> will be inherited via `extends: "@base"`. 296 + Create <root>/fence.jsonc? 297 + ``` 298 + 299 + ### Why these rules 300 + 301 + - **Never touch `~/.config/fence/`.** Per-user global affects every 302 + project; pi-fence shouldn't silently mutate it. 303 + - **Never edit nested fence files in subdirectories.** Power-user feature; 304 + out of pi-fence's lane. 305 + - **git-aware project root.** Doesn't shift when you `cd src/` mid-session. 306 + 307 + ### What "user-owned" was, and why it dissolved 308 + 309 + Earlier draft included a path classifier to skip fence's built-in 310 + templates (`code`, `code-strict`, etc.) and fence install-dir paths. With 311 + project-root-only, we don't need it: the only path we ever write is 312 + `<root>/fence.jsonc`, which is by definition user territory. 313 + 314 + Remaining edge cases the persist code still handles: 315 + - File exists but unwritable (EACCES) → clean error 316 + - File is a symlink → realpath; refuse if target is in fence install dir 317 + or `/etc` (paranoia, cheap) 318 + - File is malformed JSONC → don't try to edit; tell user to fix manually 319 + - File is valid JSONC but fails schema *before our edit* → still attempt 320 + the edit; abort only if our edit produces invalid output. We're not 321 + the linter for existing config. 322 + 323 + ## 8. Grants — semantics and storage 324 + 325 + ### Grant flow on block (TUI present) 326 + 327 + ``` 328 + ┌─ pi-fence: read denied ──────────────────────────────────────┐ 329 + │ Path: /Users/andri/other-repo/package.json │ 330 + │ Tool: read │ 331 + │ Rule: filesystem.defaultDenyRead (no allowRead match) │ 332 + │ │ 333 + │ [a] allow once [s] session [p] persist [d] deny │ 334 + └──────────────────────────────────────────────────────────────┘ 335 + ``` 336 + 337 + **Mandatory safety properties of the prompt:** 338 + 339 + - **Default-focused button is `[d] deny`.** A user who reflexively hits 340 + Enter denies the access. Intentional friction. 341 + - **Resolved (realpath) path is shown first**, requested path shown as 342 + secondary. Defends against an agent that does 343 + `ln -s ~/.ssh ./innocent` then `read("./innocent/id_rsa")` — the prompt 344 + makes the actual target visible. 345 + - **500 ms anti-fatfinger delay** before any keypress is accepted. 346 + Prevents a chain of denials from being grant-bombed by a single keypress. 347 + - **`[p] persist` requires a second confirmation step.** After picking 348 + persist, show a separate confirm: `Persist <realpath> to 349 + <root>/fence.jsonc? [y/N]`. The persist write happens only on explicit 350 + `y`. Never combine "grant for this call" with "write to disk" in one 351 + keystroke. 352 + 353 + **Action semantics:** 354 + 355 + - **`[a] allow once`** — this single tool call only; no record kept 356 + - **`[s] session`** — store grant via 357 + `pi.appendEntry("pi-fence-grant", {...})`; applies for the rest of the 358 + session; survives fork/resume; cleared on `/new` 359 + - **`[p] persist`** — session grant **plus** edit-or-scaffold 360 + `<root>/fence.jsonc` (see §7), **only after** the second confirmation 361 + - **`[d] deny`** — return the structured error to the LLM; no grant 362 + 363 + ### Grant matching 364 + 365 + A grant is keyed by `{ tool: "read"|"write", path: <realpath> }`. Grants 366 + match exactly (no globs); each unique blocked path gets its own grant. 367 + Rationale: keeps the grant list short, predictable, easy to revoke. 368 + 369 + ### Grant lookup order on each tool_call 370 + 371 + 1. Check session grants (in-memory, sourced from `pi.appendEntry` history) 372 + 2. If no match, run `policy/decide.ts` 373 + 374 + Persisted grants live in `fence.jsonc` and flow back through the 375 + `fence config show` resolver, so they don't need separate runtime tracking. 376 + 377 + ### Rate-limiting prompts 378 + 379 + To avoid prompt-fatigue exploitation by an aggressive agent: 380 + 381 + - Track `{ deniedPathsThisTurn: Set<string> }`. 382 + - On the **N**th distinct denial in a single agent turn (start with N=3), 383 + collapse the prompt to a single batch confirm: "Agent has been denied 3+ 384 + paths this turn. Allow individually, allow all once, or deny all?" 385 + - Reset the counter at `turn_end`. 386 + 387 + ### Headless mode 388 + 389 + `ctx.hasUI === false` → no prompts. Every block returns the structured 390 + error directly to the LLM and appends to the audit log. Operators 391 + configure policy via `fence.jsonc` upfront. Matches fence binary's own 392 + ethos. 393 + 394 + ## 9. Audit log 395 + 396 + - Path: `~/.pi/agent/pi-fence.log` 397 + - Format: one JSON object per line (newline-delimited) 398 + - Schema: 399 + ```jsonc 400 + { 401 + "ts": "2026-05-10T14:30:01.123Z", 402 + "session": "<session-id>", 403 + "decision": "block" | "grant-once" | "grant-session" | "grant-persist", 404 + "tool": "read", 405 + "path": "/Users/andri/.ssh/id_rsa", 406 + "rule": "filesystem.defaultDenyRead", 407 + "pattern": null 408 + } 409 + ``` 410 + - Rotation: daily. On the first append of a UTC day, if 411 + `pi-fence.log` exists, rename to `pi-fence.log.YYYY-MM-DD`. 412 + Delete files older than 7 days. 413 + - `/fence log [N]`: tail last N lines (default 20), pretty-printed. 414 + - Bash-side denials are **not** in this log (fence has its own monitor 415 + mode, `-m`). Document this in the README. 416 + 417 + ## 10. Footgun lint (one-shot at session_start) 418 + 419 + Probe the resolved policy with canary paths: 420 + 421 + | Canary | Warn if allowed | 422 + |---|---| 423 + | `/etc/passwd` | `allowRead` matches | 424 + | `~/.ssh/id_rsa` | `allowRead` matches | 425 + | `~/.aws/credentials` | `allowRead` matches | 426 + | `~/.bash_history` | `allowRead` matches | 427 + | `/etc/hosts` | `allowWrite` matches | 428 + | `~/.bashrc`, `~/.zshrc` | `allowWrite` matches | 429 + 430 + Plus: 431 + 432 + - Warn if `cwd === os.homedir()` ("workspace is your home directory; fence 433 + policies will be very permissive — consider running pi from a project 434 + subdirectory") 435 + - Warn on `**` at top level of `allowRead`/`allowWrite` (matches everything) 436 + - Warn on **brace expansion** in any pattern (e.g., 437 + `~/.{ssh,aws,gnupg}/**`). picomatch supports braces, fence's Go matcher 438 + doesn't. Without this warning the user thinks they've denied three 439 + paths but only the Pi-tool layer is actually denying them; bash sees no 440 + rule. Suggest expanding to separate entries. 441 + - Warn if any pattern contains a literal `**` segment without slashes 442 + around it (often a typo for `*`). 443 + 444 + Each warning fires once per session via `ctx.ui.notify` at "warning" 445 + level. Never blocks. 446 + 447 + ## 11. Mandatory denies (non-overridable) 448 + 449 + Some paths are dangerous to write regardless of user policy because 450 + writing to them gives the agent **persistent influence over future Pi 451 + sessions** — effectively, persistence + arbitrary code execution. 452 + The resolver augments every loaded policy with these denies, and they 453 + cannot be overridden by the user's `allowWrite` rules. 454 + 455 + ``` 456 + filesystem.denyWrite (mandatory, applied last after user rules): 457 + - ~/.pi/agent/extensions/** # extension code is loaded as TS at startup 458 + - ~/.pi/agent/skills/** # skills become part of the system prompt 459 + - ~/.pi/agent/prompts/** # prompt templates same effect 460 + - ~/.agents/skills/** # alternate skills location 461 + - ~/.config/fence/** # editing global fence config silently broadens policy 462 + - ~/.pi/agent/pi-fence.log* # audit log integrity 463 + - <ext-install-dir>/** # any path inside the running pi-fence install 464 + ``` 465 + 466 + Rationale by entry: 467 + 468 + - **Pi extensions / skills / prompts.** Pi auto-loads any TS file under 469 + `~/.pi/agent/extensions/` at session start, and any markdown / JSON 470 + under `skills/` and `prompts/` is folded into future system prompts. 471 + An agent that can write here owns every future Pi session via 472 + prompt-injection or arbitrary code execution, regardless of 473 + `fence.jsonc`. This is the highest-severity escalation path in the 474 + whole system. 475 + - **Global fence config.** Writing to `~/.config/fence/fence.jsonc` 476 + silently broadens policy for **every project on the machine**. The 477 + agent could grant itself global allowRead/allowWrite without the user 478 + ever seeing a prompt. 479 + - **Audit log.** If the agent can rewrite 480 + `~/.pi/agent/pi-fence.log`, post-incident forensics is gone. 481 + Append-only on disk is a different mechanism (chattr +a / extended 482 + attributes) and out of scope; this denyWrite is the practical floor. 483 + - **pi-fence's own install dir.** Belt-and-braces protection against an 484 + agent rewriting its own enforcement code mid-session. 485 + 486 + Applies on **both layers**: 487 + 488 + - **Pi-tool layer:** `policy/decide.ts` checks the mandatory deny list 489 + before any user-configured rules. A match returns block with rule = 490 + `pi-fence.mandatoryDenyWrite`. This is the layer we control fully. 491 + - **Bash layer:** these paths are appended to fence's `denyWrite` via the 492 + resolved config we hand off to the binary at startup time. Caveat: we 493 + can only ensure this for the bash side if we can mutate the resolved 494 + config that fence enforces. If `fence config show` is read-only and 495 + fence doesn't accept a runtime augmentation, bash-side mandatory 496 + denies require **either**: 497 + - emitting a project-root scaffold with these denies pre-populated, or 498 + - shelling out as `fence -- bash -c <cmd>` with `--deny-write` flags 499 + if/when fence supports them (currently it does not — needs upstream 500 + feature request or our own pre-flight wrapper). 501 + 502 + For v1, document the bash-side limitation clearly and ensure the 503 + Pi-tool layer always enforces. The skills/extensions deny is the most 504 + critical and *that one* we control fully because it's a Pi-tool-layer 505 + concern (Pi loads those via Pi's own Node, not via bash). 506 + 507 + ### What about `~/.pi/agent/sessions/`? 508 + 509 + Not in the mandatory deny set. Session storage is sensitive 510 + (conversation history) but writing there doesn't grant code execution 511 + or prompt-injection over future sessions — each session is its own 512 + container. Users who want this protected can add it to their own 513 + `fence.jsonc` denyWrite. 514 + 515 + ## 12. Footer status state machine 516 + 517 + | Bash layer | FS layer | Footer text | Color | 518 + |---|---|---|---| 519 + | active | active | `🔒 fenced (bash + fs)` | accent | 520 + | active | inactive (no fs rules in config) | `🔒 fenced (bash)` | accent | 521 + | inactive (no-fence flag) | inactive | `⚠ unfenced (--no-fence)` | warning | 522 + | inactive (binary missing) | inactive (forced by missing binary) | `⚠ unfenced (fence missing)` | error | 523 + | active | inactive (no `fence.jsonc`) | `🔒 fenced (bash) — no policy` | accent | 524 + 525 + ## 13. `/fence` command surface 526 + 527 + - `/fence` — show resolved policy. Each top-level field annotated with 528 + "applies to: bash | fs | both | none." Highlights the source file path 529 + for each rule when traceable. 530 + - `/fence log [N]` — tail audit log 531 + - `/fence reload` — re-resolve config without `/reload`-ing extensions 532 + - `/fence init` — proxy to `fence config init` in cwd 533 + 534 + Skipped for v1 (revisit if useful): 535 + - `/fence allow <path>` — proactive grant 536 + - `/fence diff` — what was enforced vs what config says 537 + 538 + ## 14. Threat model — be honest in the README 539 + 540 + **What pi-fence stops:** 541 + - Accidental over-reach by the LLM via routine tool calls 542 + - Casual probing via bash (caught by fence binary at the kernel level) 543 + - Symlink-escape tricks for both reads and writes (we realpath) 544 + - Path-traversal via `..` (resolve normalizes) 545 + - Stale config drift between the bash and FS layers (impossible — same 546 + source) 547 + 548 + **What pi-fence does NOT stop:** 549 + - A determined attacker with kernel-level escapes (use Firecracker / gVisor 550 + if you need that) 551 + - Content-based exfiltration to allowed destinations (fence is allowlist 552 + by domain, not content) 553 + - TOCTOU between our realpath and the actual fs operation (requires a 554 + hostile process racing pi — outside threat model) 555 + - MCP-server file access (out of scope) 556 + - Pi's own internal file/network access (not tool calls) 557 + - Smart agents that work around denials by, e.g., writing a script in `.` 558 + and calling `bash`, where bash then does something fence allows. The 559 + bash side catches genuine system-file reads, but agents can be creative. 560 + 561 + The honest pitch: **stops accidental and casual over-reach across both 562 + the agent's tools and any subprocess it spawns, with a single config and 563 + no drift.** Not a containment boundary against malicious code. 564 + 565 + ## 15. Dependencies 566 + 567 + Runtime: 568 + - `picomatch` (~10KB) 569 + - `jsonc-parser` (~30KB) 570 + - `ajv` + `ajv-formats` (~150KB combined) 571 + 572 + Vendored: 573 + - `fence.schema.json` from 574 + `https://raw.githubusercontent.com/Use-Tusk/fence/main/docs/schema/fence.schema.json` 575 + — pinned to the version we test against. Bump deliberately. 576 + 577 + Dev: 578 + - existing: `@mariozechner/pi-coding-agent`, `@types/node`, `typescript` 579 + 580 + External binaries (not npm-managed): 581 + - `fence` on PATH — required for both layers; symmetric fail-open if missing 582 + - `git` — used to find project root; fall back to cwd if absent 583 + 584 + ## 15a. Layer alignment 585 + 586 + Fence's bash layer treats `defaultDenyRead: true` (without 587 + `strictDenyRead`) as "deny reads outside `allowRead` *plus* a built-in 588 + allowlist of system paths". The implicit allowlist (probed via 589 + `fence -d -c true` on macOS 0.1.54): 590 + 591 + - OS-fixed: `/`, `/Applications`, `/Library`, `/System`, `/bin`, 592 + `/dev`, `/etc`, `/lib`, `/lib64`, `/nix`, `/opt`, `/opt/homebrew`, 593 + `/private/etc`, `/private/tmp`, `/private/var/db`, 594 + `/private/var/run`, `/proc`, `/run`, `/sbin`, `/snap`, `/sys`, 595 + `/tmp`, `/usr`, `/usr/local` 596 + - Dynamic per-host (only when present): `~/.bun/bin`, `~/.cargo/bin`, 597 + `~/.deno/bin`, `~/.fnm`, `~/.go`, `~/.local/bin`, `~/.local/pipx`, 598 + `~/.n`, `~/.nvm`, `~/.pyenv`, `~/.rbenv`, `~/.rustup`, `~/.rvm`, 599 + `~/.volta`, `~/bin`, `~/go/bin` 600 + - Plus realpath'd cwd 601 + 602 + The Pi-tool layer in `policy/decide.ts` does not honor any of these. 603 + Result: under `defaultDenyRead: true` alone, `read("/etc/passwd")` is 604 + blocked but `bash("cat /etc/passwd")` succeeds. 605 + 606 + ### Decision 607 + 608 + **Stay strict by-the-book on the Pi-tool layer.** Document the gap; 609 + recommend `strictDenyRead: true` for users who explicitly opt into 610 + `defaultDenyRead`. Fence's own agents.md recommends `extends: "code"` 611 + (no `defaultDenyRead`) for coding agents — under that template both 612 + layers default-allow reads and there is no asymmetry to engineer 613 + around. 614 + 615 + ### Rejected alternatives 616 + 617 + - **Hardcode fence's system-path list in pi-fence** — rejected because 618 + it drifts from fence on upgrades and silently loosens the Pi-tool 619 + layer in a way that user mental models won't track. Buys little 620 + since the recommended baseline (`extends: "code"`) doesn't have the 621 + problem. 622 + - **Parse `fence -d -c true` at session_start** — rejected because the 623 + debug-output format is undocumented and platform-specific (Seatbelt 624 + S-expressions on macOS, bwrap+Landlock setup on Linux). Brittle, 625 + fence-version-coupled. 626 + - **Tighten Pi-tool to fence's stricter shape automatically** — 627 + already what we do; the question was whether to *loosen* Pi-tool to 628 + match fence's default-deny + system-paths behavior, and the answer 629 + was no. 630 + 631 + ### Long-term path 632 + 633 + File an upstream feature request for `fence config show --effective` 634 + (or similar) that returns the implicit-defaults-merged rule set as 635 + JSON. Once that ships, pi-fence can consume it and offer a 636 + `mirrorFenceDefaults` mode without parsing debug output. 637 + 638 + ## 16. Things to decide while writing (no need to ask first) 639 + 640 + - Exact subprocess library for `execFile` — node's built-in `child_process` 641 + with promisified spawn, no need for `execa` 642 + - How to render the structured block message in the TUI (probably reuse 643 + Pi's existing tool-result error styling) 644 + - Whether to debounce/coalesce identical denials within ~100ms (probably 645 + yes — agents sometimes retry instantly) 646 + - Exact naming/casing of the `pi-fence-grant` custom entry type 647 + - Whether the prompt component lives in `grants/prompt.ts` or is 648 + registered as a custom UI element via `ctx.ui.custom()` (probably the 649 + former for v1; latter is more flexible but heavier) 650 + - Backup file naming format (`fence.jsonc.bak.1715357400` vs ISO-8601 — 651 + pick whichever sorts cleanly in `ls`) 652 + 653 + ## 17. Testing notes 654 + 655 + Manual smoke tests (no formal test suite for v1; revisit if this gets 656 + extracted to a package): 657 + 658 + 1. **Happy path:** `fence.jsonc` with `defaultDenyRead: true, 659 + allowRead: ["."]`, allowWrite: ["."]`. Verify: 660 + - `read("./README.md")` allowed 661 + - `read("/etc/passwd")` blocked, structured error 662 + - `write("/tmp/foo")` blocked 663 + - `bash("cat /etc/passwd")` blocked (fence binary) 664 + 2. **Symlink escape:** `ln -s ~/.ssh /tmp/x`, then 665 + `read("/tmp/x/id_rsa")` — must resolve to `~/.ssh/id_rsa` and block 666 + 3. **Grant flow:** trigger a deny, pick `[s]` session — verify same path 667 + succeeds on retry. Pick `[p]` persist — verify `fence.jsonc` updates 668 + and validates. 669 + 4. **Missing fence.jsonc:** trigger persist; verify scaffold prompt. 670 + Accept; verify created file is schema-valid. 671 + 5. **Headless:** `pi -p "read /etc/passwd"` — must deny without prompt and 672 + log to audit. 673 + 6. **Fence missing:** rename `fence` binary; verify warning, both layers 674 + no-op, status footer reads `⚠ unfenced (fence missing)`. 675 + 7. **Lint:** start pi from `$HOME` — verify warning. Set 676 + `allowRead: ["**"]` — verify warning. 677 + 8. **Rotation:** seed a `pi-fence.log` with yesterday's date, trigger an 678 + append, verify rotation to `pi-fence.log.YYYY-MM-DD` and 7-day prune. 679 + 680 + ## 18. Pickup checklist 681 + 682 + When resuming work on this: 683 + 684 + 1. Re-read this file end to end. 685 + 2. Confirm `fence` is still on PATH and pass-0 still works (`pi --version`, 686 + `/sandbox` should *not* exist; `pi-fence` should set the footer status). 687 + 3. Run `npm outdated` in `extensions/pi-fence/` and decide whether to 688 + bump deps. 689 + 4. Diff the upstream `fence.schema.json` against the vendored copy. If 690 + changed, decide whether to bump. 691 + 5. Start with §5 step 1 (`policy/resolve.ts`). Don't skip ahead — each 692 + step has a verification gate. 693 + 6. Update this file as decisions land or change. Treat the "Locked 694 + design decisions" table as authoritative; if a decision needs to 695 + change, edit the table and note it in the changelog at the bottom. 696 + 7. **Verify fence binary version** is within tested range. Add a 697 + `compatibleFenceVersions` field to the extension if you bump. 698 + 699 + --- 700 + 701 + ## Changelog 702 + 703 + - **2026-05-10 (initial):** plan written; pass 0 shipped (bash + user_bash 704 + wrapping with status footer). Pass 1 not started. 705 + - **2026-05-10 (security review):** added §11 (mandatory denies), 706 + case-insensitive FS auto-detection, `nobrace: true` for picomatch, 707 + brace-pattern lint warning, grant-prompt safety properties 708 + (default-deny, realpath display, persist confirmation, fatfinger delay). 709 + Renumbered §12–§18. See conversation thread for the security gap 710 + analysis that motivated each. 711 + - **2026-05-10 (README):** wrote `README.md` covering pass-0 usage, 712 + configuration pointer to `fence.jsonc`, and a 12-item Trade-offs 713 + section documenting the accept-and-document gaps (delayed payloads, 714 + cache staleness, adversarial LLM, headless silence, rolling rate 715 + limit, Unicode, TOCTOU, audit log integrity, version drift, fence 716 + binary trust, MCP scope, Pi-internal reads). Items planned for pass 717 + 1/2 mitigation are tagged in-line with their pass. README is the 718 + user-facing surface; this PLAN remains the build contract. 719 + - **2026-05-10 (pass 1):** shipped FS-policy layer. 720 + - `policy/resolve.ts` shells out to `fence config show`, parses stdout 721 + JSON + stderr chain, detects case-insensitive FS via inode probe, 722 + appends mandatory denies (§11) to `denyWrite`. Vendored 723 + `schema/fence.schema.json` from upstream main. 724 + - `policy/match.ts` realpath's the deepest existing ancestor for 725 + canonicalization (symlink-escape defense) and realpath's the literal 726 + prefix of patterns so `/tmp` matches realpath'd `/private/tmp/...` 727 + on macOS. picomatch options: `dot, nocase (auto), strictSlashes, 728 + nobrace`. 729 + - `policy/decide.ts` pure function over (tool, path, policy); 730 + write-default-deny, deny-overrides-allow, mandatory denies tagged 731 + with `pi-fence.mandatoryDenyWrite` rule and a non-overridable 732 + suggestion in the structured block payload (§6). 733 + - `policy/lint.ts` warns on cwd === $HOME, secret canaries matched by 734 + `allowRead`/`allowWrite`, top-level `**` patterns, brace expansion 735 + (silent layer disagreement: picomatch yes, fence's Go matcher no), 736 + and bare `**` segments without slash neighbors. 737 + - `index.ts` wires the `tool_call` event to gate `read/write/edit/` 738 + `grep/find/ls`, registers `/fence`, `/fence reload`, `/fence init`, 739 + and renders the §12 footer state machine. 740 + - **Verification gate (§5) green:** `read(".")` allowed, `read("/etc/` 741 + `passwd")` blocked, `read(symlink-to-~/.ssh)` blocked via realpath, 742 + `write("/tmp/...")` allowed via prefix-realpath, `write("~/.pi/` 743 + `agent/extensions/x.ts")` blocked as mandatory. 744 + - **Bug fixed during build:** initial `realpathOrSelf` used 745 + `require("node:fs")` which throws `ReferenceError` in ESM and 746 + silently fell through to the catch — this caused `policy.cwd` to 747 + skip realpath and break every cwd-relative pattern. Replaced with 748 + a top-level `realpathSync` import. 749 + - **Not yet implemented (deferred to pass 2):** grants prompt + store 750 + + persist, audit log + `/fence log`, jsonc-parser/ajv editing of 751 + project-root `fence.jsonc`. Pass-1 blocks return the structured 752 + error directly with a suggestion to use `/fence` or edit the file 753 + by hand. 754 + 755 + - **2026-05-10 (pass 2):** shipped grants + persistence + audit log. 756 + - `audit.ts`: append-only NDJSON at `~/.pi/agent/pi-fence.log`. Daily 757 + rotation triggered by mtime delta on first append-of-day; rotated 758 + files named `pi-fence.log.YYYY-MM-DD`, pruned past 7 days. All 759 + failures swallowed (audit must never block the agent). 760 + - `grants/store.ts`: in-memory `GrantStore` keyed by 761 + `${class}::${realpath}` where class ∈ {read, write}. `hydrate()` 762 + validates each record via `isGrant`; corrupted session entries 763 + are dropped silently. 764 + - `grants/prompt.ts`: `ctx.ui.select` with deny first (default-focus), 765 + realpath shown above requested path, `[p]` opens a follow-up 766 + `ctx.ui.confirm` before any disk write. 500ms anti-fatfinger 767 + implemented via `ctx.ui.onTerminalInput` with `consume: true` — 768 + real key drain, not just a sleep. Headless surfaces → immediate 769 + deny. Batch prompt for rate-limit trips returns 770 + `{individual, allow-all-once, deny-all}`. 771 + - `grants/persist.ts`: locates project root via 772 + `git rev-parse --show-toplevel` (fallback cwd). For existing 773 + files: jsonc-parser `modify(filesystem.<allow*>, -1, p, 774 + {isArrayInsertion: true})` preserves comments/formatting; ajv 775 + 2020-12 validates the post-edit JSON; mtime-recheck before write; 776 + `.bak.<unix-ts>` snapshot, atomic write via tmp+rename, prune to 777 + last 3 backups. For missing files: scaffolds `extends: "@base"` 778 + if a non-project config sits in the chain, else `extends: "code"`. 779 + Refuses to write `~/.config/fence/**`, `/etc/**`, or paths inside 780 + pi-fence's own install dir (defense in depth on top of 781 + `findProjectRoot`). 782 + - `index.ts` tool_call flow: session grant lookup → decide() → if 783 + block: audit, mandatory-or-headless short-circuit, otherwise 784 + consult per-turn rate limiter and prompt. Persist failure falls 785 + back to a session grant with a `notify(error)`. After successful 786 + persist we `refreshPolicy()` so the bash layer also sees the new 787 + rule on its next call. 788 + - Per-turn state (`deniedThisTurn`, `turnMode`, `batchPromptInFlight`) 789 + is reset on `turn_end`. `batchPromptInFlight` is the only async 790 + guard against two concurrent denials both opening the batch 791 + confirm. 792 + - `/fence` now lists session grants alongside the resolved policy. 793 + `/fence log [N]` tails the audit file, default N=20, capped at 794 + 500 to keep the notify view tractable. 795 + - **Smoke verified:** scaffold creates a schema-valid file with the 796 + correct `extends`; edit-existing appends to the right list while 797 + preserving prior structure; refuse-global rejects writes to 798 + `~/.config/fence/`; audit rotation triggers on backdated mtime; 799 + grant store correctly classifies grep/find/ls as read-class. 800 + - **Same ESM/`require` trap as pass 1** caught early in persist.ts 801 + (`fsyncSync`, `realpathSync`) and audit.ts (`readFileSync`) — all 802 + replaced with proper top-level imports. 803 + - **Deferred / out of scope (still):** MCP-server file access, 804 + Pi-internal reads, content-based exfil, TOCTOU between 805 + canonicalize and the actual fs op, append-only audit log on disk 806 + (chattr +a / immutable bits). All documented in README §Trade-offs. 807 + 808 + - **2026-05-10 (layer alignment, post-pass-2):** investigated whether 809 + pi-fence should auto-mirror fence's implicit "default readable 810 + system paths" allowlist that fires under `defaultDenyRead: true` 811 + alone. Probed it via `fence -d -c true` (Seatbelt profile) and 812 + catalogued the OS-fixed + dynamic paths in PLAN §15a. Decision: 813 + keep Pi-tool layer strict by-the-book; document `strictDenyRead` 814 + for users who want symmetric behavior; file upstream for a 815 + documented API. README updated to recommend `extends: "code"` 816 + (matching fence's own `docs/agents.md`) as the starter rather 817 + than `defaultDenyRead`-based configs, which moves the asymmetry 818 + out of the common path entirely. 819 + 820 + Last updated: 2026-05-10.
+499
README.md
··· 1 + # pi-fence 2 + 3 + A Pi extension that enforces one shared [`fence.jsonc`][fence] policy 4 + across **two enforcement layers**: 5 + 6 + 1. **Bash subprocesses**, wrapped via the [`fence`][fence] binary 7 + (Seatbelt on macOS, bubblewrap + Landlock on Linux). Covers the LLM 8 + `bash` tool and user `!` / `!!` commands. 9 + 2. **Pi's own file tools** (`read`, `write`, `edit`, `grep`, `find`, 10 + `ls`), gated by an in-process `tool_call` interceptor that consumes 11 + the same resolved policy. 12 + 13 + Both layers read from `fence config show`, so they cannot disagree. 14 + 15 + [fence]: https://github.com/Use-Tusk/fence 16 + 17 + ## Status 18 + 19 + | Layer | Status | Notes | 20 + |---|---|---| 21 + | **Bash + user_bash** | shipped | Pass 0 | 22 + | **Pi file tools** (read/write/edit/grep/find/ls) | shipped | Pass 1 — [`PLAN.md`](./PLAN.md) §5 | 23 + | **Interactive grants + persist + audit log** | shipped | Pass 2 — [`PLAN.md`](./PLAN.md) §8 | 24 + 25 + ## What it does 26 + 27 + When pi-fence is loaded: 28 + 29 + 1. The built-in `bash` tool is replaced by a fenced version that 30 + prefixes every command with `exec fence -c <cmd>`. The agent sees 31 + identical stdout/stderr/exit-code behavior; the kernel-level 32 + sandbox enforces filesystem and network policy on the spawned 33 + process tree. User shell commands (`!`, `!!`) go through the same 34 + wrapping. 35 + 2. Pi's `read`, `write`, `edit`, `grep`, `find`, `ls` tools each fire 36 + a `tool_call` event that pi-fence intercepts. The path is 37 + `realpath`'d (defeating symlink escape) and matched against the 38 + resolved policy. A block returns a structured JSON payload to the 39 + LLM: 40 + 41 + ``` 42 + pi-fence: blocked 43 + { 44 + "tool": "read", 45 + "path": "/private/etc/passwd", 46 + "matched_rule": "filesystem.defaultDenyRead", 47 + "matched_pattern": null, 48 + "policy_source": "/Users/me/proj/fence.jsonc", 49 + "suggestion": "If this access is legitimate, ask the user to grant 50 + it. They can use /fence or add the path to filesystem.allowRead 51 + in fence.jsonc." 52 + } 53 + ``` 54 + 55 + 3. On a block, if a TUI is available, you're prompted with four 56 + options: `[d] deny` (default), `[a] allow once`, `[s] session`, 57 + `[p] persist`. Picking persist edits (or scaffolds) the project's 58 + `fence.jsonc`. Picking session records the grant in session storage 59 + so it survives fork/resume. 60 + 4. Every block decision is appended to `~/.pi/agent/pi-fence.log` for 61 + forensics. Daily rotation, 7-day retention. 62 + 5. The footer status indicator reflects the active state: 63 + - `🔒 fenced (bash + fs)` — both layers active 64 + - `🔒 fenced (bash) — no policy` — fence available, no 65 + `fence.jsonc` (FS layer is dormant; bash uses fence defaults) 66 + - `⚠ unfenced (--no-fence)` — bypassed via the per-session flag 67 + - `⚠ unfenced (fence missing)` — `fence` binary not on PATH; 68 + symmetric fail-open with a warning notification 69 + 70 + ## Requirements 71 + 72 + - **Pi** ≥ 0.72 73 + - **`fence`** on PATH ([install instructions][fence-install]) 74 + - macOS or Linux 75 + 76 + [fence-install]: https://github.com/Use-Tusk/fence#install 77 + 78 + ## Install 79 + 80 + The directory is auto-discovered by Pi when placed at 81 + `~/.pi/agent/extensions/pi-fence/`. 82 + 83 + ```bash 84 + cd ~/.pi/agent/extensions/pi-fence 85 + npm install # installs dev deps for type-checking only 86 + ``` 87 + 88 + No runtime npm dependencies; the extension only needs the `fence` binary 89 + on PATH. 90 + 91 + ## Usage 92 + 93 + Just start Pi as usual. The footer status will show `🔒 fenced` if all 94 + prerequisites are met. 95 + 96 + To bypass for a single session: 97 + 98 + ```bash 99 + pi --no-fence 100 + ``` 101 + 102 + ## Configuration 103 + 104 + Pi-fence does not have its own configuration file. All policy lives in 105 + `fence.jsonc` (or `fence.json`), which is also the file consumed by the 106 + `fence` binary directly. Search order: 107 + 108 + 1. `<project>/fence.jsonc` (project-local) 109 + 2. `~/.config/fence/fence.jsonc` (global) 110 + 111 + If neither exists, fence runs with default-deny network and default-deny 112 + writes — see fence's [configuration reference][fence-config]. 113 + 114 + The recommended starter is what `fence config init` produces — 115 + fence's own [docs/agents.md][fence-agents] points coding-agent users at 116 + the `code` template: 117 + 118 + ```jsonc 119 + { 120 + "$schema": "https://raw.githubusercontent.com/Use-Tusk/fence/main/docs/schema/fence.schema.json", 121 + "extends": "code" 122 + } 123 + ``` 124 + 125 + That's enough for most projects. The `code` template gives you: 126 + 127 + - Reads default-allow, with `denyRead` carving out common secret 128 + locations (`~/.ssh/id_*`, `~/.aws/**`, `~/.gnupg/**`, `~/.netrc`, 129 + cloud-provider creds, etc.) 130 + - Writes restricted to the project + `/tmp` + per-tool config dirs 131 + (`~/.cache/**`, `~/.claude/**`, `~/.pi/**`, etc.) 132 + - `denyWrite` for `**/.env`, `**/*.key`, `**/*.pem`, `**/*.p12`, 133 + `**/*.pfx` 134 + - A network allowlist covering LLM providers, package registries, 135 + GitHub, formulae.brew.sh, and similar 136 + - Command denies for `git push`, `npm publish`, `sudo`, dangerous 137 + `gh` subcommands 138 + 139 + Project-specific overrides go in the same file, e.g.: 140 + 141 + ```jsonc 142 + { 143 + "extends": "code", 144 + "network": { 145 + "allowedDomains": ["private-registry.company.com"] 146 + }, 147 + "filesystem": { 148 + "denyRead": ["./secrets/**"] 149 + } 150 + } 151 + ``` 152 + 153 + [fence-agents]: https://github.com/Use-Tusk/fence/blob/main/docs/agents.md 154 + [fence-config]: https://github.com/Use-Tusk/fence/blob/main/docs/configuration.md 155 + 156 + ### Going stricter (optional): `defaultDenyRead` 157 + 158 + If you want reads to be default-deny instead of default-allow, fence 159 + ships the `code-strict` template (`extends: "code-strict"`) which sets 160 + `defaultDenyRead: true` plus a curated `allowRead` covering the 161 + project and common tool config dirs. 162 + 163 + **Caveat with `defaultDenyRead` alone**: fence's bash layer treats 164 + `defaultDenyRead: true` as "deny reads outside `allowRead` *plus* a 165 + built-in allowlist of system paths" (`/etc`, `/usr`, `/System`, 166 + language-version-manager dirs, etc.). So `cat /etc/passwd` still 167 + succeeds at the bash layer. 168 + 169 + The Pi-tool layer (the `read` tool) does **not** honor that built-in 170 + allowlist — it blocks `read("/etc/passwd")` directly. Result: the 171 + layers appear to disagree under bare `defaultDenyRead`. 172 + 173 + To make both layers reject system-path reads, also add 174 + `strictDenyRead: true`: 175 + 176 + ```jsonc 177 + { 178 + "extends": "code-strict", 179 + "filesystem": { 180 + "strictDenyRead": true // tighten bash layer to match Pi-tool layer 181 + } 182 + } 183 + ``` 184 + 185 + Most users do not need this. Stick with `extends: "code"` unless you 186 + have a specific reason to deny reads. 187 + 188 + > Why pi-fence doesn't auto-mirror fence's implicit defaults: there's 189 + > no documented API for them today (only debug-output parsing). We 190 + > chose to keep the Pi-tool layer strict by-the-book and let users opt 191 + > into `strictDenyRead` for symmetry. See PLAN.md §Layer alignment for 192 + > the trade-off discussion. 193 + 194 + ## Slash commands 195 + 196 + - `/fence` — show the resolved policy, including the chain (project 197 + config + any extends), case-sensitivity detection, and the current 198 + session grants. 199 + - `/fence reload` — re-run `fence config show` and refresh the cached 200 + policy. Use after editing `fence.jsonc` mid-session. 201 + - `/fence init` — proxy to `fence config init` in cwd. 202 + - `/fence log [N]` — tail the last `N` audit log entries (default 20, 203 + cap 500). 204 + 205 + ## Grants 206 + 207 + When the FS layer blocks a tool call and a TUI is available, you'll see: 208 + 209 + ``` 210 + pi-fence: read denied 211 + path: /Users/me/other-repo/package.json 212 + requested: ./other-repo/package.json (only shown when realpath differs) 213 + rule: filesystem.defaultDenyRead 214 + 215 + [d] deny ← default-focused 216 + [a] allow once 217 + [s] allow for this session 218 + [p] persist to fence.jsonc 219 + ``` 220 + 221 + The four options: 222 + 223 + - **deny**: structured error returned to the LLM, no record kept. 224 + - **allow once**: this single tool call only. Logged to audit as 225 + `grant-once`. 226 + - **session**: matches future calls in this session for the same 227 + `(read|write, realpath)` pair. Persisted via `pi.appendEntry` so it 228 + survives fork/resume. Cleared on `/new`. Logged as `grant-session`. 229 + - **persist**: session grant *plus* writes the path into the 230 + project-root `fence.jsonc` (see below). Requires a second 231 + confirmation. Logged as `grant-persist`. 232 + 233 + A 500ms anti-fatfinger delay drains buffered keystrokes before the 234 + prompt accepts input — a chain of denials can't be grant-bombed by a 235 + lingering Enter. 236 + 237 + ### Persist target rules 238 + 239 + Persist always writes to `<project-root>/fence.jsonc`, where project 240 + root = `git rev-parse --show-toplevel`, falling back to cwd. We never 241 + edit `~/.config/fence/fence.jsonc` (the global) and we never walk 242 + upward looking for an existing file. If no project file exists, we 243 + scaffold one with `extends: "@base"` (when a global config is in the 244 + resolution chain) or `extends: "code"` otherwise. 245 + 246 + Edits go through `jsonc-parser` so comments and formatting are 247 + preserved; the result is validated against a vendored copy of 248 + [fence's JSON schema][fence-schema]. Atomic write via tmp + rename; 249 + last 3 backups kept as `fence.jsonc.bak.<unix-ts>`. 250 + 251 + [fence-schema]: https://raw.githubusercontent.com/Use-Tusk/fence/main/docs/schema/fence.schema.json 252 + 253 + ### Mandatory denies (non-overridable) 254 + 255 + A short list of paths is denied for writes regardless of your 256 + `fence.jsonc`. They cannot be granted via any prompt or persist: 257 + 258 + - `~/.pi/agent/extensions/**` — Pi auto-loads TS extensions at startup 259 + - `~/.pi/agent/skills/**`, `~/.agents/skills/**` — skills become part 260 + of the system prompt 261 + - `~/.pi/agent/prompts/**` — prompt templates same effect 262 + - `~/.config/fence/**` — editing global fence config silently broadens 263 + policy machine-wide 264 + - `~/.pi/agent/pi-fence.log*` — audit log integrity 265 + - `<pi-fence install dir>/**` — belt-and-braces against self-modification 266 + 267 + The rationale is that a write to any of these gives the agent 268 + *persistent influence over future Pi sessions* — effectively, prompt 269 + injection or arbitrary code execution that survives even after this 270 + session ends. See [`PLAN.md`](./PLAN.md) §11. 271 + 272 + This enforcement is full-strength on the Pi-tool layer (which we 273 + control). The bash layer inherits the same denies via the resolved 274 + config fence reads. 275 + 276 + ### Headless mode 277 + 278 + When `ctx.hasUI === false` (RPC and `--print` modes), no prompts are 279 + shown. Every block returns the structured error directly to the LLM 280 + and the audit log records it. Set policy upfront via `fence.jsonc` for 281 + headless agents. 282 + 283 + ## Audit log 284 + 285 + Location: `~/.pi/agent/pi-fence.log`. One JSON object per line: 286 + 287 + ```jsonc 288 + { 289 + "ts": "2026-05-10T14:30:01.123Z", 290 + "session": "<session-id>", 291 + "decision": "block" | "grant-once" | "grant-session" | "grant-persist", 292 + "tool": "read" | "write", 293 + "path": "/Users/me/.ssh/id_rsa", 294 + "rule": "filesystem.defaultDenyRead", 295 + "pattern": null 296 + } 297 + ``` 298 + 299 + Daily rotation: on the first append of a UTC day, an existing 300 + `pi-fence.log` is renamed to `pi-fence.log.YYYY-MM-DD`. Files older 301 + than 7 days are pruned. Use `/fence log [N]` to tail. 302 + 303 + Note: only Pi-tool-layer decisions are logged here. Bash-side denials 304 + live in fence's own monitoring (`fence -m`). 305 + 306 + ## Footgun lint 307 + 308 + At session start, pi-fence runs a one-shot lint of your resolved 309 + policy and emits warnings (never blocks) for: 310 + 311 + - cwd === `$HOME` (every relative pattern becomes very permissive) 312 + - `allowRead` matching `~/.ssh/id_rsa`, `~/.aws/credentials`, 313 + `/etc/passwd`, `~/.bash_history`, `/etc/shadow` 314 + - `allowWrite` matching `/etc/hosts`, `/etc/passwd`, `~/.bashrc`, 315 + `~/.zshrc`, `~/.profile` 316 + - Top-level `**` patterns 317 + - Brace expansion in patterns (`~/.{ssh,aws}/**`) — picomatch 318 + expands these but fence's Go matcher does not, so the layers would 319 + silently disagree 320 + - Bare `**` segments without slash neighbors (likely typos for `*`) 321 + 322 + ## Trade-offs and known limits 323 + 324 + Pi-fence is **defense-in-depth against agent error and casual probing**. 325 + It is not a containment boundary against malicious code or an adversarial 326 + LLM. The items below are real and intentionally not addressed in code. 327 + Each is something to understand before relying on this extension. 328 + 329 + ### 1. Delayed-execution payloads 330 + 331 + The agent legitimately writes `package.json` (allowed by 332 + `allowWrite: ["."]`). It includes a malicious `postinstall` hook. Later, 333 + *you* run `npm install` in a normal terminal — outside any sandbox. 334 + Game over. 335 + 336 + This is fundamentally outside pi-fence's lane. Fence sandboxes the 337 + agent's bash, not your subsequent shell sessions. Mitigations: 338 + 339 + - Run package managers through fence yourself: `fence -- npm install` 340 + - Use `--ignore-scripts` (`npm ci --ignore-scripts`, 341 + `pnpm install --ignore-scripts`) 342 + - Diff-review dependency changes the agent introduces 343 + 344 + ### 2. Cache staleness between bash and Pi-tool layers 345 + 346 + The bash layer reads `fence.jsonc` per invocation; the Pi-tool layer 347 + caches at session start. If you edit `fence.jsonc` mid-session, bash 348 + gets the new policy immediately but the Pi-tool layer keeps the stale 349 + view until `/fence reload`. 350 + 351 + Worst case: tightening `denyRead` mid-session leaves a brief window where 352 + the Pi-tool layer still allows it. 353 + 354 + A future enhancement may auto-reload via `fs.watch`. Until then, run 355 + `/fence reload` after editing config. 356 + 357 + ### 3. Adversarial LLM 358 + 359 + Pi-fence assumes the LLM is *fallible*, not *adversarial*. A jailbroken 360 + or prompt-injected model can: 361 + 362 + - Construct grant requests that look legitimate but aren't 363 + - Tell you to run `--no-fence` "to fix the issue" 364 + - Chain operations that reach denied outcomes via allowed primitives 365 + - Exfiltrate data via its own response text — invisible to every layer 366 + of pi-fence 367 + 368 + If your threat model includes a malicious model served from an 369 + untrusted provider, you need VM-level isolation (Firecracker, gVisor, 370 + full container) — not pi-fence. 371 + 372 + ### 4. Headless `--no-fence` is silent by default 373 + 374 + In `-p` print mode and RPC mode, `ctx.ui.notify` may be swallowed. A CI 375 + runner that picks up `--no-fence` from a pasted command line gets 376 + unsandboxed bash with no visible warning. 377 + 378 + A future enhancement may refuse to start in headless mode if 379 + `--no-fence` is set or fence is missing, unless an explicit 380 + `--allow-unfenced` flag is also present. Tracked but not implemented. 381 + 382 + ### 5. Grant rate-limiting is per-turn 383 + 384 + The grant flow rate-limits to 3 distinct denials per agent turn, 385 + resetting on `turn_end`. A chatty agent doing one tool call per turn 386 + never hits the limit. A rolling 60-second window would close this; 387 + filter for v2. 388 + 389 + ### 6. Unicode normalization 390 + 391 + `café` in NFC and `café` in NFD are different byte strings, but the 392 + same file on macOS APFS. A deny rule written one way may not match a 393 + path requested the other way. Mostly theoretical. 394 + 395 + ### 7. TOCTOU between realpath check and operation 396 + 397 + We `realpath` a path, decide, then the tool reads. A racing process 398 + could swap a symlink between the check and the open. Defeating this 399 + requires concurrent host-level malware, outside pi-fence's threat model. 400 + 401 + ### 8. Audit log integrity 402 + 403 + The audit log at `~/.pi/agent/pi-fence.log` is plain append-only text 404 + — not cryptographically signed and not OS-level append-only. Mandatory 405 + denies (see [`PLAN.md`](./PLAN.md) §11) cover the log path so the 406 + agent cannot persist a grant to it via the normal flow, but a 407 + sufficiently determined attacker can still rewrite it. 408 + 409 + If forensic-grade auditing matters, use OS-level append-only flags 410 + (`chattr +a` on Linux, ext attrs on macOS). 411 + 412 + ### 9. Schema and binary version drift 413 + 414 + Pi-fence vendors a pinned `fence.schema.json` for config validation 415 + (used during `/fence`-driven persists). Your installed `fence` binary 416 + may be older or newer. Schema-valid configs can still be rejected by 417 + the binary, or vice versa. Tested with fence 0.1.54. 418 + 419 + ### 10. Trust in the `fence` binary 420 + 421 + If `fence` on your PATH is replaced with a malicious shim, the entire 422 + bash sandbox is gone, and the Pi-tool layer's policy comes from whatever 423 + the shim's `fence config show` returns. Same trust model as any binary 424 + on PATH. 425 + 426 + Install fence from official sources (`brew tap use-tusk/tap`, signed 427 + GitHub releases, or build from source) and verify your installation. 428 + 429 + ### 11. MCP server tools (out of scope) 430 + 431 + If you have MCP servers configured in Pi, their tools run as subprocesses 432 + of Pi — neither bash subprocesses (invisible to fence) nor Pi's built-in 433 + file tools (invisible to pi-fence's Pi-tool layer once pass 1 ships). 434 + 435 + There is no plan to extend pi-fence to MCP servers in v1. If you use 436 + MCP, treat those tools as ungated and configure their access at the 437 + MCP-server level. 438 + 439 + ### 12. Pi-internal reads and writes 440 + 441 + Pi reads files for itself outside of tool calls — AGENTS.md, skill 442 + files, session storage, image attachments, extension code. Pi-fence 443 + governs the **agent's** tool calls, not Pi's own filesystem access. 444 + 445 + The mandatory denies in [`PLAN.md`](./PLAN.md) §11 prevent the agent 446 + from writing to the most dangerous of these locations 447 + (`~/.pi/agent/extensions/`, `~/.pi/agent/skills/`, 448 + `~/.config/fence/`). Other Pi-internal paths are unprotected by design. 449 + 450 + ## Threat model summary 451 + 452 + **Pi-fence stops:** 453 + 454 + - Accidental over-reach by the LLM via routine tool calls 455 + - Casual probing via bash (caught by fence at the kernel level) 456 + - Symlink-escape tricks (we `realpath` for both reads and writes — pass 1) 457 + - Path-traversal via `..` 458 + - Stale config drift between layers (impossible — same source) 459 + - Persistent prompt-injection / arbitrary code execution via writes to 460 + Pi's extension or skill directories (mandatory denies) 461 + 462 + **Pi-fence does NOT stop:** 463 + 464 + - Determined attackers with kernel-level escapes 465 + - Content-based exfiltration to allowed network destinations 466 + - Delayed payloads in files the user later runs outside fence 467 + - TOCTOU races against host-level malware 468 + - Adversarial LLMs that work around denials via creative chains 469 + - MCP-server tool access 470 + - Pi's own internal filesystem and network access 471 + 472 + ## Roadmap 473 + 474 + See [`PLAN.md`](./PLAN.md) for the full design and build plan. 475 + 476 + - **Pass 0** (shipped): bash + user_bash wrapping, status footer, 477 + `--no-fence` flag. 478 + - **Pass 1** (shipped): Pi-tool-layer enforcement for 479 + read/write/edit/grep/find/ls, structured block messages, `/fence` 480 + command surface, mandatory denies, footgun lint. 481 + - **Pass 2** (shipped): interactive grants 482 + (once / session / persist / deny), persist-to-`fence.jsonc` flow, 483 + audit log with daily rotation. 484 + - **Future** (no pass scheduled): rolling-window rate limiter, 485 + `--allow-unfenced` headless guard, `fs.watch`-based auto-reload, 486 + Linux append-only audit log via `chattr +a`. 487 + 488 + ## License 489 + 490 + MIT. Same as Pi. 491 + 492 + ## Credits 493 + 494 + Built on [`fence`][fence] by Use-Tusk, which itself credits Anthropic's 495 + [`sandbox-runtime`][sr] as inspiration. Both are sandbox wrappers around 496 + OS-level isolation primitives — pi-fence wires `fence` into Pi's 497 + extension event surface. 498 + 499 + [sr]: https://github.com/anthropic-experimental/sandbox-runtime
+161
audit.ts
··· 1 + /** 2 + * pi-fence audit log (PLAN §9). 3 + * 4 + * Append-only, newline-delimited JSON at `~/.pi/agent/pi-fence.log`. Daily 5 + * rotation, 7-day retention. Every block decision the FS layer makes is 6 + * recorded here, regardless of whether the UI prompted or not — the 7 + * append path is the single audit funnel. 8 + * 9 + * Bash-side denials are *not* in this log. Fence has its own monitor 10 + * (`fence -m`) for that. See README's Trade-offs section. 11 + * 12 + * Schema (one entry per line): 13 + * 14 + * { 15 + * "ts": "2026-05-10T14:30:01.123Z", 16 + * "session": "<session-id>", 17 + * "decision": "block" | "grant-once" | "grant-session" | "grant-persist", 18 + * "tool": "read", 19 + * "path": "/Users/andri/.ssh/id_rsa", 20 + * "rule": "filesystem.defaultDenyRead", 21 + * "pattern": null 22 + * } 23 + * 24 + * Failure mode: if the log can't be appended (disk full, permission 25 + * error, audit-log path itself in mandatory deny — paranoid recursion), 26 + * we swallow the error. The audit log is best-effort by design; never 27 + * block the agent because we couldn't record a decision. 28 + */ 29 + 30 + import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, unlinkSync } from "node:fs"; 31 + import * as os from "node:os"; 32 + import * as path from "node:path"; 33 + 34 + export const AUDIT_DIR = path.join(os.homedir(), ".pi", "agent"); 35 + export const AUDIT_FILE = path.join(AUDIT_DIR, "pi-fence.log"); 36 + const RETENTION_DAYS = 7; 37 + 38 + export type AuditDecision = "block" | "grant-once" | "grant-session" | "grant-persist"; 39 + 40 + export interface AuditEntry { 41 + ts: string; 42 + session: string | undefined; 43 + decision: AuditDecision; 44 + tool: string; 45 + path: string; 46 + rule: string; 47 + pattern: string | null; 48 + } 49 + 50 + /** 51 + * Append a single entry to the audit log. Errors are swallowed; the audit 52 + * log is best-effort and must never block the agent. 53 + */ 54 + export function append(entry: AuditEntry): void { 55 + try { 56 + ensureDir(); 57 + rotateIfNeeded(); 58 + appendFileSync(AUDIT_FILE, JSON.stringify(entry) + "\n", "utf-8"); 59 + } catch { 60 + // Intentionally swallowed (see file-level comment). 61 + } 62 + } 63 + 64 + /** 65 + * Tail the last `n` entries from the current log file, parsed. 66 + * 67 + * Used by `/fence log [N]`. Returns oldest-first within the slice. 68 + * Malformed lines are skipped silently. Reads only the current day's 69 + * file (rotated files aren't included; we want today-by-default for 70 + * the slash command, with a clean small output). 71 + */ 72 + export function tail(n: number): AuditEntry[] { 73 + try { 74 + if (!existsSync(AUDIT_FILE)) return []; 75 + const text = readFileSync(AUDIT_FILE, "utf-8"); 76 + const lines = text.split("\n").filter((l) => l.trim().length > 0); 77 + const slice = lines.slice(-n); 78 + const out: AuditEntry[] = []; 79 + for (const ln of slice) { 80 + try { 81 + out.push(JSON.parse(ln) as AuditEntry); 82 + } catch { 83 + // skip malformed 84 + } 85 + } 86 + return out; 87 + } catch { 88 + return []; 89 + } 90 + } 91 + 92 + function ensureDir(): void { 93 + if (!existsSync(AUDIT_DIR)) mkdirSync(AUDIT_DIR, { recursive: true }); 94 + } 95 + 96 + /** 97 + * On the first append of a UTC day, rotate `pi-fence.log` to a dated 98 + * suffix and prune files older than `RETENTION_DAYS`. 99 + * 100 + * The rotation key is the mtime of the log file in UTC. If today's 101 + * date differs from the file's mtime date, rotate. This is robust to 102 + * processes that ran across midnight: the next append after midnight 103 + * triggers rotation. 104 + */ 105 + function rotateIfNeeded(): void { 106 + if (!existsSync(AUDIT_FILE)) return; 107 + let st; 108 + try { 109 + st = statSync(AUDIT_FILE); 110 + } catch { 111 + return; 112 + } 113 + const fileDay = utcDate(st.mtime); 114 + const today = utcDate(new Date()); 115 + if (fileDay === today) return; 116 + 117 + const rotated = `${AUDIT_FILE}.${fileDay}`; 118 + try { 119 + renameSync(AUDIT_FILE, rotated); 120 + } catch { 121 + // If rename fails, we'd rather keep appending to the existing file 122 + // than crash. Bail out of rotation. 123 + return; 124 + } 125 + pruneOld(); 126 + } 127 + 128 + /** 129 + * Delete rotated logs whose embedded date is older than the retention 130 + * window. We only touch files matching `pi-fence.log.YYYY-MM-DD` to 131 + * avoid clobbering anything else in the directory. 132 + */ 133 + function pruneOld(): void { 134 + let entries: string[]; 135 + try { 136 + entries = readdirSync(AUDIT_DIR); 137 + } catch { 138 + return; 139 + } 140 + const cutoff = new Date(); 141 + cutoff.setUTCDate(cutoff.getUTCDate() - RETENTION_DAYS); 142 + const cutoffStr = utcDate(cutoff); 143 + for (const name of entries) { 144 + const m = /^pi-fence\.log\.(\d{4}-\d{2}-\d{2})$/.exec(name); 145 + if (!m || !m[1]) continue; 146 + if (m[1] < cutoffStr) { 147 + try { 148 + unlinkSync(path.join(AUDIT_DIR, name)); 149 + } catch { 150 + // ignore 151 + } 152 + } 153 + } 154 + } 155 + 156 + function utcDate(d: Date): string { 157 + const y = d.getUTCFullYear(); 158 + const m = String(d.getUTCMonth() + 1).padStart(2, "0"); 159 + const day = String(d.getUTCDate()).padStart(2, "0"); 160 + return `${y}-${m}-${day}`; 161 + }
+377
grants/persist.ts
··· 1 + /** 2 + * Persist a grant to the project-root `fence.jsonc` (PLAN §7). 3 + * 4 + * Three branches: 5 + * 6 + * A. File exists at `<root>/fence.jsonc` (or `.json`) — read, edit 7 + * via jsonc-parser to preserve comments and formatting, validate 8 + * the result against the vendored schema, mtime-check, atomic 9 + * write, retain last 3 backups. 10 + * 11 + * B. No file at root — scaffold one with `extends: "@base"` if a 12 + * non-project fence config exists in the chain (the user already 13 + * has a global), else `extends: "code"` as a sensible default. 14 + * 15 + * C. File only exists outside project root (e.g. ~/.config/fence/) — 16 + * treated as branch B; we never edit the global file. 17 + * 18 + * Project root resolution: 19 + * 1. `git rev-parse --show-toplevel` from cwd. 20 + * 2. Else cwd itself. 21 + * 22 + * Schema validation uses the vendored `schema/fence.schema.json` and 23 + * Ajv 2020-12. We validate after our edit so we never write an invalid 24 + * config; we do *not* validate the existing on-disk file before our 25 + * edit (PLAN §7: "we're not the linter for existing config"). 26 + * 27 + * Failure modes return a structured error so the caller can show a 28 + * clean notify. We never throw across the extension boundary. 29 + */ 30 + 31 + import { execFileSync } from "node:child_process"; 32 + import { 33 + closeSync, 34 + copyFileSync, 35 + existsSync, 36 + fstatSync, 37 + fsyncSync, 38 + openSync, 39 + realpathSync, 40 + readFileSync, 41 + readdirSync, 42 + renameSync, 43 + statSync, 44 + unlinkSync, 45 + writeSync, 46 + } from "node:fs"; 47 + import * as path from "node:path"; 48 + import { fileURLToPath } from "node:url"; 49 + import Ajv2020 from "ajv/dist/2020.js"; 50 + import addFormats from "ajv-formats"; 51 + import { applyEdits, modify, parse, type ParseError } from "jsonc-parser"; 52 + import type { GrantClass } from "./store.js"; 53 + 54 + const __dirname = path.dirname(fileURLToPath(import.meta.url)); 55 + 56 + export interface PersistInput { 57 + cwd: string; 58 + /** "read" → filesystem.allowRead, "write" → filesystem.allowWrite. */ 59 + tool: GrantClass; 60 + /** Realpath'd absolute path to add to the allow list. */ 61 + path: string; 62 + /** Resolution chain from `fence config show`; informs scaffold's `extends`. */ 63 + chain: string[]; 64 + } 65 + 66 + export type PersistResult = 67 + | { ok: true; targetFile: string; created: boolean } 68 + | { ok: false; error: string }; 69 + 70 + /** 71 + * Project root via git, falling back to cwd. 72 + * 73 + * Stable target: doesn't shift when you `cd src/` mid-session. 74 + * If `git rev-parse` exits non-zero, we use cwd unchanged. 75 + */ 76 + export function findProjectRoot(cwd: string): string { 77 + try { 78 + const out = execFileSync("git", ["rev-parse", "--show-toplevel"], { 79 + cwd, 80 + encoding: "utf-8", 81 + stdio: ["ignore", "pipe", "ignore"], 82 + timeout: 2000, 83 + }); 84 + const root = out.trim(); 85 + if (root) return root; 86 + } catch { 87 + // not a git repo, or git not installed 88 + } 89 + return cwd; 90 + } 91 + 92 + /** Locate the existing target file, if any. Prefers `.jsonc` over `.json`. */ 93 + function findTargetFile(root: string): { file: string; exists: boolean } { 94 + const jsonc = path.join(root, "fence.jsonc"); 95 + if (existsSync(jsonc)) return { file: jsonc, exists: true }; 96 + const json = path.join(root, "fence.json"); 97 + if (existsSync(json)) return { file: json, exists: true }; 98 + return { file: jsonc, exists: false }; // proposed 99 + } 100 + 101 + /** 102 + * Add a path to the appropriate filesystem allow-list and write it back 103 + * to disk. Edits-in-place if the file exists, scaffolds otherwise. 104 + * 105 + * Returns a structured result; the caller surfaces the error message 106 + * via `ctx.ui.notify`. We never throw. 107 + */ 108 + export async function persistGrant(input: PersistInput): Promise<PersistResult> { 109 + const root = findProjectRoot(input.cwd); 110 + const { file, exists } = findTargetFile(root); 111 + 112 + if (!exists) { 113 + return scaffoldNew(file, input); 114 + } 115 + return editExisting(file, input); 116 + } 117 + 118 + const FIELD: Record<GrantClass, string> = { 119 + read: "allowRead", 120 + write: "allowWrite", 121 + }; 122 + 123 + /** 124 + * Edit branch (PLAN §7 branch A). 125 + * 126 + * Steps: 127 + * 1. Read + record mtime. 128 + * 2. jsonc-parser: parse to a CST that preserves comments + formatting. 129 + * 3. modify() to append the path to filesystem.<allow*>. 130 + * 4. JSON.parse the resulting text and Ajv-validate against the schema. 131 + * 5. mtime check — if the file changed since step 1, abort. 132 + * 6. .bak.<ts> snapshot, atomic-rename via tmp, prune to last 3 backups. 133 + */ 134 + function editExisting(file: string, input: PersistInput): PersistResult { 135 + const refusal = refuseSuspiciousTarget(file); 136 + if (refusal) return { ok: false, error: refusal }; 137 + 138 + let text: string; 139 + let mtimeMs: number; 140 + try { 141 + const st = statSync(file); 142 + mtimeMs = st.mtimeMs; 143 + text = readFileSync(file, "utf-8"); 144 + } catch (e) { 145 + return { ok: false, error: `cannot read ${file}: ${msg(e)}` }; 146 + } 147 + 148 + // Parse to detect malformed JSONC up front. We don't bail on schema 149 + // errors here — only on syntax errors that would corrupt our edit. 150 + const parseErrors: ParseError[] = []; 151 + parse(text, parseErrors, { allowTrailingComma: true, disallowComments: false }); 152 + if (parseErrors.length > 0) { 153 + return { 154 + ok: false, 155 + error: `${file} is not valid JSONC; refusing to edit. Fix it manually first.`, 156 + }; 157 + } 158 + 159 + const field = FIELD[input.tool]; 160 + const editsCst = modify(text, ["filesystem", field, -1], input.path, { 161 + isArrayInsertion: true, 162 + formattingOptions: { tabSize: 2, insertSpaces: true, eol: "\n" }, 163 + }); 164 + const newText = applyEdits(text, editsCst); 165 + 166 + // Validate the resulting JSON against the vendored schema. 167 + let parsed: unknown; 168 + try { 169 + parsed = JSON.parse(newText); 170 + } catch (e) { 171 + return { ok: false, error: `pi-fence's edit produced invalid JSON: ${msg(e)}` }; 172 + } 173 + const schemaError = validateAgainstSchema(parsed); 174 + if (schemaError) { 175 + return { ok: false, error: `pi-fence's edit failed schema validation: ${schemaError}` }; 176 + } 177 + 178 + // mtime re-check — abort if the file moved under us. 179 + try { 180 + const stNow = statSync(file); 181 + if (stNow.mtimeMs !== mtimeMs) { 182 + return { ok: false, error: `${file} changed on disk during edit; retry the grant.` }; 183 + } 184 + } catch (e) { 185 + return { ok: false, error: `mtime check failed: ${msg(e)}` }; 186 + } 187 + 188 + // Backup, then atomic write. 189 + try { 190 + const ts = Math.floor(Date.now() / 1000); 191 + const bak = `${file}.bak.${ts}`; 192 + copyFileSync(file, bak); 193 + pruneBackups(file, 3); 194 + } catch { 195 + // non-fatal; we still attempt the write 196 + } 197 + 198 + try { 199 + atomicWrite(file, newText); 200 + } catch (e) { 201 + return { ok: false, error: `atomic write failed: ${msg(e)}` }; 202 + } 203 + 204 + return { ok: true, targetFile: file, created: false }; 205 + } 206 + 207 + /** 208 + * Scaffold branch (PLAN §7 branches B + C). 209 + * 210 + * Creates a minimal `fence.jsonc` with: 211 + * - `$schema` pointing at upstream 212 + * - `extends: "@base"` if a non-project config exists in the chain, 213 + * else `extends: "code"` 214 + * - the seed allow rule 215 + */ 216 + function scaffoldNew(file: string, input: PersistInput): PersistResult { 217 + const refusal = refuseSuspiciousTarget(file); 218 + if (refusal) return { ok: false, error: refusal }; 219 + 220 + // Pick `extends` based on whether the user has a non-project config 221 + // already loaded. The chain contains absolute paths to loaded config 222 + // files. If any of them sit outside the project root, we want to 223 + // inherit them via `@base` — fence's documented escape hatch for 224 + // stacking project on top of user globals. Otherwise we default to 225 + // the `code` template, the standard "coding agent" baseline. 226 + const root = path.dirname(file); 227 + const hasGlobal = input.chain.some((p) => !p.startsWith(root + path.sep) && p !== file); 228 + const ext = hasGlobal ? "@base" : "code"; 229 + 230 + const field = FIELD[input.tool]; 231 + const body = { 232 + $schema: "https://raw.githubusercontent.com/Use-Tusk/fence/main/docs/schema/fence.schema.json", 233 + extends: ext, 234 + filesystem: { 235 + [field]: [input.path], 236 + }, 237 + }; 238 + 239 + const schemaError = validateAgainstSchema(body); 240 + if (schemaError) { 241 + return { ok: false, error: `scaffold failed schema validation: ${schemaError}` }; 242 + } 243 + 244 + const text = JSON.stringify(body, null, 2) + "\n"; 245 + try { 246 + atomicWrite(file, text); 247 + } catch (e) { 248 + return { ok: false, error: `atomic write failed: ${msg(e)}` }; 249 + } 250 + return { ok: true, targetFile: file, created: true }; 251 + } 252 + 253 + /** 254 + * Reject paths that look like a user/system global (defense in depth — 255 + * `findProjectRoot` should never produce these, but cheap to check). 256 + */ 257 + function refuseSuspiciousTarget(file: string): string | undefined { 258 + const realFile = realpathOrSelf(file); 259 + const lower = realFile.toLowerCase(); 260 + if (lower.includes("/.config/fence/")) { 261 + return `refusing to edit global fence config (${realFile}). Persist requires a project-local fence.jsonc.`; 262 + } 263 + if (lower.startsWith("/etc/")) { 264 + return `refusing to edit ${realFile}: outside project root.`; 265 + } 266 + // Anything inside our own install dir is off-limits. 267 + const installDir = path.resolve(__dirname, ".."); 268 + if (realFile.startsWith(installDir + path.sep)) { 269 + return `refusing to edit ${realFile}: inside pi-fence install dir.`; 270 + } 271 + return undefined; 272 + } 273 + 274 + let cachedAjv: { validate: (data: unknown) => string | undefined } | undefined; 275 + 276 + function getValidator(): { validate: (data: unknown) => string | undefined } { 277 + if (cachedAjv) return cachedAjv; 278 + try { 279 + const schemaPath = path.join(__dirname, "..", "schema", "fence.schema.json"); 280 + const schema = JSON.parse(readFileSync(schemaPath, "utf-8")); 281 + // ajv and ajv-formats are CJS-with-default; the default-import unwraps it. 282 + const ajv = new Ajv2020({ allErrors: false, strict: false }); 283 + addFormats(ajv); 284 + const validateFn = ajv.compile(schema); 285 + cachedAjv = { 286 + validate(data: unknown) { 287 + const ok = validateFn(data); 288 + if (ok) return undefined; 289 + const errs = validateFn.errors ?? []; 290 + const first = errs[0]; 291 + if (!first) return "validation failed"; 292 + return `${first.instancePath || "/"} ${first.message ?? "invalid"}`; 293 + }, 294 + }; 295 + return cachedAjv; 296 + } catch (e) { 297 + // If the schema itself can't be loaded, validation is best-effort. 298 + // Surface the failure but don't block the user. 299 + cachedAjv = { 300 + validate(_) { 301 + return undefined; 302 + }, 303 + }; 304 + return cachedAjv; 305 + } 306 + } 307 + 308 + function validateAgainstSchema(data: unknown): string | undefined { 309 + return getValidator().validate(data); 310 + } 311 + 312 + /** 313 + * Atomic write: write to `<file>.tmp.<pid>`, fsync, then rename. 314 + * 315 + * The fsync ensures the data is on disk before the rename completes; 316 + * a crash mid-write leaves the original file intact and an orphan 317 + * tmp file (which we don't bother to clean — next persist will 318 + * overwrite it). 319 + */ 320 + function atomicWrite(file: string, text: string): void { 321 + const tmp = `${file}.tmp.${process.pid}`; 322 + const fd = openSync(tmp, "w"); 323 + try { 324 + writeSync(fd, text); 325 + // Best-effort fsync. Some filesystems (or sandboxes) reject this; swallow. 326 + try { 327 + fsyncSync(fd); 328 + } catch { 329 + // ignore 330 + } 331 + // Verify the file we're about to rename has the bytes we just wrote. 332 + // Cheap sanity check: fstat says size matches. 333 + const st = fstatSync(fd); 334 + if (st.size !== Buffer.byteLength(text, "utf-8")) { 335 + throw new Error(`tmp file size mismatch (${st.size} vs ${Buffer.byteLength(text, "utf-8")})`); 336 + } 337 + } finally { 338 + closeSync(fd); 339 + } 340 + renameSync(tmp, file); 341 + } 342 + 343 + /** Keep the most recent `keep` backups; delete older ones. */ 344 + function pruneBackups(file: string, keep: number): void { 345 + const dir = path.dirname(file); 346 + const base = path.basename(file); 347 + let entries: string[]; 348 + try { 349 + entries = readdirSync(dir); 350 + } catch { 351 + return; 352 + } 353 + const baks = entries 354 + .filter((n) => n.startsWith(`${base}.bak.`)) 355 + .map((n) => ({ name: n, ts: parseInt(n.slice(`${base}.bak.`.length), 10) })) 356 + .filter((e) => Number.isFinite(e.ts)) 357 + .sort((a, b) => b.ts - a.ts); 358 + for (const old of baks.slice(keep)) { 359 + try { 360 + unlinkSync(path.join(dir, old.name)); 361 + } catch { 362 + // ignore 363 + } 364 + } 365 + } 366 + 367 + function realpathOrSelf(p: string): string { 368 + try { 369 + return realpathSync(p); 370 + } catch { 371 + return p; 372 + } 373 + } 374 + 375 + function msg(e: unknown): string { 376 + return e instanceof Error ? e.message : String(e); 377 + }
+143
grants/prompt.ts
··· 1 + /** 2 + * Interactive grant prompt (PLAN §8 — "Grant flow on block"). 3 + * 4 + * Shown on a tool_call block when: 5 + * - `ctx.hasUI === true` (no prompts in headless mode), and 6 + * - the deny is not from the mandatory list (those are non-overridable). 7 + * 8 + * Mandatory safety properties from PLAN §3 + §8, all enforced here: 9 + * 10 + * 1. Default-focused option is `[d] deny`. A user who reflexively 11 + * hits Enter denies the access. Intentional friction. 12 + * 2. Resolved (realpath) path is shown first; the requested path is 13 + * shown as secondary. Defends against an agent that does 14 + * `ln -s ~/.ssh ./innocent` then `read("./innocent/id_rsa")`. 15 + * 3. 500 ms anti-fatfinger delay before any keypress is accepted. 16 + * We achieve this by registering an `onTerminalInput` consumer 17 + * that swallows all input for 500ms, *then* opening the dialog. 18 + * Anything the user typed during that window is dropped instead 19 + * of being delivered to the select. 20 + * 4. `[p] persist` requires a second confirmation. After picking 21 + * persist, we show a separate confirm dialog. The actual write 22 + * to disk happens only on explicit `y`. 23 + */ 24 + 25 + import type { ExtensionContext } from "@mariozechner/pi-coding-agent"; 26 + import type { FsTool } from "../policy/decide.js"; 27 + 28 + export type PromptChoice = "once" | "session" | "persist" | "deny"; 29 + 30 + export interface PromptInput { 31 + tool: FsTool; 32 + /** Path the LLM passed in (un-canonicalized). */ 33 + requestedPath: string; 34 + /** realpath'd absolute path. Shown first. */ 35 + realPath: string; 36 + /** Rule that fired (e.g. `filesystem.defaultDenyRead`). */ 37 + rule: string; 38 + } 39 + 40 + const ANTI_FATFINGER_MS = 500; 41 + 42 + /** 43 + * Show the grant prompt, return the user's choice. 44 + * 45 + * Does *not* execute the grant — caller (index.ts) wires the choice 46 + * into the grant store, audit log, and (for persist) the disk-edit 47 + * code in `grants/persist.ts`. 48 + */ 49 + export async function promptForGrant(ctx: ExtensionContext, input: PromptInput): Promise<PromptChoice> { 50 + if (!ctx.hasUI) return "deny"; // headless: no prompts, deny is final (PLAN §3) 51 + 52 + await drainInputFor(ctx, ANTI_FATFINGER_MS); 53 + 54 + const title = `pi-fence: ${input.tool} denied`; 55 + 56 + // We render the body lines as the option labels' descriptive prefix. 57 + // `select` doesn't have a separate description field, so we fold the 58 + // path/tool/rule into the title via a leading notify? No — notify is 59 + // transient. Use the option labels themselves to carry the path on 60 + // the first line, then four real options. 61 + // 62 + // In practice ctx.ui.select shows just the title + options. We jam 63 + // the salient details into the title. 64 + const titleWithDetails = 65 + `${title}\n` + 66 + ` path: ${input.realPath}\n` + 67 + (input.realPath !== input.requestedPath ? ` requested: ${input.requestedPath}\n` : "") + 68 + ` rule: ${input.rule}`; 69 + 70 + // Order matters: deny is FIRST so it's the default-focused option (PLAN §8). 71 + const labels = [ 72 + "[d] deny", 73 + "[a] allow once", 74 + "[s] allow for this session", 75 + "[p] persist to fence.jsonc", 76 + ] as const; 77 + 78 + const choice = await ctx.ui.select(titleWithDetails, labels as unknown as string[]); 79 + if (choice === undefined) return "deny"; // dialog dismissed → deny 80 + if (choice === labels[0]) return "deny"; 81 + if (choice === labels[1]) return "once"; 82 + if (choice === labels[2]) return "session"; 83 + 84 + // Persist needs a second confirmation step. The first selection just 85 + // expresses intent; the second is the irrevocable disk-write commit. 86 + const confirmed = await ctx.ui.confirm( 87 + "pi-fence: persist to fence.jsonc?", 88 + `This will modify your project's fence.jsonc to permanently allow this path.\n\n ${input.realPath}\n\nProceed?`, 89 + ); 90 + if (!confirmed) return "deny"; 91 + return "persist"; 92 + } 93 + 94 + /** 95 + * Show a batched prompt when the rate limiter has tripped (PLAN §8 — 96 + * "Rate-limiting prompts"). Three-way choice: allow individually, allow 97 + * all once, deny all. Returns the choice; caller sets the per-turn 98 + * mode flag based on the result. 99 + */ 100 + export async function promptForBatch( 101 + ctx: ExtensionContext, 102 + deniedCount: number, 103 + ): Promise<"individual" | "allow-all-once" | "deny-all"> { 104 + if (!ctx.hasUI) return "deny-all"; 105 + await drainInputFor(ctx, ANTI_FATFINGER_MS); 106 + 107 + const labels = [ 108 + "[d] deny all (recommended)", 109 + "[i] keep prompting individually", 110 + "[a] allow all remaining for this turn", 111 + ] as const; 112 + const choice = await ctx.ui.select( 113 + `pi-fence: ${deniedCount} denials this turn — agent appears to be probing. How do you want to handle the rest?`, 114 + labels as unknown as string[], 115 + ); 116 + if (choice === labels[1]) return "individual"; 117 + if (choice === labels[2]) return "allow-all-once"; 118 + return "deny-all"; 119 + } 120 + 121 + /** 122 + * Drain raw terminal input for `ms` milliseconds before resolving. 123 + * 124 + * `onTerminalInput` is interactive-only. In RPC/print modes the 125 + * registration is a no-op (or absent), but those modes also have 126 + * `hasUI === false`, so we never reach here. 127 + * 128 + * The handler returns `{ consume: true }` so swallowed input is *not* 129 + * forwarded to whatever the next focused component is. This is the 130 + * essential property: we drop keys that were buffered before the 131 + * prompt opened. 132 + */ 133 + function drainInputFor(ctx: ExtensionContext, ms: number): Promise<void> { 134 + return new Promise((resolve) => { 135 + const unsubscribe = ctx.ui.onTerminalInput((_data) => { 136 + return { consume: true }; 137 + }); 138 + setTimeout(() => { 139 + unsubscribe(); 140 + resolve(); 141 + }, ms); 142 + }); 143 + }
+100
grants/store.ts
··· 1 + /** 2 + * Session-scoped grants (PLAN §8). 3 + * 4 + * In-memory map of grants the user has chosen to apply for the rest of 5 + * the session. Persisted via `pi.appendEntry("pi-fence-grant", ...)` so 6 + * they survive fork/resume; cleared on `/new`. 7 + * 8 + * Two grant classes — "read" (covers read/grep/find/ls) and "write" 9 + * (covers write/edit). PLAN §8: "A grant is keyed by `{ tool: 'read' | 10 + * 'write', path: <realpath> }`. Grants match exactly (no globs)." 11 + * 12 + * "Allow once" grants are NOT stored here; they bypass the prompt for 13 + * the immediate retry only. "Persist" grants ARE recorded here too 14 + * (in addition to being written to fence.jsonc) so that a session 15 + * audit trail exists even after the persistence target moves. 16 + * 17 + * The store is *not* responsible for calling `pi.appendEntry`. The 18 + * caller (index.ts) does that after `record()`. This keeps the store 19 + * pure-data and easy to unit-test. 20 + */ 21 + 22 + import type { FsTool } from "../policy/decide.js"; 23 + 24 + /** Class a tool falls into for grant purposes. read, grep, find, ls → "read"; write, edit → "write". */ 25 + export type GrantClass = "read" | "write"; 26 + 27 + export type GrantKind = "session" | "persist"; 28 + 29 + export interface Grant { 30 + tool: GrantClass; 31 + /** Canonicalized (realpath'd) absolute path. */ 32 + path: string; 33 + kind: GrantKind; 34 + /** ISO timestamp the grant was created. */ 35 + ts: string; 36 + } 37 + 38 + export function grantClass(tool: FsTool): GrantClass { 39 + if (tool === "write" || tool === "edit") return "write"; 40 + return "read"; 41 + } 42 + 43 + export class GrantStore { 44 + private grants = new Map<string, Grant>(); 45 + 46 + /** Insert or replace a grant. */ 47 + record(grant: Grant): void { 48 + this.grants.set(this.key(grant.tool, grant.path), grant); 49 + } 50 + 51 + /** 52 + * Look up a session grant for this (toolClass, path). A "write" 53 + * grant covers both `write` and `edit`; a "read" grant covers 54 + * `read`/`grep`/`find`/`ls`. Returns undefined if no grant exists. 55 + */ 56 + lookup(tool: GrantClass, p: string): Grant | undefined { 57 + return this.grants.get(this.key(tool, p)); 58 + } 59 + 60 + /** 61 + * Reconstitute the in-memory map from a list of grant records loaded 62 + * from session storage on resume/fork. Called once at session_start. 63 + * 64 + * Later entries overwrite earlier ones for the same key — the latest 65 + * recorded decision wins, which matches user expectation when they 66 + * granted, denied, then granted again across fork/resume. 67 + */ 68 + hydrate(records: unknown[]): void { 69 + this.grants.clear(); 70 + for (const v of records) { 71 + if (isGrant(v)) this.record(v); 72 + } 73 + } 74 + 75 + /** All grants, in insertion order. For `/fence` display. */ 76 + all(): Grant[] { 77 + return Array.from(this.grants.values()); 78 + } 79 + 80 + private key(tool: GrantClass, p: string): string { 81 + return `${tool}::${p}`; 82 + } 83 + } 84 + 85 + /** 86 + * Validate a value coming back from `pi.appendEntry`-recorded data. 87 + * 88 + * Defensive: session storage is on disk and could theoretically be 89 + * corrupted or hand-edited. Reject anything that doesn't conform. 90 + */ 91 + export function isGrant(v: unknown): v is Grant { 92 + if (!v || typeof v !== "object") return false; 93 + const o = v as Record<string, unknown>; 94 + return ( 95 + (o.tool === "read" || o.tool === "write") && 96 + typeof o.path === "string" && 97 + (o.kind === "session" || o.kind === "persist") && 98 + typeof o.ts === "string" 99 + ); 100 + }
+555
index.ts
··· 1 + /** 2 + * pi-fence — single source of FS/exec policy across two enforcement layers: 3 + * 4 + * 1. Bash subprocesses, wrapped via the `fence` binary 5 + * (kernel-level sandbox: Seatbelt on macOS, bubblewrap+Landlock on Linux). 6 + * Affects the LLM `bash` tool and user `!` / `!!` commands. 7 + * 8 + * 2. Pi's own file tools (read, write, edit, grep, find, ls), gated by an 9 + * in-process `tool_call` interceptor that consumes the *same* resolved 10 + * `fence config show` policy. The two layers cannot disagree because 11 + * they read the same source. 12 + * 13 + * Configure via `fence.jsonc` (project-local) or `~/.config/fence/fence.jsonc`. 14 + * This extension itself has no config beyond the `--no-fence` flag. 15 + * 16 + * Surface (passes 0+1+2): 17 + * - bash + user_bash wrapping 18 + * - tool_call interceptor for the six file tools, with structured block 19 + * messages back to the LLM (policy/decide.ts) 20 + * - footer status state machine (PLAN §12) 21 + * - /fence, /fence reload, /fence init, /fence log slash commands 22 + * - one-shot footgun lint warnings at session_start 23 + * - interactive grant prompt with [once / session / persist / deny], 24 + * default-deny, realpath display, 500ms anti-fatfinger, persist 25 + * second-confirm 26 + * - session-scoped grants persisted via pi.appendEntry, restored on 27 + * fork/resume 28 + * - persist-to-disk grants edit-or-scaffold the project-root fence.jsonc, 29 + * ajv-validated, atomic-written, with .bak.<ts> backups 30 + * - audit log at ~/.pi/agent/pi-fence.log with daily rotation, 7-day 31 + * retention, every block + every grant 32 + * - per-turn rate limit: 3+ denials trigger a batch prompt 33 + * 34 + * Requires the `fence` binary on PATH: https://github.com/Use-Tusk/fence 35 + * Use `--no-fence` to bypass enforcement on both layers for the session. 36 + */ 37 + 38 + import { execFileSync } from "node:child_process"; 39 + import type { CustomEntry, ExtensionAPI, ExtensionContext, SessionEntry, ToolCallEvent } from "@mariozechner/pi-coding-agent"; 40 + import { createBashTool, createLocalBashOperations } from "@mariozechner/pi-coding-agent"; 41 + import { resolveConfig, type ResolvedPolicy } from "./policy/resolve.js"; 42 + import { canonicalize } from "./policy/match.js"; 43 + import { decide, formatBlockReason, isFsTool, isWriteTool, type FsTool } from "./policy/decide.js"; 44 + import { lintPolicy } from "./policy/lint.js"; 45 + import { append as auditAppend, tail as auditTail, type AuditDecision } from "./audit.js"; 46 + import { GrantStore, grantClass, type Grant } from "./grants/store.js"; 47 + import { promptForBatch, promptForGrant } from "./grants/prompt.js"; 48 + import { persistGrant } from "./grants/persist.js"; 49 + 50 + const GRANT_CUSTOM_TYPE = "pi-fence-grant"; 51 + const RATE_LIMIT_THRESHOLD = 3; 52 + 53 + /** POSIX single-quote escape: wraps `s` in `'…'`, escaping any embedded `'`. */ 54 + function shQuote(s: string): string { 55 + return `'${s.replace(/'/g, "'\\''")}'`; 56 + } 57 + 58 + /** 59 + * Rewrite a bash command so it runs inside `fence`. 60 + * 61 + * The bash tool spawns `bash -c <command>`. By prefixing with `exec`, the 62 + * outer bash replaces itself with fence — no orphan bash process. 63 + */ 64 + function fenceWrap(command: string): string { 65 + return `exec fence -c ${shQuote(command)}`; 66 + } 67 + 68 + /** Probe whether `fence` is on PATH and runnable. */ 69 + function detectFence(): { ok: true; version: string } | { ok: false; error: string } { 70 + try { 71 + const out = execFileSync("fence", ["--version"], { 72 + encoding: "utf-8", 73 + stdio: ["ignore", "pipe", "pipe"], 74 + timeout: 2000, 75 + }); 76 + return { ok: true, version: out.split("\n")[0]?.trim() ?? "unknown" }; 77 + } catch (e) { 78 + return { ok: false, error: e instanceof Error ? e.message : String(e) }; 79 + } 80 + } 81 + 82 + /** 83 + * Extract the path argument from a tool_call event for an FS tool. 84 + * 85 + * `read`/`write`/`edit` always carry `path`. `grep`/`find`/`ls` have an 86 + * optional `path` that defaults to cwd. Returns the *raw* path the 87 + * caller supplied (un-canonicalized); the tool_call handler canonicalizes. 88 + */ 89 + function extractToolPath(event: ToolCallEvent, cwd: string): string | undefined { 90 + const input = event.input as Record<string, unknown>; 91 + switch (event.toolName) { 92 + case "read": 93 + case "write": 94 + case "edit": 95 + return typeof input.path === "string" ? input.path : undefined; 96 + case "grep": 97 + case "find": 98 + case "ls": 99 + return typeof input.path === "string" ? input.path : cwd; 100 + default: 101 + return undefined; 102 + } 103 + } 104 + 105 + /** Compose the footer status string per PLAN §12. See pass-1 commentary. */ 106 + function statusLine(state: { 107 + bashActive: boolean; 108 + fsActive: boolean; 109 + hasFsRules: boolean; 110 + disabledBy: "no-flag" | "missing" | undefined; 111 + }): { text: string; color: "accent" | "warning" | "error" } { 112 + if (state.disabledBy === "no-flag") return { text: "\u26a0 unfenced (--no-fence)", color: "warning" }; 113 + if (state.disabledBy === "missing") return { text: "\u26a0 unfenced (fence missing)", color: "error" }; 114 + if (state.bashActive && state.fsActive && state.hasFsRules) return { text: "\ud83d\udd12 fenced (bash + fs)", color: "accent" }; 115 + if (state.bashActive && !state.hasFsRules) return { text: "\ud83d\udd12 fenced (bash) \u2014 no policy", color: "accent" }; 116 + if (state.bashActive) return { text: "\ud83d\udd12 fenced (bash)", color: "accent" }; 117 + return { text: "\u26a0 unfenced", color: "warning" }; 118 + } 119 + 120 + /** Pretty-print a resolved policy for the `/fence` command. */ 121 + function renderPolicy(policy: ResolvedPolicy, grants: Grant[]): string { 122 + const lines: string[] = []; 123 + lines.push("pi-fence — resolved policy"); 124 + lines.push(""); 125 + if (!policy.available) { 126 + lines.push(" fence binary not available; both layers are no-ops."); 127 + return lines.join("\n"); 128 + } 129 + lines.push(` cwd: ${policy.cwd}`); 130 + lines.push(` home: ${policy.home}`); 131 + lines.push(` case-insensitive fs: ${policy.caseInsensitive ? "yes" : "no"}`); 132 + if (policy.chain.length > 0) { 133 + lines.push(" chain:"); 134 + for (const f of policy.chain) lines.push(` - ${f}`); 135 + } else { 136 + lines.push(" chain: <built-in defaults only>"); 137 + } 138 + const fs = policy.resolved.filesystem ?? {}; 139 + lines.push(""); 140 + lines.push(` filesystem (applies to: bash + fs):`); 141 + lines.push(` defaultDenyRead: ${fs.defaultDenyRead ?? false}`); 142 + for (const field of ["allowRead", "denyRead", "allowWrite", "denyWrite"] as const) { 143 + const list = fs[field] ?? []; 144 + if (list.length === 0) { 145 + lines.push(` ${field}: []`); 146 + continue; 147 + } 148 + lines.push(` ${field}:`); 149 + for (const p of list) { 150 + const tag = field === "denyWrite" && policy.mandatoryDenyWrite.includes(p) ? " [pi-fence mandatory]" : ""; 151 + lines.push(` - ${p}${tag}`); 152 + } 153 + } 154 + const network = (policy.resolved as { network?: { allowedDomains?: string[]; deniedDomains?: string[] } }).network; 155 + if (network) { 156 + lines.push(""); 157 + lines.push(" network (applies to: bash):"); 158 + lines.push(` allowedDomains: ${network.allowedDomains?.length ?? 0} entries`); 159 + lines.push(` deniedDomains: ${network.deniedDomains?.length ?? 0} entries`); 160 + } 161 + if (grants.length > 0) { 162 + lines.push(""); 163 + lines.push(` session grants (cleared on /new):`); 164 + for (const g of grants) lines.push(` - [${g.tool}/${g.kind}] ${g.path}`); 165 + } 166 + return lines.join("\n"); 167 + } 168 + 169 + export default function (pi: ExtensionAPI) { 170 + pi.registerFlag("no-fence", { 171 + description: "Disable pi-fence wrapping for this session", 172 + type: "boolean", 173 + default: false, 174 + }); 175 + 176 + const cwd = process.cwd(); 177 + const localBash = createBashTool(cwd); 178 + const fencedBash = createBashTool(cwd, { 179 + spawnHook: ({ command, cwd, env }) => ({ 180 + command: fenceWrap(command), 181 + cwd, 182 + env, 183 + }), 184 + }); 185 + 186 + let fenceAvailable = false; 187 + /** Cached resolved policy. Refreshed on session_start and `/fence reload`. */ 188 + let policy: ResolvedPolicy | undefined; 189 + /** Lint warning IDs already shown — prevents repeat fires across reloads. */ 190 + const lintShown = new Set<string>(); 191 + /** Session-grant store. Hydrated from session entries on session_start. */ 192 + const grants = new GrantStore(); 193 + 194 + // Per-turn rate-limit state. Reset at turn_end. 195 + let deniedThisTurn = new Set<string>(); 196 + let turnMode: "individual" | "allow-all-once" | "deny-all" = "individual"; 197 + /** True while we're awaiting the batch prompt — prevents racing concurrent denials from re-prompting. */ 198 + let batchPromptInFlight = false; 199 + 200 + const bashActive = () => fenceAvailable && !pi.getFlag("no-fence"); 201 + const fsActive = () => fenceAvailable && !pi.getFlag("no-fence") && !!policy?.available; 202 + 203 + const refreshStatus = (ctx: ExtensionContext) => { 204 + const disabledBy = !fenceAvailable ? "missing" : pi.getFlag("no-fence") ? "no-flag" : undefined; 205 + const { text, color } = statusLine({ 206 + bashActive: bashActive(), 207 + fsActive: fsActive(), 208 + hasFsRules: !!policy?.hasFsRules, 209 + disabledBy, 210 + }); 211 + ctx.ui.setStatus("pi-fence", ctx.ui.theme.fg(color, text)); 212 + }; 213 + 214 + const refreshPolicy = async (): Promise<ResolvedPolicy> => { 215 + policy = await resolveConfig({ cwd }); 216 + return policy; 217 + }; 218 + 219 + /** 220 + * Reconstruct in-memory session grants from session storage. 221 + * 222 + * On session_start we walk the entry list for `custom` entries with 223 + * our customType. Defensive validation via `isGrant` guards against 224 + * corrupted/hand-edited entries; bad ones are silently dropped. 225 + */ 226 + const hydrateGrants = (entries: SessionEntry[]) => { 227 + const found: unknown[] = []; 228 + for (const e of entries) { 229 + if (e.type !== "custom") continue; 230 + const ce = e as CustomEntry; 231 + if (ce.customType !== GRANT_CUSTOM_TYPE) continue; 232 + found.push(ce.data); 233 + } 234 + // `hydrate` validates each entry via isGrant; bad records are dropped. 235 + grants.hydrate(found); 236 + }; 237 + 238 + pi.on("session_start", async (_event, ctx) => { 239 + const probe = detectFence(); 240 + fenceAvailable = probe.ok; 241 + 242 + if (!probe.ok) { 243 + ctx.ui.notify( 244 + "pi-fence: `fence` not found on PATH \u2014 falling back to unsandboxed bash. " + 245 + "Install from https://github.com/Use-Tusk/fence to enable wrapping.", 246 + "warning", 247 + ); 248 + refreshStatus(ctx); 249 + return; 250 + } 251 + 252 + if (pi.getFlag("no-fence")) { 253 + ctx.ui.notify(`pi-fence: ${probe.version} detected, but disabled via --no-fence`, "info"); 254 + refreshStatus(ctx); 255 + return; 256 + } 257 + 258 + await refreshPolicy(); 259 + refreshStatus(ctx); 260 + 261 + // Hydrate session grants from prior entries (covers fork + resume). 262 + try { 263 + hydrateGrants(ctx.sessionManager.getEntries()); 264 + } catch { 265 + // session manager not ready yet on some startup paths; harmless 266 + } 267 + 268 + if (policy?.available && policy.chain.length === 0) { 269 + ctx.ui.notify( 270 + "pi-fence: no fence.jsonc detected. Run `/fence init` to create one, or your bash will use built-in defaults only.", 271 + "info", 272 + ); 273 + } 274 + 275 + if (policy) { 276 + for (const w of lintPolicy(policy)) { 277 + if (lintShown.has(w.id)) continue; 278 + lintShown.add(w.id); 279 + ctx.ui.notify(w.message, "warning"); 280 + } 281 + } 282 + }); 283 + 284 + // Reset per-turn rate-limit state. The same agent loop run that 285 + // triggered N denials shouldn't carry that state into the next user 286 + // turn — fresh prompt budget each turn. 287 + pi.on("turn_end", () => { 288 + deniedThisTurn = new Set(); 289 + turnMode = "individual"; 290 + batchPromptInFlight = false; 291 + }); 292 + 293 + // LLM-issued `bash` tool — overrides the built-in. 294 + pi.registerTool({ 295 + ...fencedBash, 296 + label: "bash (fenced)", 297 + async execute(id, params, signal, onUpdate, _ctx) { 298 + const tool = bashActive() ? fencedBash : localBash; 299 + return tool.execute(id, params, signal, onUpdate); 300 + }, 301 + }); 302 + 303 + // User `!` / `!!` commands. 304 + pi.on("user_bash", () => { 305 + if (!bashActive()) return; 306 + const ops = createLocalBashOperations(); 307 + return { 308 + operations: { 309 + exec: (command, cwd, options) => ops.exec(fenceWrap(command), cwd, options), 310 + }, 311 + }; 312 + }); 313 + 314 + /** 315 + * Record a grant: in-memory store + session entry + audit log. 316 + * 317 + * Persist grants ALSO go through `persistGrant()` to write fence.jsonc. 318 + * That happens before this is called so a failed disk write doesn't 319 + * leave a session entry pointing at nothing. 320 + */ 321 + const recordGrant = (g: Grant, decision: AuditDecision, rule: string) => { 322 + grants.record(g); 323 + try { 324 + pi.appendEntry(GRANT_CUSTOM_TYPE, g); 325 + } catch { 326 + // session not writable (e.g. read-only mode); in-memory grant still applies for this session 327 + } 328 + auditAppend({ 329 + ts: new Date().toISOString(), 330 + session: safeSessionId(), 331 + decision, 332 + tool: g.tool, 333 + path: g.path, 334 + rule, 335 + pattern: null, 336 + }); 337 + }; 338 + 339 + const safeSessionId = (): string | undefined => { 340 + // Best-effort: many ctx paths expose sessionManager but we don't 341 + // have the ctx here. Audit gracefully omits the session id when 342 + // undefined. 343 + return undefined; 344 + }; 345 + 346 + // FS-policy layer: gate the six file tools. 347 + pi.on("tool_call", async (event, ctx) => { 348 + if (!fsActive() || !policy) return; // fail-open per PLAN §3 349 + if (!isFsTool(event.toolName)) return; 350 + 351 + const rawPath = extractToolPath(event, policy.cwd); 352 + if (rawPath === undefined) return; 353 + 354 + const tool = event.toolName as FsTool; 355 + const absPath = canonicalize(rawPath, policy.cwd); 356 + const klass = grantClass(tool); 357 + 358 + // Lookup order (PLAN §8): session grants first, then decide(). 359 + if (grants.lookup(klass, absPath)) return; 360 + 361 + const decision = decide(tool, absPath, policy); 362 + if (decision.allow) return; 363 + 364 + // At this point the policy says block. Audit it unconditionally — 365 + // even if a grant is about to flip it to allow, the structured 366 + // record matters for forensics ("what did the policy think before 367 + // the user intervened?"). 368 + auditAppend({ 369 + ts: new Date().toISOString(), 370 + session: ctx.sessionManager.getSessionId(), 371 + decision: "block", 372 + tool, 373 + path: absPath, 374 + rule: decision.rule, 375 + pattern: decision.pattern ?? null, 376 + }); 377 + 378 + // Mandatory denies are non-overridable — never prompt. 379 + if (decision.mandatory) { 380 + return { block: true, reason: formatBlockReason(tool, absPath, decision) }; 381 + } 382 + 383 + // Headless: deny is final, return structured error to LLM. 384 + if (!ctx.hasUI) { 385 + return { block: true, reason: formatBlockReason(tool, absPath, decision) }; 386 + } 387 + 388 + // Per-turn rate limiter (PLAN §8). Track distinct denied paths; 389 + // on the Nth distinct one, ask the user how to handle the rest. 390 + deniedThisTurn.add(absPath); 391 + 392 + if (turnMode === "deny-all") { 393 + return { block: true, reason: formatBlockReason(tool, absPath, decision) }; 394 + } 395 + if (turnMode === "allow-all-once") { 396 + // User opted in to allow the rest of the turn. Each is a 397 + // "grant-once" for audit purposes; we don't record session grants. 398 + auditAppend({ 399 + ts: new Date().toISOString(), 400 + session: ctx.sessionManager.getSessionId(), 401 + decision: "grant-once", 402 + tool: klass, 403 + path: absPath, 404 + rule: decision.rule, 405 + pattern: decision.pattern ?? null, 406 + }); 407 + return; // allow 408 + } 409 + 410 + // `individual` mode: prompt unless the threshold tripped this very 411 + // call, in which case open the batch confirm first. 412 + if (deniedThisTurn.size >= RATE_LIMIT_THRESHOLD && !batchPromptInFlight) { 413 + batchPromptInFlight = true; 414 + let mode: "individual" | "allow-all-once" | "deny-all"; 415 + try { 416 + mode = await promptForBatch(ctx, deniedThisTurn.size); 417 + } finally { 418 + batchPromptInFlight = false; 419 + } 420 + turnMode = mode; 421 + if (mode === "deny-all") { 422 + return { block: true, reason: formatBlockReason(tool, absPath, decision) }; 423 + } 424 + if (mode === "allow-all-once") { 425 + auditAppend({ 426 + ts: new Date().toISOString(), 427 + session: ctx.sessionManager.getSessionId(), 428 + decision: "grant-once", 429 + tool: klass, 430 + path: absPath, 431 + rule: decision.rule, 432 + pattern: decision.pattern ?? null, 433 + }); 434 + return; 435 + } 436 + // fall through to individual prompt for this call 437 + } 438 + 439 + const choice = await promptForGrant(ctx, { 440 + tool, 441 + requestedPath: rawPath, 442 + realPath: absPath, 443 + rule: decision.rule, 444 + }); 445 + 446 + switch (choice) { 447 + case "deny": 448 + return { block: true, reason: formatBlockReason(tool, absPath, decision) }; 449 + case "once": { 450 + auditAppend({ 451 + ts: new Date().toISOString(), 452 + session: ctx.sessionManager.getSessionId(), 453 + decision: "grant-once", 454 + tool: klass, 455 + path: absPath, 456 + rule: decision.rule, 457 + pattern: decision.pattern ?? null, 458 + }); 459 + return; 460 + } 461 + case "session": { 462 + const grant: Grant = { tool: klass, path: absPath, kind: "session", ts: new Date().toISOString() }; 463 + recordGrant(grant, "grant-session", decision.rule); 464 + return; 465 + } 466 + case "persist": { 467 + const result = await persistGrant({ 468 + cwd: ctx.cwd, 469 + tool: klass, 470 + path: absPath, 471 + chain: policy.chain, 472 + }); 473 + if (!result.ok) { 474 + ctx.ui.notify(`pi-fence: persist failed — ${result.error}. Granting for session only.`, "error"); 475 + const grant: Grant = { tool: klass, path: absPath, kind: "session", ts: new Date().toISOString() }; 476 + recordGrant(grant, "grant-session", decision.rule); 477 + return; 478 + } 479 + const grant: Grant = { tool: klass, path: absPath, kind: "persist", ts: new Date().toISOString() }; 480 + recordGrant(grant, "grant-persist", decision.rule); 481 + ctx.ui.notify( 482 + result.created 483 + ? `pi-fence: created ${result.targetFile} with this grant.` 484 + : `pi-fence: added grant to ${result.targetFile}.`, 485 + "info", 486 + ); 487 + // Refresh policy so the persisted rule takes effect immediately 488 + // for the *bash* layer too (and so /fence reflects reality). 489 + await refreshPolicy(); 490 + refreshStatus(ctx); 491 + return; 492 + } 493 + } 494 + }); 495 + 496 + // /fence — show resolved policy, log tail, reload, init. 497 + pi.registerCommand("fence", { 498 + description: "Show pi-fence policy. Subcommands: reload, init, log [N].", 499 + async handler(args, ctx) { 500 + const parts = args.trim().split(/\s+/).filter(Boolean); 501 + const sub = parts[0] ?? ""; 502 + if (sub === "" || sub === "show") { 503 + if (!policy) await refreshPolicy(); 504 + if (!policy) { 505 + ctx.ui.notify("pi-fence: no policy loaded.", "warning"); 506 + return; 507 + } 508 + ctx.ui.notify(renderPolicy(policy, grants.all()), "info"); 509 + return; 510 + } 511 + if (sub === "reload") { 512 + await refreshPolicy(); 513 + refreshStatus(ctx); 514 + ctx.ui.notify("pi-fence: policy reloaded.", "info"); 515 + return; 516 + } 517 + if (sub === "init") { 518 + try { 519 + const out = execFileSync("fence", ["config", "init"], { 520 + cwd: ctx.cwd, 521 + encoding: "utf-8", 522 + stdio: ["ignore", "pipe", "pipe"], 523 + timeout: 5000, 524 + }); 525 + await refreshPolicy(); 526 + refreshStatus(ctx); 527 + ctx.ui.notify(`pi-fence: ${out.trim() || "fence config init complete."}`, "info"); 528 + } catch (e) { 529 + ctx.ui.notify(`pi-fence: \`fence config init\` failed — ${e instanceof Error ? e.message : String(e)}`, "error"); 530 + } 531 + return; 532 + } 533 + if (sub === "log") { 534 + const n = parts[1] ? Math.max(1, Math.min(500, parseInt(parts[1], 10) || 20)) : 20; 535 + const entries = auditTail(n); 536 + if (entries.length === 0) { 537 + ctx.ui.notify("pi-fence: audit log is empty.", "info"); 538 + return; 539 + } 540 + const lines: string[] = [`pi-fence audit log (last ${entries.length}):`, ""]; 541 + for (const e of entries) { 542 + const tag = e.decision.padEnd(14); 543 + const pat = e.pattern ? ` [${e.pattern}]` : ""; 544 + lines.push(` ${e.ts} ${tag} ${e.tool.padEnd(5)} ${e.path} (${e.rule}${pat})`); 545 + } 546 + ctx.ui.notify(lines.join("\n"), "info"); 547 + return; 548 + } 549 + ctx.ui.notify(`pi-fence: unknown subcommand "${sub}". Try /fence, /fence reload, /fence init, /fence log.`, "warning"); 550 + }, 551 + }); 552 + 553 + // Reference suppress for diagnostics. 554 + void isWriteTool; 555 + }
+3955
package-lock.json
··· 1 + { 2 + "name": "pi-fence", 3 + "version": "0.1.0", 4 + "lockfileVersion": 3, 5 + "requires": true, 6 + "packages": { 7 + "": { 8 + "name": "pi-fence", 9 + "version": "0.1.0", 10 + "dependencies": { 11 + "ajv": "^8.20.0", 12 + "ajv-formats": "^3.0.1", 13 + "jsonc-parser": "^3.3.1", 14 + "picomatch": "^4.0.4" 15 + }, 16 + "devDependencies": { 17 + "@mariozechner/pi-coding-agent": "^0.72.0", 18 + "@types/node": "^22.0.0", 19 + "@types/picomatch": "^4.0.3", 20 + "typescript": "^5.5.0" 21 + } 22 + }, 23 + "node_modules/@anthropic-ai/sdk": { 24 + "version": "0.91.1", 25 + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.91.1.tgz", 26 + "integrity": "sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw==", 27 + "dev": true, 28 + "license": "MIT", 29 + "dependencies": { 30 + "json-schema-to-ts": "^3.1.1" 31 + }, 32 + "bin": { 33 + "anthropic-ai-sdk": "bin/cli" 34 + }, 35 + "peerDependencies": { 36 + "zod": "^3.25.0 || ^4.0.0" 37 + }, 38 + "peerDependenciesMeta": { 39 + "zod": { 40 + "optional": true 41 + } 42 + } 43 + }, 44 + "node_modules/@aws-crypto/crc32": { 45 + "version": "5.2.0", 46 + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", 47 + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", 48 + "dev": true, 49 + "license": "Apache-2.0", 50 + "dependencies": { 51 + "@aws-crypto/util": "^5.2.0", 52 + "@aws-sdk/types": "^3.222.0", 53 + "tslib": "^2.6.2" 54 + }, 55 + "engines": { 56 + "node": ">=16.0.0" 57 + } 58 + }, 59 + "node_modules/@aws-crypto/sha256-browser": { 60 + "version": "5.2.0", 61 + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", 62 + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", 63 + "dev": true, 64 + "license": "Apache-2.0", 65 + "dependencies": { 66 + "@aws-crypto/sha256-js": "^5.2.0", 67 + "@aws-crypto/supports-web-crypto": "^5.2.0", 68 + "@aws-crypto/util": "^5.2.0", 69 + "@aws-sdk/types": "^3.222.0", 70 + "@aws-sdk/util-locate-window": "^3.0.0", 71 + "@smithy/util-utf8": "^2.0.0", 72 + "tslib": "^2.6.2" 73 + } 74 + }, 75 + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { 76 + "version": "2.2.0", 77 + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", 78 + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", 79 + "dev": true, 80 + "license": "Apache-2.0", 81 + "dependencies": { 82 + "tslib": "^2.6.2" 83 + }, 84 + "engines": { 85 + "node": ">=14.0.0" 86 + } 87 + }, 88 + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { 89 + "version": "2.2.0", 90 + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", 91 + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", 92 + "dev": true, 93 + "license": "Apache-2.0", 94 + "dependencies": { 95 + "@smithy/is-array-buffer": "^2.2.0", 96 + "tslib": "^2.6.2" 97 + }, 98 + "engines": { 99 + "node": ">=14.0.0" 100 + } 101 + }, 102 + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { 103 + "version": "2.3.0", 104 + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", 105 + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", 106 + "dev": true, 107 + "license": "Apache-2.0", 108 + "dependencies": { 109 + "@smithy/util-buffer-from": "^2.2.0", 110 + "tslib": "^2.6.2" 111 + }, 112 + "engines": { 113 + "node": ">=14.0.0" 114 + } 115 + }, 116 + "node_modules/@aws-crypto/sha256-js": { 117 + "version": "5.2.0", 118 + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", 119 + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", 120 + "dev": true, 121 + "license": "Apache-2.0", 122 + "dependencies": { 123 + "@aws-crypto/util": "^5.2.0", 124 + "@aws-sdk/types": "^3.222.0", 125 + "tslib": "^2.6.2" 126 + }, 127 + "engines": { 128 + "node": ">=16.0.0" 129 + } 130 + }, 131 + "node_modules/@aws-crypto/supports-web-crypto": { 132 + "version": "5.2.0", 133 + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", 134 + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", 135 + "dev": true, 136 + "license": "Apache-2.0", 137 + "dependencies": { 138 + "tslib": "^2.6.2" 139 + } 140 + }, 141 + "node_modules/@aws-crypto/util": { 142 + "version": "5.2.0", 143 + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", 144 + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", 145 + "dev": true, 146 + "license": "Apache-2.0", 147 + "dependencies": { 148 + "@aws-sdk/types": "^3.222.0", 149 + "@smithy/util-utf8": "^2.0.0", 150 + "tslib": "^2.6.2" 151 + } 152 + }, 153 + "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { 154 + "version": "2.2.0", 155 + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", 156 + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", 157 + "dev": true, 158 + "license": "Apache-2.0", 159 + "dependencies": { 160 + "tslib": "^2.6.2" 161 + }, 162 + "engines": { 163 + "node": ">=14.0.0" 164 + } 165 + }, 166 + "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { 167 + "version": "2.2.0", 168 + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", 169 + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", 170 + "dev": true, 171 + "license": "Apache-2.0", 172 + "dependencies": { 173 + "@smithy/is-array-buffer": "^2.2.0", 174 + "tslib": "^2.6.2" 175 + }, 176 + "engines": { 177 + "node": ">=14.0.0" 178 + } 179 + }, 180 + "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { 181 + "version": "2.3.0", 182 + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", 183 + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", 184 + "dev": true, 185 + "license": "Apache-2.0", 186 + "dependencies": { 187 + "@smithy/util-buffer-from": "^2.2.0", 188 + "tslib": "^2.6.2" 189 + }, 190 + "engines": { 191 + "node": ">=14.0.0" 192 + } 193 + }, 194 + "node_modules/@aws-sdk/client-bedrock-runtime": { 195 + "version": "3.1045.0", 196 + "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1045.0.tgz", 197 + "integrity": "sha512-aPC6gAz9uKRiwfnKB7peTs6yD0FpSzmVnSkx0f2QtJfosFM6J6KtBvR1lMKby050K4C4PAyEScwA5YTsGfTcGA==", 198 + "dev": true, 199 + "license": "Apache-2.0", 200 + "dependencies": { 201 + "@aws-crypto/sha256-browser": "5.2.0", 202 + "@aws-crypto/sha256-js": "5.2.0", 203 + "@aws-sdk/core": "^3.974.8", 204 + "@aws-sdk/credential-provider-node": "^3.972.39", 205 + "@aws-sdk/eventstream-handler-node": "^3.972.14", 206 + "@aws-sdk/middleware-eventstream": "^3.972.10", 207 + "@aws-sdk/middleware-host-header": "^3.972.10", 208 + "@aws-sdk/middleware-logger": "^3.972.10", 209 + "@aws-sdk/middleware-recursion-detection": "^3.972.11", 210 + "@aws-sdk/middleware-user-agent": "^3.972.38", 211 + "@aws-sdk/middleware-websocket": "^3.972.16", 212 + "@aws-sdk/region-config-resolver": "^3.972.13", 213 + "@aws-sdk/token-providers": "3.1045.0", 214 + "@aws-sdk/types": "^3.973.8", 215 + "@aws-sdk/util-endpoints": "^3.996.8", 216 + "@aws-sdk/util-user-agent-browser": "^3.972.10", 217 + "@aws-sdk/util-user-agent-node": "^3.973.24", 218 + "@smithy/config-resolver": "^4.4.17", 219 + "@smithy/core": "^3.23.17", 220 + "@smithy/eventstream-serde-browser": "^4.2.14", 221 + "@smithy/eventstream-serde-config-resolver": "^4.3.14", 222 + "@smithy/eventstream-serde-node": "^4.2.14", 223 + "@smithy/fetch-http-handler": "^5.3.17", 224 + "@smithy/hash-node": "^4.2.14", 225 + "@smithy/invalid-dependency": "^4.2.14", 226 + "@smithy/middleware-content-length": "^4.2.14", 227 + "@smithy/middleware-endpoint": "^4.4.32", 228 + "@smithy/middleware-retry": "^4.5.7", 229 + "@smithy/middleware-serde": "^4.2.20", 230 + "@smithy/middleware-stack": "^4.2.14", 231 + "@smithy/node-config-provider": "^4.3.14", 232 + "@smithy/node-http-handler": "^4.6.1", 233 + "@smithy/protocol-http": "^5.3.14", 234 + "@smithy/smithy-client": "^4.12.13", 235 + "@smithy/types": "^4.14.1", 236 + "@smithy/url-parser": "^4.2.14", 237 + "@smithy/util-base64": "^4.3.2", 238 + "@smithy/util-body-length-browser": "^4.2.2", 239 + "@smithy/util-body-length-node": "^4.2.3", 240 + "@smithy/util-defaults-mode-browser": "^4.3.49", 241 + "@smithy/util-defaults-mode-node": "^4.2.54", 242 + "@smithy/util-endpoints": "^3.4.2", 243 + "@smithy/util-middleware": "^4.2.14", 244 + "@smithy/util-retry": "^4.3.6", 245 + "@smithy/util-stream": "^4.5.25", 246 + "@smithy/util-utf8": "^4.2.2", 247 + "tslib": "^2.6.2" 248 + }, 249 + "engines": { 250 + "node": ">=20.0.0" 251 + } 252 + }, 253 + "node_modules/@aws-sdk/core": { 254 + "version": "3.974.8", 255 + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.8.tgz", 256 + "integrity": "sha512-njR2qoG6ZuB0kvAS2FyICsFZJ6gmCcf2X/7JcD14sUvGDm26wiZ5BrA6LOiUxKFEF+IVe7kdroxyE00YlkiYsw==", 257 + "dev": true, 258 + "license": "Apache-2.0", 259 + "dependencies": { 260 + "@aws-sdk/types": "^3.973.8", 261 + "@aws-sdk/xml-builder": "^3.972.22", 262 + "@smithy/core": "^3.23.17", 263 + "@smithy/node-config-provider": "^4.3.14", 264 + "@smithy/property-provider": "^4.2.14", 265 + "@smithy/protocol-http": "^5.3.14", 266 + "@smithy/signature-v4": "^5.3.14", 267 + "@smithy/smithy-client": "^4.12.13", 268 + "@smithy/types": "^4.14.1", 269 + "@smithy/util-base64": "^4.3.2", 270 + "@smithy/util-middleware": "^4.2.14", 271 + "@smithy/util-retry": "^4.3.6", 272 + "@smithy/util-utf8": "^4.2.2", 273 + "tslib": "^2.6.2" 274 + }, 275 + "engines": { 276 + "node": ">=20.0.0" 277 + } 278 + }, 279 + "node_modules/@aws-sdk/credential-provider-env": { 280 + "version": "3.972.34", 281 + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.34.tgz", 282 + "integrity": "sha512-XT0jtf8Fw9JE6ppsQeoNnZRiG+jqRixMT1v1ZR17G60UvVdsQmTG8nbEyHuEPfMxDXEhfdARaM/XiEhca4lGHQ==", 283 + "dev": true, 284 + "license": "Apache-2.0", 285 + "dependencies": { 286 + "@aws-sdk/core": "^3.974.8", 287 + "@aws-sdk/types": "^3.973.8", 288 + "@smithy/property-provider": "^4.2.14", 289 + "@smithy/types": "^4.14.1", 290 + "tslib": "^2.6.2" 291 + }, 292 + "engines": { 293 + "node": ">=20.0.0" 294 + } 295 + }, 296 + "node_modules/@aws-sdk/credential-provider-http": { 297 + "version": "3.972.36", 298 + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.36.tgz", 299 + "integrity": "sha512-DPoGWfy7J7RKxvbf5kOKIGQkD2ek3dbKgzKIGrnLuvZBz5myU+Im/H6pmc14QcnFbqHMqxvtWSgRDSJW3qXLQg==", 300 + "dev": true, 301 + "license": "Apache-2.0", 302 + "dependencies": { 303 + "@aws-sdk/core": "^3.974.8", 304 + "@aws-sdk/types": "^3.973.8", 305 + "@smithy/fetch-http-handler": "^5.3.17", 306 + "@smithy/node-http-handler": "^4.6.1", 307 + "@smithy/property-provider": "^4.2.14", 308 + "@smithy/protocol-http": "^5.3.14", 309 + "@smithy/smithy-client": "^4.12.13", 310 + "@smithy/types": "^4.14.1", 311 + "@smithy/util-stream": "^4.5.25", 312 + "tslib": "^2.6.2" 313 + }, 314 + "engines": { 315 + "node": ">=20.0.0" 316 + } 317 + }, 318 + "node_modules/@aws-sdk/credential-provider-ini": { 319 + "version": "3.972.38", 320 + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.38.tgz", 321 + "integrity": "sha512-oDzUBu2MGJFgoar05sPMCwSrhw44ASyccrHzj66vO69OZqi7I6hZZxXfuPLC8OCzW7C+sU+bI73XHij41yekgQ==", 322 + "dev": true, 323 + "license": "Apache-2.0", 324 + "dependencies": { 325 + "@aws-sdk/core": "^3.974.8", 326 + "@aws-sdk/credential-provider-env": "^3.972.34", 327 + "@aws-sdk/credential-provider-http": "^3.972.36", 328 + "@aws-sdk/credential-provider-login": "^3.972.38", 329 + "@aws-sdk/credential-provider-process": "^3.972.34", 330 + "@aws-sdk/credential-provider-sso": "^3.972.38", 331 + "@aws-sdk/credential-provider-web-identity": "^3.972.38", 332 + "@aws-sdk/nested-clients": "^3.997.6", 333 + "@aws-sdk/types": "^3.973.8", 334 + "@smithy/credential-provider-imds": "^4.2.14", 335 + "@smithy/property-provider": "^4.2.14", 336 + "@smithy/shared-ini-file-loader": "^4.4.9", 337 + "@smithy/types": "^4.14.1", 338 + "tslib": "^2.6.2" 339 + }, 340 + "engines": { 341 + "node": ">=20.0.0" 342 + } 343 + }, 344 + "node_modules/@aws-sdk/credential-provider-login": { 345 + "version": "3.972.38", 346 + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.38.tgz", 347 + "integrity": "sha512-g1NosS8qe4OF++G2UFCM5ovSkgipC7YYor5KCWatG0UoMSO5YFj9C8muePlyVmOBV/WTI16Jo3/s1NUo/o1Bww==", 348 + "dev": true, 349 + "license": "Apache-2.0", 350 + "dependencies": { 351 + "@aws-sdk/core": "^3.974.8", 352 + "@aws-sdk/nested-clients": "^3.997.6", 353 + "@aws-sdk/types": "^3.973.8", 354 + "@smithy/property-provider": "^4.2.14", 355 + "@smithy/protocol-http": "^5.3.14", 356 + "@smithy/shared-ini-file-loader": "^4.4.9", 357 + "@smithy/types": "^4.14.1", 358 + "tslib": "^2.6.2" 359 + }, 360 + "engines": { 361 + "node": ">=20.0.0" 362 + } 363 + }, 364 + "node_modules/@aws-sdk/credential-provider-node": { 365 + "version": "3.972.39", 366 + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.39.tgz", 367 + "integrity": "sha512-HEswDQyxUtadoZ/bJsPPENHg7R0Lzym5LuMksJeHvqhCOpP+rtkDLKI4/ZChH4w3cf5kG8n6bZuI8PzajoiqMg==", 368 + "dev": true, 369 + "license": "Apache-2.0", 370 + "dependencies": { 371 + "@aws-sdk/credential-provider-env": "^3.972.34", 372 + "@aws-sdk/credential-provider-http": "^3.972.36", 373 + "@aws-sdk/credential-provider-ini": "^3.972.38", 374 + "@aws-sdk/credential-provider-process": "^3.972.34", 375 + "@aws-sdk/credential-provider-sso": "^3.972.38", 376 + "@aws-sdk/credential-provider-web-identity": "^3.972.38", 377 + "@aws-sdk/types": "^3.973.8", 378 + "@smithy/credential-provider-imds": "^4.2.14", 379 + "@smithy/property-provider": "^4.2.14", 380 + "@smithy/shared-ini-file-loader": "^4.4.9", 381 + "@smithy/types": "^4.14.1", 382 + "tslib": "^2.6.2" 383 + }, 384 + "engines": { 385 + "node": ">=20.0.0" 386 + } 387 + }, 388 + "node_modules/@aws-sdk/credential-provider-process": { 389 + "version": "3.972.34", 390 + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.34.tgz", 391 + "integrity": "sha512-T3IFs4EVmVi1dVN5RciFnklCANSzvrQd/VuHY9ThHSQmYkTogjcGkoJEr+oNUPQZnso52183088NqysMPji1/Q==", 392 + "dev": true, 393 + "license": "Apache-2.0", 394 + "dependencies": { 395 + "@aws-sdk/core": "^3.974.8", 396 + "@aws-sdk/types": "^3.973.8", 397 + "@smithy/property-provider": "^4.2.14", 398 + "@smithy/shared-ini-file-loader": "^4.4.9", 399 + "@smithy/types": "^4.14.1", 400 + "tslib": "^2.6.2" 401 + }, 402 + "engines": { 403 + "node": ">=20.0.0" 404 + } 405 + }, 406 + "node_modules/@aws-sdk/credential-provider-sso": { 407 + "version": "3.972.38", 408 + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.38.tgz", 409 + "integrity": "sha512-5ZxG+t0+3Q3QPh8KEjX6syskhgNf7I0MN7oGioTf6Lm1NTjfP7sIcYGNsthXC2qR8vcD3edNZwCr2ovfSSWuRA==", 410 + "dev": true, 411 + "license": "Apache-2.0", 412 + "dependencies": { 413 + "@aws-sdk/core": "^3.974.8", 414 + "@aws-sdk/nested-clients": "^3.997.6", 415 + "@aws-sdk/token-providers": "3.1041.0", 416 + "@aws-sdk/types": "^3.973.8", 417 + "@smithy/property-provider": "^4.2.14", 418 + "@smithy/shared-ini-file-loader": "^4.4.9", 419 + "@smithy/types": "^4.14.1", 420 + "tslib": "^2.6.2" 421 + }, 422 + "engines": { 423 + "node": ">=20.0.0" 424 + } 425 + }, 426 + "node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers": { 427 + "version": "3.1041.0", 428 + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1041.0.tgz", 429 + "integrity": "sha512-Th7kPI6YPtvJUcdznooXJMy+9rQWjmEF81LxaJssngBzuysK4a/x+l8kjm1zb7nYsUPbndnBdUnwng/3PLvtGw==", 430 + "dev": true, 431 + "license": "Apache-2.0", 432 + "dependencies": { 433 + "@aws-sdk/core": "^3.974.8", 434 + "@aws-sdk/nested-clients": "^3.997.6", 435 + "@aws-sdk/types": "^3.973.8", 436 + "@smithy/property-provider": "^4.2.14", 437 + "@smithy/shared-ini-file-loader": "^4.4.9", 438 + "@smithy/types": "^4.14.1", 439 + "tslib": "^2.6.2" 440 + }, 441 + "engines": { 442 + "node": ">=20.0.0" 443 + } 444 + }, 445 + "node_modules/@aws-sdk/credential-provider-web-identity": { 446 + "version": "3.972.38", 447 + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.38.tgz", 448 + "integrity": "sha512-lYHFF30DGI20jZcYX8cm6Ns0V7f1dDN6g/MBDLTyD/5iw+bXs3yBr2iAiHDkx4RFU5JgsnZvCHYKiRVPRdmOgw==", 449 + "dev": true, 450 + "license": "Apache-2.0", 451 + "dependencies": { 452 + "@aws-sdk/core": "^3.974.8", 453 + "@aws-sdk/nested-clients": "^3.997.6", 454 + "@aws-sdk/types": "^3.973.8", 455 + "@smithy/property-provider": "^4.2.14", 456 + "@smithy/shared-ini-file-loader": "^4.4.9", 457 + "@smithy/types": "^4.14.1", 458 + "tslib": "^2.6.2" 459 + }, 460 + "engines": { 461 + "node": ">=20.0.0" 462 + } 463 + }, 464 + "node_modules/@aws-sdk/eventstream-handler-node": { 465 + "version": "3.972.14", 466 + "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.14.tgz", 467 + "integrity": "sha512-m4X56gxG76/CKfxNVbOFuYwnAZcHgS6HOH8lgp15HoGHIAVTcZfZrXvcYzJFOMLEJgVn+JHBu6EiNV+xSNXXFg==", 468 + "dev": true, 469 + "license": "Apache-2.0", 470 + "dependencies": { 471 + "@aws-sdk/types": "^3.973.8", 472 + "@smithy/eventstream-codec": "^4.2.14", 473 + "@smithy/types": "^4.14.1", 474 + "tslib": "^2.6.2" 475 + }, 476 + "engines": { 477 + "node": ">=20.0.0" 478 + } 479 + }, 480 + "node_modules/@aws-sdk/middleware-eventstream": { 481 + "version": "3.972.10", 482 + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.10.tgz", 483 + "integrity": "sha512-QUqLs7Af1II9X4fCRAu+EGHG3KHyOp4RkuLhRKoA3NuFlh6TL8i+zXBl8w2LUxqm44B/Kom45hgSlwA1SpTsXQ==", 484 + "dev": true, 485 + "license": "Apache-2.0", 486 + "dependencies": { 487 + "@aws-sdk/types": "^3.973.8", 488 + "@smithy/protocol-http": "^5.3.14", 489 + "@smithy/types": "^4.14.1", 490 + "tslib": "^2.6.2" 491 + }, 492 + "engines": { 493 + "node": ">=20.0.0" 494 + } 495 + }, 496 + "node_modules/@aws-sdk/middleware-host-header": { 497 + "version": "3.972.10", 498 + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.10.tgz", 499 + "integrity": "sha512-IJSsIMeVQ8MMCPbuh1AbltkFhLBLXn7aejzfX5YKT/VLDHn++Dcz8886tXckE+wQssyPUhaXrJhdakO2VilRhg==", 500 + "dev": true, 501 + "license": "Apache-2.0", 502 + "dependencies": { 503 + "@aws-sdk/types": "^3.973.8", 504 + "@smithy/protocol-http": "^5.3.14", 505 + "@smithy/types": "^4.14.1", 506 + "tslib": "^2.6.2" 507 + }, 508 + "engines": { 509 + "node": ">=20.0.0" 510 + } 511 + }, 512 + "node_modules/@aws-sdk/middleware-logger": { 513 + "version": "3.972.10", 514 + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.10.tgz", 515 + "integrity": "sha512-OOuGvvz1Dm20SjZo5oEBePFqxt5nf8AwkNDSyUHvD9/bfNASmstcYxFAHUowy4n6Io7mWUZ04JURZwSBvyQanQ==", 516 + "dev": true, 517 + "license": "Apache-2.0", 518 + "dependencies": { 519 + "@aws-sdk/types": "^3.973.8", 520 + "@smithy/types": "^4.14.1", 521 + "tslib": "^2.6.2" 522 + }, 523 + "engines": { 524 + "node": ">=20.0.0" 525 + } 526 + }, 527 + "node_modules/@aws-sdk/middleware-recursion-detection": { 528 + "version": "3.972.11", 529 + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.11.tgz", 530 + "integrity": "sha512-+zz6f79Kj9V5qFK2P+D8Ehjnw4AhphAlCAsPjUqEcInA9umtSSKMrHbSagEeOIsDNuvVrH98bjRHcyQukTrhaQ==", 531 + "dev": true, 532 + "license": "Apache-2.0", 533 + "dependencies": { 534 + "@aws-sdk/types": "^3.973.8", 535 + "@aws/lambda-invoke-store": "^0.2.2", 536 + "@smithy/protocol-http": "^5.3.14", 537 + "@smithy/types": "^4.14.1", 538 + "tslib": "^2.6.2" 539 + }, 540 + "engines": { 541 + "node": ">=20.0.0" 542 + } 543 + }, 544 + "node_modules/@aws-sdk/middleware-sdk-s3": { 545 + "version": "3.972.37", 546 + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.37.tgz", 547 + "integrity": "sha512-Km7M+i8DrLArVzrid1gfxeGhYHBd3uxvE77g0s5a52zPSVosxzQBnJ0gwWb6NIp/DOk8gsBMhi7V+cpJG0ndTA==", 548 + "dev": true, 549 + "license": "Apache-2.0", 550 + "dependencies": { 551 + "@aws-sdk/core": "^3.974.8", 552 + "@aws-sdk/types": "^3.973.8", 553 + "@aws-sdk/util-arn-parser": "^3.972.3", 554 + "@smithy/core": "^3.23.17", 555 + "@smithy/node-config-provider": "^4.3.14", 556 + "@smithy/protocol-http": "^5.3.14", 557 + "@smithy/signature-v4": "^5.3.14", 558 + "@smithy/smithy-client": "^4.12.13", 559 + "@smithy/types": "^4.14.1", 560 + "@smithy/util-config-provider": "^4.2.2", 561 + "@smithy/util-middleware": "^4.2.14", 562 + "@smithy/util-stream": "^4.5.25", 563 + "@smithy/util-utf8": "^4.2.2", 564 + "tslib": "^2.6.2" 565 + }, 566 + "engines": { 567 + "node": ">=20.0.0" 568 + } 569 + }, 570 + "node_modules/@aws-sdk/middleware-user-agent": { 571 + "version": "3.972.38", 572 + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.38.tgz", 573 + "integrity": "sha512-iz+B29TXcAZsJpwB+AwG/TTGA5l/VnmMZ2UxtiySOZjI6gCdmviXPwdgzcmuazMy16rXoPY4mYCGe7zdNKfx5A==", 574 + "dev": true, 575 + "license": "Apache-2.0", 576 + "dependencies": { 577 + "@aws-sdk/core": "^3.974.8", 578 + "@aws-sdk/types": "^3.973.8", 579 + "@aws-sdk/util-endpoints": "^3.996.8", 580 + "@smithy/core": "^3.23.17", 581 + "@smithy/protocol-http": "^5.3.14", 582 + "@smithy/types": "^4.14.1", 583 + "@smithy/util-retry": "^4.3.6", 584 + "tslib": "^2.6.2" 585 + }, 586 + "engines": { 587 + "node": ">=20.0.0" 588 + } 589 + }, 590 + "node_modules/@aws-sdk/middleware-websocket": { 591 + "version": "3.972.16", 592 + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.16.tgz", 593 + "integrity": "sha512-86+S9oCyRVGzoMRpQhxkArp7kD2K75GPmaNevd9B6EyNhWoNvnCZZ3WbgN4j7ZT+jvtvBCGZvI2XHsWZJ+BRIg==", 594 + "dev": true, 595 + "license": "Apache-2.0", 596 + "dependencies": { 597 + "@aws-sdk/types": "^3.973.8", 598 + "@aws-sdk/util-format-url": "^3.972.10", 599 + "@smithy/eventstream-codec": "^4.2.14", 600 + "@smithy/eventstream-serde-browser": "^4.2.14", 601 + "@smithy/fetch-http-handler": "^5.3.17", 602 + "@smithy/protocol-http": "^5.3.14", 603 + "@smithy/signature-v4": "^5.3.14", 604 + "@smithy/types": "^4.14.1", 605 + "@smithy/util-base64": "^4.3.2", 606 + "@smithy/util-hex-encoding": "^4.2.2", 607 + "@smithy/util-utf8": "^4.2.2", 608 + "tslib": "^2.6.2" 609 + }, 610 + "engines": { 611 + "node": ">= 14.0.0" 612 + } 613 + }, 614 + "node_modules/@aws-sdk/nested-clients": { 615 + "version": "3.997.6", 616 + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.6.tgz", 617 + "integrity": "sha512-WBDnqatJl+kGObpfmfSxqnXeYTu3Me8wx8WCtvoxX3pfWrrTv8I4WTMSSs7PZqcRcVh8WeUKMgGFjMG+52SR1w==", 618 + "dev": true, 619 + "license": "Apache-2.0", 620 + "dependencies": { 621 + "@aws-crypto/sha256-browser": "5.2.0", 622 + "@aws-crypto/sha256-js": "5.2.0", 623 + "@aws-sdk/core": "^3.974.8", 624 + "@aws-sdk/middleware-host-header": "^3.972.10", 625 + "@aws-sdk/middleware-logger": "^3.972.10", 626 + "@aws-sdk/middleware-recursion-detection": "^3.972.11", 627 + "@aws-sdk/middleware-user-agent": "^3.972.38", 628 + "@aws-sdk/region-config-resolver": "^3.972.13", 629 + "@aws-sdk/signature-v4-multi-region": "^3.996.25", 630 + "@aws-sdk/types": "^3.973.8", 631 + "@aws-sdk/util-endpoints": "^3.996.8", 632 + "@aws-sdk/util-user-agent-browser": "^3.972.10", 633 + "@aws-sdk/util-user-agent-node": "^3.973.24", 634 + "@smithy/config-resolver": "^4.4.17", 635 + "@smithy/core": "^3.23.17", 636 + "@smithy/fetch-http-handler": "^5.3.17", 637 + "@smithy/hash-node": "^4.2.14", 638 + "@smithy/invalid-dependency": "^4.2.14", 639 + "@smithy/middleware-content-length": "^4.2.14", 640 + "@smithy/middleware-endpoint": "^4.4.32", 641 + "@smithy/middleware-retry": "^4.5.7", 642 + "@smithy/middleware-serde": "^4.2.20", 643 + "@smithy/middleware-stack": "^4.2.14", 644 + "@smithy/node-config-provider": "^4.3.14", 645 + "@smithy/node-http-handler": "^4.6.1", 646 + "@smithy/protocol-http": "^5.3.14", 647 + "@smithy/smithy-client": "^4.12.13", 648 + "@smithy/types": "^4.14.1", 649 + "@smithy/url-parser": "^4.2.14", 650 + "@smithy/util-base64": "^4.3.2", 651 + "@smithy/util-body-length-browser": "^4.2.2", 652 + "@smithy/util-body-length-node": "^4.2.3", 653 + "@smithy/util-defaults-mode-browser": "^4.3.49", 654 + "@smithy/util-defaults-mode-node": "^4.2.54", 655 + "@smithy/util-endpoints": "^3.4.2", 656 + "@smithy/util-middleware": "^4.2.14", 657 + "@smithy/util-retry": "^4.3.6", 658 + "@smithy/util-utf8": "^4.2.2", 659 + "tslib": "^2.6.2" 660 + }, 661 + "engines": { 662 + "node": ">=20.0.0" 663 + } 664 + }, 665 + "node_modules/@aws-sdk/region-config-resolver": { 666 + "version": "3.972.13", 667 + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.13.tgz", 668 + "integrity": "sha512-CvJ2ZIjK/jVD/lbOpowBVElJyC1YxLTIJ13yM0AEo0t2v7swOzGjSA6lJGH+DwZXQhcjUjoYwc8bVYCX5MDr1A==", 669 + "dev": true, 670 + "license": "Apache-2.0", 671 + "dependencies": { 672 + "@aws-sdk/types": "^3.973.8", 673 + "@smithy/config-resolver": "^4.4.17", 674 + "@smithy/node-config-provider": "^4.3.14", 675 + "@smithy/types": "^4.14.1", 676 + "tslib": "^2.6.2" 677 + }, 678 + "engines": { 679 + "node": ">=20.0.0" 680 + } 681 + }, 682 + "node_modules/@aws-sdk/signature-v4-multi-region": { 683 + "version": "3.996.25", 684 + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.25.tgz", 685 + "integrity": "sha512-+CMIt3e1VzlklAECmG+DtP1sV8iKq25FuA0OKpnJ4KA0kxUtd7CgClY7/RU6VzJBQwbN4EJ9Ue6plvqx1qGadw==", 686 + "dev": true, 687 + "license": "Apache-2.0", 688 + "dependencies": { 689 + "@aws-sdk/middleware-sdk-s3": "^3.972.37", 690 + "@aws-sdk/types": "^3.973.8", 691 + "@smithy/protocol-http": "^5.3.14", 692 + "@smithy/signature-v4": "^5.3.14", 693 + "@smithy/types": "^4.14.1", 694 + "tslib": "^2.6.2" 695 + }, 696 + "engines": { 697 + "node": ">=20.0.0" 698 + } 699 + }, 700 + "node_modules/@aws-sdk/token-providers": { 701 + "version": "3.1045.0", 702 + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1045.0.tgz", 703 + "integrity": "sha512-/o4qcty0DmQola0DBniRVeBakYY6ALOvKEFo1AtJpTmMn/cJ+Fk3RWGe5ieT/f/eYbHG9k5E7poKge/E+WGv4Q==", 704 + "dev": true, 705 + "license": "Apache-2.0", 706 + "dependencies": { 707 + "@aws-sdk/core": "^3.974.8", 708 + "@aws-sdk/nested-clients": "^3.997.6", 709 + "@aws-sdk/types": "^3.973.8", 710 + "@smithy/property-provider": "^4.2.14", 711 + "@smithy/shared-ini-file-loader": "^4.4.9", 712 + "@smithy/types": "^4.14.1", 713 + "tslib": "^2.6.2" 714 + }, 715 + "engines": { 716 + "node": ">=20.0.0" 717 + } 718 + }, 719 + "node_modules/@aws-sdk/types": { 720 + "version": "3.973.8", 721 + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.8.tgz", 722 + "integrity": "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw==", 723 + "dev": true, 724 + "license": "Apache-2.0", 725 + "dependencies": { 726 + "@smithy/types": "^4.14.1", 727 + "tslib": "^2.6.2" 728 + }, 729 + "engines": { 730 + "node": ">=20.0.0" 731 + } 732 + }, 733 + "node_modules/@aws-sdk/util-arn-parser": { 734 + "version": "3.972.3", 735 + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.972.3.tgz", 736 + "integrity": "sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA==", 737 + "dev": true, 738 + "license": "Apache-2.0", 739 + "dependencies": { 740 + "tslib": "^2.6.2" 741 + }, 742 + "engines": { 743 + "node": ">=20.0.0" 744 + } 745 + }, 746 + "node_modules/@aws-sdk/util-endpoints": { 747 + "version": "3.996.8", 748 + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.8.tgz", 749 + "integrity": "sha512-oOZHcRDihk5iEe5V25NVWg45b3qEA8OpHWVdU/XQh8Zj4heVPAJqWvMphQnU7LkufmUo10EpvFPZuQMiFLJK3g==", 750 + "dev": true, 751 + "license": "Apache-2.0", 752 + "dependencies": { 753 + "@aws-sdk/types": "^3.973.8", 754 + "@smithy/types": "^4.14.1", 755 + "@smithy/url-parser": "^4.2.14", 756 + "@smithy/util-endpoints": "^3.4.2", 757 + "tslib": "^2.6.2" 758 + }, 759 + "engines": { 760 + "node": ">=20.0.0" 761 + } 762 + }, 763 + "node_modules/@aws-sdk/util-format-url": { 764 + "version": "3.972.10", 765 + "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.972.10.tgz", 766 + "integrity": "sha512-DEKiHNJVtNxdyTeQspzY+15Po/kHm6sF0Cs4HV9Q2+lplB63+DrvdeiSoOSdWEWAoO2RcY1veoXVDz2tWxWCgQ==", 767 + "dev": true, 768 + "license": "Apache-2.0", 769 + "dependencies": { 770 + "@aws-sdk/types": "^3.973.8", 771 + "@smithy/querystring-builder": "^4.2.14", 772 + "@smithy/types": "^4.14.1", 773 + "tslib": "^2.6.2" 774 + }, 775 + "engines": { 776 + "node": ">=20.0.0" 777 + } 778 + }, 779 + "node_modules/@aws-sdk/util-locate-window": { 780 + "version": "3.965.5", 781 + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.5.tgz", 782 + "integrity": "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==", 783 + "dev": true, 784 + "license": "Apache-2.0", 785 + "dependencies": { 786 + "tslib": "^2.6.2" 787 + }, 788 + "engines": { 789 + "node": ">=20.0.0" 790 + } 791 + }, 792 + "node_modules/@aws-sdk/util-user-agent-browser": { 793 + "version": "3.972.10", 794 + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.10.tgz", 795 + "integrity": "sha512-FAzqXvfEssGdSIz8ejatan0bOdx1qefBWKF/gWmVBXIP1HkS7v/wjjaqrAGGKvyihrXTXW00/2/1nTJtxpXz7g==", 796 + "dev": true, 797 + "license": "Apache-2.0", 798 + "dependencies": { 799 + "@aws-sdk/types": "^3.973.8", 800 + "@smithy/types": "^4.14.1", 801 + "bowser": "^2.11.0", 802 + "tslib": "^2.6.2" 803 + } 804 + }, 805 + "node_modules/@aws-sdk/util-user-agent-node": { 806 + "version": "3.973.24", 807 + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.24.tgz", 808 + "integrity": "sha512-ZWwlkjcIp7cEL8ZfTpTAPNkwx25p7xol0xlKoWVVf22+nsjwmLcHYtTPjIV1cSpmB/b6DaK4cb1fSkvCXHgRdw==", 809 + "dev": true, 810 + "license": "Apache-2.0", 811 + "dependencies": { 812 + "@aws-sdk/middleware-user-agent": "^3.972.38", 813 + "@aws-sdk/types": "^3.973.8", 814 + "@smithy/node-config-provider": "^4.3.14", 815 + "@smithy/types": "^4.14.1", 816 + "@smithy/util-config-provider": "^4.2.2", 817 + "tslib": "^2.6.2" 818 + }, 819 + "engines": { 820 + "node": ">=20.0.0" 821 + }, 822 + "peerDependencies": { 823 + "aws-crt": ">=1.0.0" 824 + }, 825 + "peerDependenciesMeta": { 826 + "aws-crt": { 827 + "optional": true 828 + } 829 + } 830 + }, 831 + "node_modules/@aws-sdk/xml-builder": { 832 + "version": "3.972.22", 833 + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.22.tgz", 834 + "integrity": "sha512-PMYKKtJd70IsSG0yHrdAbxBr+ZWBKLvzFZfD3/urxgf6hXVMzuU5M+3MJ5G67RpOmLBu1fAUN65SbWuKUCOlAA==", 835 + "dev": true, 836 + "license": "Apache-2.0", 837 + "dependencies": { 838 + "@nodable/entities": "2.1.0", 839 + "@smithy/types": "^4.14.1", 840 + "fast-xml-parser": "5.7.2", 841 + "tslib": "^2.6.2" 842 + }, 843 + "engines": { 844 + "node": ">=20.0.0" 845 + } 846 + }, 847 + "node_modules/@aws/lambda-invoke-store": { 848 + "version": "0.2.4", 849 + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz", 850 + "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==", 851 + "dev": true, 852 + "license": "Apache-2.0", 853 + "engines": { 854 + "node": ">=18.0.0" 855 + } 856 + }, 857 + "node_modules/@babel/runtime": { 858 + "version": "7.29.2", 859 + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", 860 + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", 861 + "dev": true, 862 + "license": "MIT", 863 + "engines": { 864 + "node": ">=6.9.0" 865 + } 866 + }, 867 + "node_modules/@borewit/text-codec": { 868 + "version": "0.2.2", 869 + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz", 870 + "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==", 871 + "dev": true, 872 + "license": "MIT", 873 + "funding": { 874 + "type": "github", 875 + "url": "https://github.com/sponsors/Borewit" 876 + } 877 + }, 878 + "node_modules/@google/genai": { 879 + "version": "1.52.0", 880 + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.52.0.tgz", 881 + "integrity": "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==", 882 + "dev": true, 883 + "hasInstallScript": true, 884 + "license": "Apache-2.0", 885 + "dependencies": { 886 + "google-auth-library": "^10.3.0", 887 + "p-retry": "^4.6.2", 888 + "protobufjs": "^7.5.4", 889 + "ws": "^8.18.0" 890 + }, 891 + "engines": { 892 + "node": ">=20.0.0" 893 + }, 894 + "peerDependencies": { 895 + "@modelcontextprotocol/sdk": "^1.25.2" 896 + }, 897 + "peerDependenciesMeta": { 898 + "@modelcontextprotocol/sdk": { 899 + "optional": true 900 + } 901 + } 902 + }, 903 + "node_modules/@mariozechner/clipboard": { 904 + "version": "0.3.5", 905 + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard/-/clipboard-0.3.5.tgz", 906 + "integrity": "sha512-D3F+UrU9CR7roJt0zDLp6Oc+4/KlLDIrN4frH+6V90SJNW2KKUec1oCQIPaaDjCqeOsQyX9dyqYbImIQIM45PA==", 907 + "dev": true, 908 + "license": "MIT", 909 + "optional": true, 910 + "engines": { 911 + "node": ">= 10" 912 + }, 913 + "optionalDependencies": { 914 + "@mariozechner/clipboard-darwin-arm64": "0.3.2", 915 + "@mariozechner/clipboard-darwin-universal": "0.3.2", 916 + "@mariozechner/clipboard-darwin-x64": "0.3.2", 917 + "@mariozechner/clipboard-linux-arm64-gnu": "0.3.2", 918 + "@mariozechner/clipboard-linux-arm64-musl": "0.3.2", 919 + "@mariozechner/clipboard-linux-riscv64-gnu": "0.3.2", 920 + "@mariozechner/clipboard-linux-x64-gnu": "0.3.2", 921 + "@mariozechner/clipboard-linux-x64-musl": "0.3.2", 922 + "@mariozechner/clipboard-win32-arm64-msvc": "0.3.2", 923 + "@mariozechner/clipboard-win32-x64-msvc": "0.3.2" 924 + } 925 + }, 926 + "node_modules/@mariozechner/clipboard-darwin-arm64": { 927 + "version": "0.3.2", 928 + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-arm64/-/clipboard-darwin-arm64-0.3.2.tgz", 929 + "integrity": "sha512-uBf6K7Je1ihsgvmWxA8UCGCeI+nbRVRXoarZdLjl6slz94Zs1tNKFZqx7aCI5O1i3e0B6ja82zZ06BWrl0MCVw==", 930 + "cpu": [ 931 + "arm64" 932 + ], 933 + "dev": true, 934 + "license": "MIT", 935 + "optional": true, 936 + "os": [ 937 + "darwin" 938 + ], 939 + "engines": { 940 + "node": ">= 10" 941 + } 942 + }, 943 + "node_modules/@mariozechner/clipboard-darwin-universal": { 944 + "version": "0.3.2", 945 + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-universal/-/clipboard-darwin-universal-0.3.2.tgz", 946 + "integrity": "sha512-mxSheKTW2U9LsBdXy0SdmdCAE5HqNS9QUmpNHLnfJ+SsbFKALjEZc5oRrVMXxGQSirDvYf5bjmRyT0QYYonnlg==", 947 + "dev": true, 948 + "license": "MIT", 949 + "optional": true, 950 + "os": [ 951 + "darwin" 952 + ], 953 + "engines": { 954 + "node": ">= 10" 955 + } 956 + }, 957 + "node_modules/@mariozechner/clipboard-darwin-x64": { 958 + "version": "0.3.2", 959 + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-x64/-/clipboard-darwin-x64-0.3.2.tgz", 960 + "integrity": "sha512-U1BcVEoidvwIp95+HJswSW+xr28EQiHR7rZjH6pn8Sja5yO4Yoe3yCN0Zm8Lo72BbSOK/fTSq0je7CJpaPCspg==", 961 + "cpu": [ 962 + "x64" 963 + ], 964 + "dev": true, 965 + "license": "MIT", 966 + "optional": true, 967 + "os": [ 968 + "darwin" 969 + ], 970 + "engines": { 971 + "node": ">= 10" 972 + } 973 + }, 974 + "node_modules/@mariozechner/clipboard-linux-arm64-gnu": { 975 + "version": "0.3.2", 976 + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-gnu/-/clipboard-linux-arm64-gnu-0.3.2.tgz", 977 + "integrity": "sha512-BsinwG3yWTIjdgNCxsFlip7LkfwPk+ruw/aFCXHUg/fb5XC/Ksp+YMQ7u0LUtiKzIv/7LMXgZInJQH6gxbAaqQ==", 978 + "cpu": [ 979 + "arm64" 980 + ], 981 + "dev": true, 982 + "libc": [ 983 + "glibc" 984 + ], 985 + "license": "MIT", 986 + "optional": true, 987 + "os": [ 988 + "linux" 989 + ], 990 + "engines": { 991 + "node": ">= 10" 992 + } 993 + }, 994 + "node_modules/@mariozechner/clipboard-linux-arm64-musl": { 995 + "version": "0.3.2", 996 + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-musl/-/clipboard-linux-arm64-musl-0.3.2.tgz", 997 + "integrity": "sha512-0/Gi5Xq2V6goXBop19ePoHvXsmJD9SzFlO3S+d6+T2b+BlPcpOu3Oa0wTjl+cZrLAAEzA86aPNBI+VVAFDFPKw==", 998 + "cpu": [ 999 + "arm64" 1000 + ], 1001 + "dev": true, 1002 + "libc": [ 1003 + "musl" 1004 + ], 1005 + "license": "MIT", 1006 + "optional": true, 1007 + "os": [ 1008 + "linux" 1009 + ], 1010 + "engines": { 1011 + "node": ">= 10" 1012 + } 1013 + }, 1014 + "node_modules/@mariozechner/clipboard-linux-riscv64-gnu": { 1015 + "version": "0.3.2", 1016 + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-riscv64-gnu/-/clipboard-linux-riscv64-gnu-0.3.2.tgz", 1017 + "integrity": "sha512-2AFFiXB24qf0zOZsxI1GJGb9wQGlOJyN6UwoXqmKS3dpQi/l6ix30IzDDA4c4ZcCcx4D+9HLYXhC1w7Sov8pXA==", 1018 + "cpu": [ 1019 + "riscv64" 1020 + ], 1021 + "dev": true, 1022 + "libc": [ 1023 + "glibc" 1024 + ], 1025 + "license": "MIT", 1026 + "optional": true, 1027 + "os": [ 1028 + "linux" 1029 + ], 1030 + "engines": { 1031 + "node": ">= 10" 1032 + } 1033 + }, 1034 + "node_modules/@mariozechner/clipboard-linux-x64-gnu": { 1035 + "version": "0.3.2", 1036 + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-gnu/-/clipboard-linux-x64-gnu-0.3.2.tgz", 1037 + "integrity": "sha512-v6fVnsn7WMGg73Dab8QMwyFce7tzGfgEixKgzLP8f1GJqkJZi5zO4k4FOHzSgUufgLil63gnxvMpjWkgfeQN7A==", 1038 + "cpu": [ 1039 + "x64" 1040 + ], 1041 + "dev": true, 1042 + "libc": [ 1043 + "glibc" 1044 + ], 1045 + "license": "MIT", 1046 + "optional": true, 1047 + "os": [ 1048 + "linux" 1049 + ], 1050 + "engines": { 1051 + "node": ">= 10" 1052 + } 1053 + }, 1054 + "node_modules/@mariozechner/clipboard-linux-x64-musl": { 1055 + "version": "0.3.2", 1056 + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-musl/-/clipboard-linux-x64-musl-0.3.2.tgz", 1057 + "integrity": "sha512-xVUtnoMQ8v2JVyfJLKKXACA6avdnchdbBkTsZs8BgJQo29qwCp5NIHAUO8gbJ40iaEGToW5RlmVk2M9V0HsHEw==", 1058 + "cpu": [ 1059 + "x64" 1060 + ], 1061 + "dev": true, 1062 + "libc": [ 1063 + "musl" 1064 + ], 1065 + "license": "MIT", 1066 + "optional": true, 1067 + "os": [ 1068 + "linux" 1069 + ], 1070 + "engines": { 1071 + "node": ">= 10" 1072 + } 1073 + }, 1074 + "node_modules/@mariozechner/clipboard-win32-arm64-msvc": { 1075 + "version": "0.3.2", 1076 + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-arm64-msvc/-/clipboard-win32-arm64-msvc-0.3.2.tgz", 1077 + "integrity": "sha512-AEgg95TNi8TGgak2wSXZkXKCvAUTjWoU1Pqb0ON7JHrX78p616XUFNTJohtIon3e0w6k0pYPZeCuqRCza/Tqeg==", 1078 + "cpu": [ 1079 + "arm64" 1080 + ], 1081 + "dev": true, 1082 + "license": "MIT", 1083 + "optional": true, 1084 + "os": [ 1085 + "win32" 1086 + ], 1087 + "engines": { 1088 + "node": ">= 10" 1089 + } 1090 + }, 1091 + "node_modules/@mariozechner/clipboard-win32-x64-msvc": { 1092 + "version": "0.3.2", 1093 + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-x64-msvc/-/clipboard-win32-x64-msvc-0.3.2.tgz", 1094 + "integrity": "sha512-tGRuYpZwDOD7HBrCpyRuhGnHHSCknELvqwKKUG4JSfSB7JIU7LKRh6zx6fMUOQd8uISK35TjFg5UcNih+vJhFA==", 1095 + "cpu": [ 1096 + "x64" 1097 + ], 1098 + "dev": true, 1099 + "license": "MIT", 1100 + "optional": true, 1101 + "os": [ 1102 + "win32" 1103 + ], 1104 + "engines": { 1105 + "node": ">= 10" 1106 + } 1107 + }, 1108 + "node_modules/@mariozechner/jiti": { 1109 + "version": "2.6.5", 1110 + "resolved": "https://registry.npmjs.org/@mariozechner/jiti/-/jiti-2.6.5.tgz", 1111 + "integrity": "sha512-faGUlTcXka5l7rv0lP3K3vGW/ejRuOS24RR2aSFWREUQqzjgdsuWNo/IiPqL3kWRGt6Ahl2+qcDAwtdeWeuGUw==", 1112 + "dev": true, 1113 + "license": "MIT", 1114 + "dependencies": { 1115 + "std-env": "^3.10.0", 1116 + "yoctocolors": "^2.1.2" 1117 + }, 1118 + "bin": { 1119 + "jiti": "lib/jiti-cli.mjs" 1120 + } 1121 + }, 1122 + "node_modules/@mariozechner/pi-agent-core": { 1123 + "version": "0.72.1", 1124 + "resolved": "https://registry.npmjs.org/@mariozechner/pi-agent-core/-/pi-agent-core-0.72.1.tgz", 1125 + "integrity": "sha512-Z5XH9mUDsBymh7Prtk5Eun4Cs+s9C3uzynug+oWAjzQESjjnCuy7KU6Ek8E9Eg+b9yroCwVw2ItLDxp2E3vYjA==", 1126 + "deprecated": "please use @earendil-works/pi-agent-core instead going forward", 1127 + "dev": true, 1128 + "license": "MIT", 1129 + "dependencies": { 1130 + "@mariozechner/pi-ai": "^0.72.1", 1131 + "typebox": "^1.1.24" 1132 + }, 1133 + "engines": { 1134 + "node": ">=20.0.0" 1135 + } 1136 + }, 1137 + "node_modules/@mariozechner/pi-ai": { 1138 + "version": "0.72.1", 1139 + "resolved": "https://registry.npmjs.org/@mariozechner/pi-ai/-/pi-ai-0.72.1.tgz", 1140 + "integrity": "sha512-mOq71Pjnu72xxzwrh52VIiNwt+/a+Wpa11k5segi01/zTZJt8eMDc5Q2z6GhczYMr5+6EpZ8T+BaeHqq0jk5ag==", 1141 + "deprecated": "please use @earendil-works/pi-ai instead going forward", 1142 + "dev": true, 1143 + "license": "MIT", 1144 + "dependencies": { 1145 + "@anthropic-ai/sdk": "^0.91.1", 1146 + "@aws-sdk/client-bedrock-runtime": "^3.1030.0", 1147 + "@google/genai": "^1.40.0", 1148 + "@mistralai/mistralai": "^2.2.0", 1149 + "chalk": "^5.6.2", 1150 + "openai": "6.26.0", 1151 + "partial-json": "^0.1.7", 1152 + "proxy-agent": "^6.5.0", 1153 + "typebox": "^1.1.24", 1154 + "undici": "^7.19.1", 1155 + "zod-to-json-schema": "^3.24.6" 1156 + }, 1157 + "bin": { 1158 + "pi-ai": "dist/cli.js" 1159 + }, 1160 + "engines": { 1161 + "node": ">=20.0.0" 1162 + } 1163 + }, 1164 + "node_modules/@mariozechner/pi-coding-agent": { 1165 + "version": "0.72.1", 1166 + "resolved": "https://registry.npmjs.org/@mariozechner/pi-coding-agent/-/pi-coding-agent-0.72.1.tgz", 1167 + "integrity": "sha512-gKApiLfAYpvPnWThvwXrLjZIhsziSSUKBph7DHCy/1IomuIGN1MTxS/9ZtcuFqPy3/+VfEUOKegGlY6m+CRNlA==", 1168 + "deprecated": "please use @earendil-works/pi-coding-agent instead going forward", 1169 + "dev": true, 1170 + "license": "MIT", 1171 + "dependencies": { 1172 + "@mariozechner/jiti": "^2.6.2", 1173 + "@mariozechner/pi-agent-core": "^0.72.1", 1174 + "@mariozechner/pi-ai": "^0.72.1", 1175 + "@mariozechner/pi-tui": "^0.72.1", 1176 + "@silvia-odwyer/photon-node": "^0.3.4", 1177 + "chalk": "^5.5.0", 1178 + "cli-highlight": "^2.1.11", 1179 + "diff": "^8.0.2", 1180 + "extract-zip": "^2.0.1", 1181 + "file-type": "^21.1.1", 1182 + "glob": "^13.0.1", 1183 + "hosted-git-info": "^9.0.2", 1184 + "ignore": "^7.0.5", 1185 + "marked": "^15.0.12", 1186 + "minimatch": "^10.2.3", 1187 + "proper-lockfile": "^4.1.2", 1188 + "strip-ansi": "^7.1.0", 1189 + "typebox": "^1.1.24", 1190 + "undici": "^7.19.1", 1191 + "uuid": "^14.0.0", 1192 + "yaml": "^2.8.2" 1193 + }, 1194 + "bin": { 1195 + "pi": "dist/cli.js" 1196 + }, 1197 + "engines": { 1198 + "node": ">=20.6.0" 1199 + }, 1200 + "optionalDependencies": { 1201 + "@mariozechner/clipboard": "^0.3.5" 1202 + } 1203 + }, 1204 + "node_modules/@mariozechner/pi-tui": { 1205 + "version": "0.72.1", 1206 + "resolved": "https://registry.npmjs.org/@mariozechner/pi-tui/-/pi-tui-0.72.1.tgz", 1207 + "integrity": "sha512-KjeqzMQp4vafomfkvI8HKv5/52PqRP5BmlowGC8WKyOaB+u0UkkTdfM5U1Ui3ty2gA7FOfglVRiyvcitiWwvMQ==", 1208 + "deprecated": "please use @earendil-works/pi-tui instead going forward", 1209 + "dev": true, 1210 + "license": "MIT", 1211 + "dependencies": { 1212 + "@types/mime-types": "^2.1.4", 1213 + "chalk": "^5.5.0", 1214 + "get-east-asian-width": "^1.3.0", 1215 + "marked": "^15.0.12", 1216 + "mime-types": "^3.0.1" 1217 + }, 1218 + "engines": { 1219 + "node": ">=20.0.0" 1220 + }, 1221 + "optionalDependencies": { 1222 + "koffi": "^2.9.0" 1223 + } 1224 + }, 1225 + "node_modules/@mistralai/mistralai": { 1226 + "version": "2.2.1", 1227 + "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.1.tgz", 1228 + "integrity": "sha512-uKU8CZmL2RzYKmplsU01hii4p3pe4HqJefpWNRWXm1Tcm0Sm4xXfwSLIy4k7ZCPlbETCGcp69E7hZs+WOJ5itQ==", 1229 + "dev": true, 1230 + "license": "Apache-2.0", 1231 + "dependencies": { 1232 + "ws": "^8.18.0", 1233 + "zod": "^3.25.0 || ^4.0.0", 1234 + "zod-to-json-schema": "^3.25.0" 1235 + } 1236 + }, 1237 + "node_modules/@nodable/entities": { 1238 + "version": "2.1.0", 1239 + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", 1240 + "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==", 1241 + "dev": true, 1242 + "funding": [ 1243 + { 1244 + "type": "github", 1245 + "url": "https://github.com/sponsors/nodable" 1246 + } 1247 + ], 1248 + "license": "MIT" 1249 + }, 1250 + "node_modules/@protobufjs/aspromise": { 1251 + "version": "1.1.2", 1252 + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", 1253 + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", 1254 + "dev": true, 1255 + "license": "BSD-3-Clause" 1256 + }, 1257 + "node_modules/@protobufjs/base64": { 1258 + "version": "1.1.2", 1259 + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", 1260 + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", 1261 + "dev": true, 1262 + "license": "BSD-3-Clause" 1263 + }, 1264 + "node_modules/@protobufjs/codegen": { 1265 + "version": "2.0.5", 1266 + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", 1267 + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", 1268 + "dev": true, 1269 + "license": "BSD-3-Clause" 1270 + }, 1271 + "node_modules/@protobufjs/eventemitter": { 1272 + "version": "1.1.0", 1273 + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", 1274 + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", 1275 + "dev": true, 1276 + "license": "BSD-3-Clause" 1277 + }, 1278 + "node_modules/@protobufjs/fetch": { 1279 + "version": "1.1.0", 1280 + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", 1281 + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", 1282 + "dev": true, 1283 + "license": "BSD-3-Clause", 1284 + "dependencies": { 1285 + "@protobufjs/aspromise": "^1.1.1", 1286 + "@protobufjs/inquire": "^1.1.0" 1287 + } 1288 + }, 1289 + "node_modules/@protobufjs/float": { 1290 + "version": "1.0.2", 1291 + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", 1292 + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", 1293 + "dev": true, 1294 + "license": "BSD-3-Clause" 1295 + }, 1296 + "node_modules/@protobufjs/inquire": { 1297 + "version": "1.1.1", 1298 + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.1.tgz", 1299 + "integrity": "sha512-mnzgDV26ueAvk7rsbt9L7bE0SuAoqyuys/sMMrmVcN5x9VsxpcG3rqAUSgDyLp0UZlmNfIbQ4fHfCtreVBk8Ew==", 1300 + "dev": true, 1301 + "license": "BSD-3-Clause" 1302 + }, 1303 + "node_modules/@protobufjs/path": { 1304 + "version": "1.1.2", 1305 + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", 1306 + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", 1307 + "dev": true, 1308 + "license": "BSD-3-Clause" 1309 + }, 1310 + "node_modules/@protobufjs/pool": { 1311 + "version": "1.1.0", 1312 + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", 1313 + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", 1314 + "dev": true, 1315 + "license": "BSD-3-Clause" 1316 + }, 1317 + "node_modules/@protobufjs/utf8": { 1318 + "version": "1.1.1", 1319 + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", 1320 + "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", 1321 + "dev": true, 1322 + "license": "BSD-3-Clause" 1323 + }, 1324 + "node_modules/@silvia-odwyer/photon-node": { 1325 + "version": "0.3.4", 1326 + "resolved": "https://registry.npmjs.org/@silvia-odwyer/photon-node/-/photon-node-0.3.4.tgz", 1327 + "integrity": "sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==", 1328 + "dev": true, 1329 + "license": "Apache-2.0" 1330 + }, 1331 + "node_modules/@smithy/config-resolver": { 1332 + "version": "4.4.17", 1333 + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.17.tgz", 1334 + "integrity": "sha512-TzDZcAnhTyAHbXVxWZo7/tEcrIeFq20IBk8So3OLOetWpR8EwY/yEqBMBFaJMeyEiREDq4NfEl+qO3OAUD+vbQ==", 1335 + "dev": true, 1336 + "license": "Apache-2.0", 1337 + "dependencies": { 1338 + "@smithy/node-config-provider": "^4.3.14", 1339 + "@smithy/types": "^4.14.1", 1340 + "@smithy/util-config-provider": "^4.2.2", 1341 + "@smithy/util-endpoints": "^3.4.2", 1342 + "@smithy/util-middleware": "^4.2.14", 1343 + "tslib": "^2.6.2" 1344 + }, 1345 + "engines": { 1346 + "node": ">=18.0.0" 1347 + } 1348 + }, 1349 + "node_modules/@smithy/core": { 1350 + "version": "3.23.17", 1351 + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.17.tgz", 1352 + "integrity": "sha512-x7BlLbUFL8NWCGjMF9C+1N5cVCxcPa7g6Tv9B4A2luWx3be3oU8hQ96wIwxe/s7OhIzvoJH73HAUSg5JXVlEtQ==", 1353 + "dev": true, 1354 + "license": "Apache-2.0", 1355 + "dependencies": { 1356 + "@smithy/protocol-http": "^5.3.14", 1357 + "@smithy/types": "^4.14.1", 1358 + "@smithy/url-parser": "^4.2.14", 1359 + "@smithy/util-base64": "^4.3.2", 1360 + "@smithy/util-body-length-browser": "^4.2.2", 1361 + "@smithy/util-middleware": "^4.2.14", 1362 + "@smithy/util-stream": "^4.5.25", 1363 + "@smithy/util-utf8": "^4.2.2", 1364 + "@smithy/uuid": "^1.1.2", 1365 + "tslib": "^2.6.2" 1366 + }, 1367 + "engines": { 1368 + "node": ">=18.0.0" 1369 + } 1370 + }, 1371 + "node_modules/@smithy/credential-provider-imds": { 1372 + "version": "4.2.14", 1373 + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.14.tgz", 1374 + "integrity": "sha512-Au28zBN48ZAoXdooGUHemuVBrkE+Ie6RPmGNIAJsFqj33Vhb6xAgRifUydZ2aY+M+KaMAETAlKk5NC5h1G7wpg==", 1375 + "dev": true, 1376 + "license": "Apache-2.0", 1377 + "dependencies": { 1378 + "@smithy/node-config-provider": "^4.3.14", 1379 + "@smithy/property-provider": "^4.2.14", 1380 + "@smithy/types": "^4.14.1", 1381 + "@smithy/url-parser": "^4.2.14", 1382 + "tslib": "^2.6.2" 1383 + }, 1384 + "engines": { 1385 + "node": ">=18.0.0" 1386 + } 1387 + }, 1388 + "node_modules/@smithy/eventstream-codec": { 1389 + "version": "4.2.14", 1390 + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.14.tgz", 1391 + "integrity": "sha512-erZq0nOIpzfeZdCyzZjdJb4nVSKLUmSkaQUVkRGQTXs30gyUGeKnrYEg+Xe1W5gE3aReS7IgsvANwVPxSzY6Pw==", 1392 + "dev": true, 1393 + "license": "Apache-2.0", 1394 + "dependencies": { 1395 + "@aws-crypto/crc32": "5.2.0", 1396 + "@smithy/types": "^4.14.1", 1397 + "@smithy/util-hex-encoding": "^4.2.2", 1398 + "tslib": "^2.6.2" 1399 + }, 1400 + "engines": { 1401 + "node": ">=18.0.0" 1402 + } 1403 + }, 1404 + "node_modules/@smithy/eventstream-serde-browser": { 1405 + "version": "4.2.14", 1406 + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.14.tgz", 1407 + "integrity": "sha512-8IelTCtTctWRbb+0Dcy+C0aICh1qa0qWXqgjcXDmMuCvPJRnv26hiDZoAau2ILOniki65mCPKqOQs/BaWvO4CQ==", 1408 + "dev": true, 1409 + "license": "Apache-2.0", 1410 + "dependencies": { 1411 + "@smithy/eventstream-serde-universal": "^4.2.14", 1412 + "@smithy/types": "^4.14.1", 1413 + "tslib": "^2.6.2" 1414 + }, 1415 + "engines": { 1416 + "node": ">=18.0.0" 1417 + } 1418 + }, 1419 + "node_modules/@smithy/eventstream-serde-config-resolver": { 1420 + "version": "4.3.14", 1421 + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.14.tgz", 1422 + "integrity": "sha512-sqHiHpYRYo3FJlaIxD1J8PhbcmJAm7IuM16mVnwSkCToD7g00IBZzKuiLNMGmftULmEUX6/UAz8/NN5uMP8bVA==", 1423 + "dev": true, 1424 + "license": "Apache-2.0", 1425 + "dependencies": { 1426 + "@smithy/types": "^4.14.1", 1427 + "tslib": "^2.6.2" 1428 + }, 1429 + "engines": { 1430 + "node": ">=18.0.0" 1431 + } 1432 + }, 1433 + "node_modules/@smithy/eventstream-serde-node": { 1434 + "version": "4.2.14", 1435 + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.14.tgz", 1436 + "integrity": "sha512-Ht/8BuGlKfFTy0H3+8eEu0vdpwGztCnaLLXtpXNdQqiR7Hj4vFScU3T436vRAjATglOIPjJXronY+1WxxNLSiw==", 1437 + "dev": true, 1438 + "license": "Apache-2.0", 1439 + "dependencies": { 1440 + "@smithy/eventstream-serde-universal": "^4.2.14", 1441 + "@smithy/types": "^4.14.1", 1442 + "tslib": "^2.6.2" 1443 + }, 1444 + "engines": { 1445 + "node": ">=18.0.0" 1446 + } 1447 + }, 1448 + "node_modules/@smithy/eventstream-serde-universal": { 1449 + "version": "4.2.14", 1450 + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.14.tgz", 1451 + "integrity": "sha512-lWyt4T2XQZUZgK3tQ3Wn0w3XBvZsK/vjTuJl6bXbnGZBHH0ZUSONTYiK9TgjTTzU54xQr3DRFwpjmhp0oLm3gg==", 1452 + "dev": true, 1453 + "license": "Apache-2.0", 1454 + "dependencies": { 1455 + "@smithy/eventstream-codec": "^4.2.14", 1456 + "@smithy/types": "^4.14.1", 1457 + "tslib": "^2.6.2" 1458 + }, 1459 + "engines": { 1460 + "node": ">=18.0.0" 1461 + } 1462 + }, 1463 + "node_modules/@smithy/fetch-http-handler": { 1464 + "version": "5.3.17", 1465 + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.17.tgz", 1466 + "integrity": "sha512-bXOvQzaSm6MnmLaWA1elgfQcAtN4UP3vXqV97bHuoOrHQOJiLT3ds6o9eo5bqd0TJfRFpzdGnDQdW3FACiAVdw==", 1467 + "dev": true, 1468 + "license": "Apache-2.0", 1469 + "dependencies": { 1470 + "@smithy/protocol-http": "^5.3.14", 1471 + "@smithy/querystring-builder": "^4.2.14", 1472 + "@smithy/types": "^4.14.1", 1473 + "@smithy/util-base64": "^4.3.2", 1474 + "tslib": "^2.6.2" 1475 + }, 1476 + "engines": { 1477 + "node": ">=18.0.0" 1478 + } 1479 + }, 1480 + "node_modules/@smithy/hash-node": { 1481 + "version": "4.2.14", 1482 + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.14.tgz", 1483 + "integrity": "sha512-8ZBDY2DD4wr+GGjTpPtiglEsqr0lUP+KHqgZcWczFf6qeZ/YRjMIOoQWVQlmwu7EtxKTd8YXD8lblmYcpBIA1g==", 1484 + "dev": true, 1485 + "license": "Apache-2.0", 1486 + "dependencies": { 1487 + "@smithy/types": "^4.14.1", 1488 + "@smithy/util-buffer-from": "^4.2.2", 1489 + "@smithy/util-utf8": "^4.2.2", 1490 + "tslib": "^2.6.2" 1491 + }, 1492 + "engines": { 1493 + "node": ">=18.0.0" 1494 + } 1495 + }, 1496 + "node_modules/@smithy/invalid-dependency": { 1497 + "version": "4.2.14", 1498 + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.14.tgz", 1499 + "integrity": "sha512-c21qJiTSb25xvvOp+H2TNZzPCngrvl5vIPqPB8zQ/DmJF4QWXO19x1dWfMJZ6wZuuWUPPm0gV8C0cU3+ifcWuw==", 1500 + "dev": true, 1501 + "license": "Apache-2.0", 1502 + "dependencies": { 1503 + "@smithy/types": "^4.14.1", 1504 + "tslib": "^2.6.2" 1505 + }, 1506 + "engines": { 1507 + "node": ">=18.0.0" 1508 + } 1509 + }, 1510 + "node_modules/@smithy/is-array-buffer": { 1511 + "version": "4.2.2", 1512 + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.2.tgz", 1513 + "integrity": "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==", 1514 + "dev": true, 1515 + "license": "Apache-2.0", 1516 + "dependencies": { 1517 + "tslib": "^2.6.2" 1518 + }, 1519 + "engines": { 1520 + "node": ">=18.0.0" 1521 + } 1522 + }, 1523 + "node_modules/@smithy/middleware-content-length": { 1524 + "version": "4.2.14", 1525 + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.14.tgz", 1526 + "integrity": "sha512-xhHq7fX4/3lv5NHxLUk3OeEvl0xZ+Ek3qIbWaCL4f9JwgDZEclPBElljaZCAItdGPQl/kSM4LPMOpy1MYgprpw==", 1527 + "dev": true, 1528 + "license": "Apache-2.0", 1529 + "dependencies": { 1530 + "@smithy/protocol-http": "^5.3.14", 1531 + "@smithy/types": "^4.14.1", 1532 + "tslib": "^2.6.2" 1533 + }, 1534 + "engines": { 1535 + "node": ">=18.0.0" 1536 + } 1537 + }, 1538 + "node_modules/@smithy/middleware-endpoint": { 1539 + "version": "4.4.32", 1540 + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.32.tgz", 1541 + "integrity": "sha512-ZZkgyjnJppiZbIm6Qbx92pbXYi1uzenIvGhBSCDlc7NwuAkiqSgS75j1czAD25ZLs2FjMjYy1q7gyRVWG6JA0Q==", 1542 + "dev": true, 1543 + "license": "Apache-2.0", 1544 + "dependencies": { 1545 + "@smithy/core": "^3.23.17", 1546 + "@smithy/middleware-serde": "^4.2.20", 1547 + "@smithy/node-config-provider": "^4.3.14", 1548 + "@smithy/shared-ini-file-loader": "^4.4.9", 1549 + "@smithy/types": "^4.14.1", 1550 + "@smithy/url-parser": "^4.2.14", 1551 + "@smithy/util-middleware": "^4.2.14", 1552 + "tslib": "^2.6.2" 1553 + }, 1554 + "engines": { 1555 + "node": ">=18.0.0" 1556 + } 1557 + }, 1558 + "node_modules/@smithy/middleware-retry": { 1559 + "version": "4.5.7", 1560 + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.5.7.tgz", 1561 + "integrity": "sha512-bRt6ZImqVSeTk39Nm81K20ObIiAZ3WefY7G6+iz/0tZjs4dgRRjvRX2sgsH+zi6iDCRR/aQvQofLKxxz4rPBZg==", 1562 + "dev": true, 1563 + "license": "Apache-2.0", 1564 + "dependencies": { 1565 + "@smithy/core": "^3.23.17", 1566 + "@smithy/node-config-provider": "^4.3.14", 1567 + "@smithy/protocol-http": "^5.3.14", 1568 + "@smithy/service-error-classification": "^4.3.1", 1569 + "@smithy/smithy-client": "^4.12.13", 1570 + "@smithy/types": "^4.14.1", 1571 + "@smithy/util-middleware": "^4.2.14", 1572 + "@smithy/util-retry": "^4.3.6", 1573 + "@smithy/uuid": "^1.1.2", 1574 + "tslib": "^2.6.2" 1575 + }, 1576 + "engines": { 1577 + "node": ">=18.0.0" 1578 + } 1579 + }, 1580 + "node_modules/@smithy/middleware-serde": { 1581 + "version": "4.2.20", 1582 + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.20.tgz", 1583 + "integrity": "sha512-Lx9JMO9vArPtiChE3wbEZ5akMIDQpWQtlu90lhACQmNOXcGXRbaDywMHDzuDZ2OkZzP+9wQfZi3YJT9F67zTQQ==", 1584 + "dev": true, 1585 + "license": "Apache-2.0", 1586 + "dependencies": { 1587 + "@smithy/core": "^3.23.17", 1588 + "@smithy/protocol-http": "^5.3.14", 1589 + "@smithy/types": "^4.14.1", 1590 + "tslib": "^2.6.2" 1591 + }, 1592 + "engines": { 1593 + "node": ">=18.0.0" 1594 + } 1595 + }, 1596 + "node_modules/@smithy/middleware-stack": { 1597 + "version": "4.2.14", 1598 + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.14.tgz", 1599 + "integrity": "sha512-2dvkUKLuFdKsCRmOE4Mn63co0Djtsm+JMh0bYZQupN1pJwMeE8FmQmRLLzzEMN0dnNi7CDCYYH8F0EVwWiPBeA==", 1600 + "dev": true, 1601 + "license": "Apache-2.0", 1602 + "dependencies": { 1603 + "@smithy/types": "^4.14.1", 1604 + "tslib": "^2.6.2" 1605 + }, 1606 + "engines": { 1607 + "node": ">=18.0.0" 1608 + } 1609 + }, 1610 + "node_modules/@smithy/node-config-provider": { 1611 + "version": "4.3.14", 1612 + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.14.tgz", 1613 + "integrity": "sha512-S+gFjyo/weSVL0P1b9Ts8C/CwIfNCgUPikk3sl6QVsfE/uUuO+QsF+NsE/JkpvWqqyz1wg7HFdiaZuj5CoBMRg==", 1614 + "dev": true, 1615 + "license": "Apache-2.0", 1616 + "dependencies": { 1617 + "@smithy/property-provider": "^4.2.14", 1618 + "@smithy/shared-ini-file-loader": "^4.4.9", 1619 + "@smithy/types": "^4.14.1", 1620 + "tslib": "^2.6.2" 1621 + }, 1622 + "engines": { 1623 + "node": ">=18.0.0" 1624 + } 1625 + }, 1626 + "node_modules/@smithy/node-http-handler": { 1627 + "version": "4.6.1", 1628 + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.6.1.tgz", 1629 + "integrity": "sha512-iB+orM4x3xrr57X3YaXazfKnntl0LHlZB1kcXSGzMV1Tt0+YwEjGlbjk/44qEGtBzXAz6yFDzkYTKSV6Pj2HUg==", 1630 + "dev": true, 1631 + "license": "Apache-2.0", 1632 + "dependencies": { 1633 + "@smithy/protocol-http": "^5.3.14", 1634 + "@smithy/querystring-builder": "^4.2.14", 1635 + "@smithy/types": "^4.14.1", 1636 + "tslib": "^2.6.2" 1637 + }, 1638 + "engines": { 1639 + "node": ">=18.0.0" 1640 + } 1641 + }, 1642 + "node_modules/@smithy/property-provider": { 1643 + "version": "4.2.14", 1644 + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.14.tgz", 1645 + "integrity": "sha512-WuM31CgfsnQ/10i7NYr0PyxqknD72Y5uMfUMVSniPjbEPceiTErb4eIqJQ+pdxNEAUEWrewrGjIRjVbVHsxZiQ==", 1646 + "dev": true, 1647 + "license": "Apache-2.0", 1648 + "dependencies": { 1649 + "@smithy/types": "^4.14.1", 1650 + "tslib": "^2.6.2" 1651 + }, 1652 + "engines": { 1653 + "node": ">=18.0.0" 1654 + } 1655 + }, 1656 + "node_modules/@smithy/protocol-http": { 1657 + "version": "5.3.14", 1658 + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.14.tgz", 1659 + "integrity": "sha512-dN5F8kHx8RNU0r+pCwNmFZyz6ChjMkzShy/zup6MtkRmmix4vZzJdW+di7x//b1LiynIev88FM18ie+wwPcQtQ==", 1660 + "dev": true, 1661 + "license": "Apache-2.0", 1662 + "dependencies": { 1663 + "@smithy/types": "^4.14.1", 1664 + "tslib": "^2.6.2" 1665 + }, 1666 + "engines": { 1667 + "node": ">=18.0.0" 1668 + } 1669 + }, 1670 + "node_modules/@smithy/querystring-builder": { 1671 + "version": "4.2.14", 1672 + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.14.tgz", 1673 + "integrity": "sha512-XYA5Z0IqTeF+5XDdh4BBmSA0HvbgVZIyv4cmOoUheDNR57K1HgBp9ukUMx3Cr3XpDHHpLBnexPE3LAtDsZkj2A==", 1674 + "dev": true, 1675 + "license": "Apache-2.0", 1676 + "dependencies": { 1677 + "@smithy/types": "^4.14.1", 1678 + "@smithy/util-uri-escape": "^4.2.2", 1679 + "tslib": "^2.6.2" 1680 + }, 1681 + "engines": { 1682 + "node": ">=18.0.0" 1683 + } 1684 + }, 1685 + "node_modules/@smithy/querystring-parser": { 1686 + "version": "4.2.14", 1687 + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.14.tgz", 1688 + "integrity": "sha512-hr+YyqBD23GVvRxGGrcc/oOeNlK3PzT5Fu4dzrDXxzS1LpFiuL2PQQqKPs87M79aW7ziMs+nvB3qdw77SqE7Lw==", 1689 + "dev": true, 1690 + "license": "Apache-2.0", 1691 + "dependencies": { 1692 + "@smithy/types": "^4.14.1", 1693 + "tslib": "^2.6.2" 1694 + }, 1695 + "engines": { 1696 + "node": ">=18.0.0" 1697 + } 1698 + }, 1699 + "node_modules/@smithy/service-error-classification": { 1700 + "version": "4.3.1", 1701 + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.3.1.tgz", 1702 + "integrity": "sha512-aUQuDGh760ts/8MU+APjIZhlLPKhIIfqyzZaJikLEIMrdxFvxuLYD0WxWzaYWpmLbQlXDe9p7EWM3HsBe0K6Gw==", 1703 + "dev": true, 1704 + "license": "Apache-2.0", 1705 + "dependencies": { 1706 + "@smithy/types": "^4.14.1" 1707 + }, 1708 + "engines": { 1709 + "node": ">=18.0.0" 1710 + } 1711 + }, 1712 + "node_modules/@smithy/shared-ini-file-loader": { 1713 + "version": "4.4.9", 1714 + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.9.tgz", 1715 + "integrity": "sha512-495/V2I15SHgedSJoDPD23JuSfKAp726ZI1V0wtjB07Wh7q/0tri/0e0DLefZCHgxZonrGKt/OCTpAtP1wE1kQ==", 1716 + "dev": true, 1717 + "license": "Apache-2.0", 1718 + "dependencies": { 1719 + "@smithy/types": "^4.14.1", 1720 + "tslib": "^2.6.2" 1721 + }, 1722 + "engines": { 1723 + "node": ">=18.0.0" 1724 + } 1725 + }, 1726 + "node_modules/@smithy/signature-v4": { 1727 + "version": "5.3.14", 1728 + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.14.tgz", 1729 + "integrity": "sha512-1D9Y/nmlVjCeSivCbhZ7hgEpmHyY1h0GvpSZt3l0xcD9JjmjVC1CHOozS6+Gh+/ldMH8JuJ6cujObQqfayAVFA==", 1730 + "dev": true, 1731 + "license": "Apache-2.0", 1732 + "dependencies": { 1733 + "@smithy/is-array-buffer": "^4.2.2", 1734 + "@smithy/protocol-http": "^5.3.14", 1735 + "@smithy/types": "^4.14.1", 1736 + "@smithy/util-hex-encoding": "^4.2.2", 1737 + "@smithy/util-middleware": "^4.2.14", 1738 + "@smithy/util-uri-escape": "^4.2.2", 1739 + "@smithy/util-utf8": "^4.2.2", 1740 + "tslib": "^2.6.2" 1741 + }, 1742 + "engines": { 1743 + "node": ">=18.0.0" 1744 + } 1745 + }, 1746 + "node_modules/@smithy/smithy-client": { 1747 + "version": "4.12.13", 1748 + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.13.tgz", 1749 + "integrity": "sha512-y/Pcj1V9+qG98gyu1gvftHB7rDpdh+7kIBIggs55yGm3JdtBV8GT8IFF3a1qxZ79QnaJHX9GXzvBG6tAd+czJA==", 1750 + "dev": true, 1751 + "license": "Apache-2.0", 1752 + "dependencies": { 1753 + "@smithy/core": "^3.23.17", 1754 + "@smithy/middleware-endpoint": "^4.4.32", 1755 + "@smithy/middleware-stack": "^4.2.14", 1756 + "@smithy/protocol-http": "^5.3.14", 1757 + "@smithy/types": "^4.14.1", 1758 + "@smithy/util-stream": "^4.5.25", 1759 + "tslib": "^2.6.2" 1760 + }, 1761 + "engines": { 1762 + "node": ">=18.0.0" 1763 + } 1764 + }, 1765 + "node_modules/@smithy/types": { 1766 + "version": "4.14.1", 1767 + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.1.tgz", 1768 + "integrity": "sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg==", 1769 + "dev": true, 1770 + "license": "Apache-2.0", 1771 + "dependencies": { 1772 + "tslib": "^2.6.2" 1773 + }, 1774 + "engines": { 1775 + "node": ">=18.0.0" 1776 + } 1777 + }, 1778 + "node_modules/@smithy/url-parser": { 1779 + "version": "4.2.14", 1780 + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.14.tgz", 1781 + "integrity": "sha512-p06BiBigJ8bTA3MgnOfCtDUWnAMY0YfedO/GRpmc7p+wg3KW8vbXy1xwSu5ASy0wV7rRYtlfZOIKH4XqfhjSQQ==", 1782 + "dev": true, 1783 + "license": "Apache-2.0", 1784 + "dependencies": { 1785 + "@smithy/querystring-parser": "^4.2.14", 1786 + "@smithy/types": "^4.14.1", 1787 + "tslib": "^2.6.2" 1788 + }, 1789 + "engines": { 1790 + "node": ">=18.0.0" 1791 + } 1792 + }, 1793 + "node_modules/@smithy/util-base64": { 1794 + "version": "4.3.2", 1795 + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.2.tgz", 1796 + "integrity": "sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ==", 1797 + "dev": true, 1798 + "license": "Apache-2.0", 1799 + "dependencies": { 1800 + "@smithy/util-buffer-from": "^4.2.2", 1801 + "@smithy/util-utf8": "^4.2.2", 1802 + "tslib": "^2.6.2" 1803 + }, 1804 + "engines": { 1805 + "node": ">=18.0.0" 1806 + } 1807 + }, 1808 + "node_modules/@smithy/util-body-length-browser": { 1809 + "version": "4.2.2", 1810 + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.2.tgz", 1811 + "integrity": "sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ==", 1812 + "dev": true, 1813 + "license": "Apache-2.0", 1814 + "dependencies": { 1815 + "tslib": "^2.6.2" 1816 + }, 1817 + "engines": { 1818 + "node": ">=18.0.0" 1819 + } 1820 + }, 1821 + "node_modules/@smithy/util-body-length-node": { 1822 + "version": "4.2.3", 1823 + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.3.tgz", 1824 + "integrity": "sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g==", 1825 + "dev": true, 1826 + "license": "Apache-2.0", 1827 + "dependencies": { 1828 + "tslib": "^2.6.2" 1829 + }, 1830 + "engines": { 1831 + "node": ">=18.0.0" 1832 + } 1833 + }, 1834 + "node_modules/@smithy/util-buffer-from": { 1835 + "version": "4.2.2", 1836 + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.2.tgz", 1837 + "integrity": "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==", 1838 + "dev": true, 1839 + "license": "Apache-2.0", 1840 + "dependencies": { 1841 + "@smithy/is-array-buffer": "^4.2.2", 1842 + "tslib": "^2.6.2" 1843 + }, 1844 + "engines": { 1845 + "node": ">=18.0.0" 1846 + } 1847 + }, 1848 + "node_modules/@smithy/util-config-provider": { 1849 + "version": "4.2.2", 1850 + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.2.tgz", 1851 + "integrity": "sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ==", 1852 + "dev": true, 1853 + "license": "Apache-2.0", 1854 + "dependencies": { 1855 + "tslib": "^2.6.2" 1856 + }, 1857 + "engines": { 1858 + "node": ">=18.0.0" 1859 + } 1860 + }, 1861 + "node_modules/@smithy/util-defaults-mode-browser": { 1862 + "version": "4.3.49", 1863 + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.49.tgz", 1864 + "integrity": "sha512-a5bNrdiONYB/qE2BuKegvUMd/+ZDwdg4vsNuuSzYE8qs2EYAdK9CynL+Rzn29PbPiUqoz/cbpRbcLzD5lEevHw==", 1865 + "dev": true, 1866 + "license": "Apache-2.0", 1867 + "dependencies": { 1868 + "@smithy/property-provider": "^4.2.14", 1869 + "@smithy/smithy-client": "^4.12.13", 1870 + "@smithy/types": "^4.14.1", 1871 + "tslib": "^2.6.2" 1872 + }, 1873 + "engines": { 1874 + "node": ">=18.0.0" 1875 + } 1876 + }, 1877 + "node_modules/@smithy/util-defaults-mode-node": { 1878 + "version": "4.2.54", 1879 + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.54.tgz", 1880 + "integrity": "sha512-g1cvrJvOnzeJgEdf7AE4luI7gp6L8weE0y9a9wQUSGtjb8QRHDbCJYuE4Sy0SD9N8RrnNPFsPltAz/OSoBR9Zw==", 1881 + "dev": true, 1882 + "license": "Apache-2.0", 1883 + "dependencies": { 1884 + "@smithy/config-resolver": "^4.4.17", 1885 + "@smithy/credential-provider-imds": "^4.2.14", 1886 + "@smithy/node-config-provider": "^4.3.14", 1887 + "@smithy/property-provider": "^4.2.14", 1888 + "@smithy/smithy-client": "^4.12.13", 1889 + "@smithy/types": "^4.14.1", 1890 + "tslib": "^2.6.2" 1891 + }, 1892 + "engines": { 1893 + "node": ">=18.0.0" 1894 + } 1895 + }, 1896 + "node_modules/@smithy/util-endpoints": { 1897 + "version": "3.4.2", 1898 + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.4.2.tgz", 1899 + "integrity": "sha512-a55Tr+3OKld4TTtnT+RhKOQHyPxm3j/xL4OR83WBUhLJaKDS9dnJ7arRMOp3t31dcLhApwG9bgvrRXBHlLdIkg==", 1900 + "dev": true, 1901 + "license": "Apache-2.0", 1902 + "dependencies": { 1903 + "@smithy/node-config-provider": "^4.3.14", 1904 + "@smithy/types": "^4.14.1", 1905 + "tslib": "^2.6.2" 1906 + }, 1907 + "engines": { 1908 + "node": ">=18.0.0" 1909 + } 1910 + }, 1911 + "node_modules/@smithy/util-hex-encoding": { 1912 + "version": "4.2.2", 1913 + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.2.tgz", 1914 + "integrity": "sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg==", 1915 + "dev": true, 1916 + "license": "Apache-2.0", 1917 + "dependencies": { 1918 + "tslib": "^2.6.2" 1919 + }, 1920 + "engines": { 1921 + "node": ">=18.0.0" 1922 + } 1923 + }, 1924 + "node_modules/@smithy/util-middleware": { 1925 + "version": "4.2.14", 1926 + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.14.tgz", 1927 + "integrity": "sha512-1Su2vj9RYNDEv/V+2E+jXkkwGsgR7dc4sfHn9Z7ruzQHJIEni9zzw5CauvRXlFJfmgcqYP8fWa0dkh2Q2YaQyw==", 1928 + "dev": true, 1929 + "license": "Apache-2.0", 1930 + "dependencies": { 1931 + "@smithy/types": "^4.14.1", 1932 + "tslib": "^2.6.2" 1933 + }, 1934 + "engines": { 1935 + "node": ">=18.0.0" 1936 + } 1937 + }, 1938 + "node_modules/@smithy/util-retry": { 1939 + "version": "4.3.8", 1940 + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.3.8.tgz", 1941 + "integrity": "sha512-LUIxbTBi+OpvXpg91poGA6BdyoleMDLnfXjVDqyi2RvZmTveY5loE/FgYUBCR5LU2BThW2SoZRh8dTIIy38IPw==", 1942 + "dev": true, 1943 + "license": "Apache-2.0", 1944 + "dependencies": { 1945 + "@smithy/service-error-classification": "^4.3.1", 1946 + "@smithy/types": "^4.14.1", 1947 + "tslib": "^2.6.2" 1948 + }, 1949 + "engines": { 1950 + "node": ">=18.0.0" 1951 + } 1952 + }, 1953 + "node_modules/@smithy/util-stream": { 1954 + "version": "4.5.25", 1955 + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.25.tgz", 1956 + "integrity": "sha512-/PFpG4k8Ze8Ei+mMKj3oiPICYekthuzePZMgZbCqMiXIHHf4n2aZ4Ps0aSRShycFTGuj/J6XldmC0x0DwednIA==", 1957 + "dev": true, 1958 + "license": "Apache-2.0", 1959 + "dependencies": { 1960 + "@smithy/fetch-http-handler": "^5.3.17", 1961 + "@smithy/node-http-handler": "^4.6.1", 1962 + "@smithy/types": "^4.14.1", 1963 + "@smithy/util-base64": "^4.3.2", 1964 + "@smithy/util-buffer-from": "^4.2.2", 1965 + "@smithy/util-hex-encoding": "^4.2.2", 1966 + "@smithy/util-utf8": "^4.2.2", 1967 + "tslib": "^2.6.2" 1968 + }, 1969 + "engines": { 1970 + "node": ">=18.0.0" 1971 + } 1972 + }, 1973 + "node_modules/@smithy/util-uri-escape": { 1974 + "version": "4.2.2", 1975 + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.2.tgz", 1976 + "integrity": "sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw==", 1977 + "dev": true, 1978 + "license": "Apache-2.0", 1979 + "dependencies": { 1980 + "tslib": "^2.6.2" 1981 + }, 1982 + "engines": { 1983 + "node": ">=18.0.0" 1984 + } 1985 + }, 1986 + "node_modules/@smithy/util-utf8": { 1987 + "version": "4.2.2", 1988 + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.2.tgz", 1989 + "integrity": "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==", 1990 + "dev": true, 1991 + "license": "Apache-2.0", 1992 + "dependencies": { 1993 + "@smithy/util-buffer-from": "^4.2.2", 1994 + "tslib": "^2.6.2" 1995 + }, 1996 + "engines": { 1997 + "node": ">=18.0.0" 1998 + } 1999 + }, 2000 + "node_modules/@smithy/uuid": { 2001 + "version": "1.1.2", 2002 + "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.2.tgz", 2003 + "integrity": "sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g==", 2004 + "dev": true, 2005 + "license": "Apache-2.0", 2006 + "dependencies": { 2007 + "tslib": "^2.6.2" 2008 + }, 2009 + "engines": { 2010 + "node": ">=18.0.0" 2011 + } 2012 + }, 2013 + "node_modules/@tokenizer/inflate": { 2014 + "version": "0.4.1", 2015 + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", 2016 + "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", 2017 + "dev": true, 2018 + "license": "MIT", 2019 + "dependencies": { 2020 + "debug": "^4.4.3", 2021 + "token-types": "^6.1.1" 2022 + }, 2023 + "engines": { 2024 + "node": ">=18" 2025 + }, 2026 + "funding": { 2027 + "type": "github", 2028 + "url": "https://github.com/sponsors/Borewit" 2029 + } 2030 + }, 2031 + "node_modules/@tokenizer/token": { 2032 + "version": "0.3.0", 2033 + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", 2034 + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", 2035 + "dev": true, 2036 + "license": "MIT" 2037 + }, 2038 + "node_modules/@tootallnate/quickjs-emscripten": { 2039 + "version": "0.23.0", 2040 + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", 2041 + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", 2042 + "dev": true, 2043 + "license": "MIT" 2044 + }, 2045 + "node_modules/@types/mime-types": { 2046 + "version": "2.1.4", 2047 + "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.4.tgz", 2048 + "integrity": "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==", 2049 + "dev": true, 2050 + "license": "MIT" 2051 + }, 2052 + "node_modules/@types/node": { 2053 + "version": "22.19.18", 2054 + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.18.tgz", 2055 + "integrity": "sha512-9v00a+dn2yWVsYDEunWC4g/TcRKVq3r8N5FuZp7u0SGrPvdN9c2yXI9bBuf5Fl0hNCb+QTIePTn5pJs2pwBOQQ==", 2056 + "dev": true, 2057 + "license": "MIT", 2058 + "dependencies": { 2059 + "undici-types": "~6.21.0" 2060 + } 2061 + }, 2062 + "node_modules/@types/picomatch": { 2063 + "version": "4.0.3", 2064 + "resolved": "https://registry.npmjs.org/@types/picomatch/-/picomatch-4.0.3.tgz", 2065 + "integrity": "sha512-iG0T6+nYJ9FAPmx9SsUlnwcq1ZVRuCXcVEvWnntoPlrOpwtSTKNDC9uVAxTsC3PUvJ+99n4RpAcNgBbHX3JSnQ==", 2066 + "dev": true, 2067 + "license": "MIT" 2068 + }, 2069 + "node_modules/@types/retry": { 2070 + "version": "0.12.0", 2071 + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", 2072 + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", 2073 + "dev": true, 2074 + "license": "MIT" 2075 + }, 2076 + "node_modules/@types/yauzl": { 2077 + "version": "2.10.3", 2078 + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", 2079 + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", 2080 + "dev": true, 2081 + "license": "MIT", 2082 + "optional": true, 2083 + "dependencies": { 2084 + "@types/node": "*" 2085 + } 2086 + }, 2087 + "node_modules/agent-base": { 2088 + "version": "7.1.4", 2089 + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", 2090 + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", 2091 + "dev": true, 2092 + "license": "MIT", 2093 + "engines": { 2094 + "node": ">= 14" 2095 + } 2096 + }, 2097 + "node_modules/ajv": { 2098 + "version": "8.20.0", 2099 + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", 2100 + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", 2101 + "license": "MIT", 2102 + "dependencies": { 2103 + "fast-deep-equal": "^3.1.3", 2104 + "fast-uri": "^3.0.1", 2105 + "json-schema-traverse": "^1.0.0", 2106 + "require-from-string": "^2.0.2" 2107 + }, 2108 + "funding": { 2109 + "type": "github", 2110 + "url": "https://github.com/sponsors/epoberezkin" 2111 + } 2112 + }, 2113 + "node_modules/ajv-formats": { 2114 + "version": "3.0.1", 2115 + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", 2116 + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", 2117 + "license": "MIT", 2118 + "dependencies": { 2119 + "ajv": "^8.0.0" 2120 + }, 2121 + "peerDependencies": { 2122 + "ajv": "^8.0.0" 2123 + }, 2124 + "peerDependenciesMeta": { 2125 + "ajv": { 2126 + "optional": true 2127 + } 2128 + } 2129 + }, 2130 + "node_modules/ansi-regex": { 2131 + "version": "6.2.2", 2132 + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", 2133 + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", 2134 + "dev": true, 2135 + "license": "MIT", 2136 + "engines": { 2137 + "node": ">=12" 2138 + }, 2139 + "funding": { 2140 + "url": "https://github.com/chalk/ansi-regex?sponsor=1" 2141 + } 2142 + }, 2143 + "node_modules/ansi-styles": { 2144 + "version": "4.3.0", 2145 + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 2146 + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 2147 + "dev": true, 2148 + "license": "MIT", 2149 + "dependencies": { 2150 + "color-convert": "^2.0.1" 2151 + }, 2152 + "engines": { 2153 + "node": ">=8" 2154 + }, 2155 + "funding": { 2156 + "url": "https://github.com/chalk/ansi-styles?sponsor=1" 2157 + } 2158 + }, 2159 + "node_modules/any-promise": { 2160 + "version": "1.3.0", 2161 + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", 2162 + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", 2163 + "dev": true, 2164 + "license": "MIT" 2165 + }, 2166 + "node_modules/ast-types": { 2167 + "version": "0.13.4", 2168 + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", 2169 + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", 2170 + "dev": true, 2171 + "license": "MIT", 2172 + "dependencies": { 2173 + "tslib": "^2.0.1" 2174 + }, 2175 + "engines": { 2176 + "node": ">=4" 2177 + } 2178 + }, 2179 + "node_modules/balanced-match": { 2180 + "version": "4.0.4", 2181 + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", 2182 + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", 2183 + "dev": true, 2184 + "license": "MIT", 2185 + "engines": { 2186 + "node": "18 || 20 || >=22" 2187 + } 2188 + }, 2189 + "node_modules/base64-js": { 2190 + "version": "1.5.1", 2191 + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", 2192 + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", 2193 + "dev": true, 2194 + "funding": [ 2195 + { 2196 + "type": "github", 2197 + "url": "https://github.com/sponsors/feross" 2198 + }, 2199 + { 2200 + "type": "patreon", 2201 + "url": "https://www.patreon.com/feross" 2202 + }, 2203 + { 2204 + "type": "consulting", 2205 + "url": "https://feross.org/support" 2206 + } 2207 + ], 2208 + "license": "MIT" 2209 + }, 2210 + "node_modules/basic-ftp": { 2211 + "version": "5.3.1", 2212 + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", 2213 + "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", 2214 + "dev": true, 2215 + "license": "MIT", 2216 + "engines": { 2217 + "node": ">=10.0.0" 2218 + } 2219 + }, 2220 + "node_modules/bignumber.js": { 2221 + "version": "9.3.1", 2222 + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", 2223 + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", 2224 + "dev": true, 2225 + "license": "MIT", 2226 + "engines": { 2227 + "node": "*" 2228 + } 2229 + }, 2230 + "node_modules/bowser": { 2231 + "version": "2.14.1", 2232 + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", 2233 + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", 2234 + "dev": true, 2235 + "license": "MIT" 2236 + }, 2237 + "node_modules/brace-expansion": { 2238 + "version": "5.0.6", 2239 + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", 2240 + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", 2241 + "dev": true, 2242 + "license": "MIT", 2243 + "dependencies": { 2244 + "balanced-match": "^4.0.2" 2245 + }, 2246 + "engines": { 2247 + "node": "18 || 20 || >=22" 2248 + } 2249 + }, 2250 + "node_modules/buffer-crc32": { 2251 + "version": "0.2.13", 2252 + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", 2253 + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", 2254 + "dev": true, 2255 + "license": "MIT", 2256 + "engines": { 2257 + "node": "*" 2258 + } 2259 + }, 2260 + "node_modules/buffer-equal-constant-time": { 2261 + "version": "1.0.1", 2262 + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", 2263 + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", 2264 + "dev": true, 2265 + "license": "BSD-3-Clause" 2266 + }, 2267 + "node_modules/chalk": { 2268 + "version": "5.6.2", 2269 + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", 2270 + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", 2271 + "dev": true, 2272 + "license": "MIT", 2273 + "engines": { 2274 + "node": "^12.17.0 || ^14.13 || >=16.0.0" 2275 + }, 2276 + "funding": { 2277 + "url": "https://github.com/chalk/chalk?sponsor=1" 2278 + } 2279 + }, 2280 + "node_modules/cli-highlight": { 2281 + "version": "2.1.11", 2282 + "resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.11.tgz", 2283 + "integrity": "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==", 2284 + "dev": true, 2285 + "license": "ISC", 2286 + "dependencies": { 2287 + "chalk": "^4.0.0", 2288 + "highlight.js": "^10.7.1", 2289 + "mz": "^2.4.0", 2290 + "parse5": "^5.1.1", 2291 + "parse5-htmlparser2-tree-adapter": "^6.0.0", 2292 + "yargs": "^16.0.0" 2293 + }, 2294 + "bin": { 2295 + "highlight": "bin/highlight" 2296 + }, 2297 + "engines": { 2298 + "node": ">=8.0.0", 2299 + "npm": ">=5.0.0" 2300 + } 2301 + }, 2302 + "node_modules/cli-highlight/node_modules/chalk": { 2303 + "version": "4.1.2", 2304 + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", 2305 + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 2306 + "dev": true, 2307 + "license": "MIT", 2308 + "dependencies": { 2309 + "ansi-styles": "^4.1.0", 2310 + "supports-color": "^7.1.0" 2311 + }, 2312 + "engines": { 2313 + "node": ">=10" 2314 + }, 2315 + "funding": { 2316 + "url": "https://github.com/chalk/chalk?sponsor=1" 2317 + } 2318 + }, 2319 + "node_modules/cliui": { 2320 + "version": "7.0.4", 2321 + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", 2322 + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", 2323 + "dev": true, 2324 + "license": "ISC", 2325 + "dependencies": { 2326 + "string-width": "^4.2.0", 2327 + "strip-ansi": "^6.0.0", 2328 + "wrap-ansi": "^7.0.0" 2329 + } 2330 + }, 2331 + "node_modules/cliui/node_modules/ansi-regex": { 2332 + "version": "5.0.1", 2333 + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 2334 + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 2335 + "dev": true, 2336 + "license": "MIT", 2337 + "engines": { 2338 + "node": ">=8" 2339 + } 2340 + }, 2341 + "node_modules/cliui/node_modules/strip-ansi": { 2342 + "version": "6.0.1", 2343 + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 2344 + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 2345 + "dev": true, 2346 + "license": "MIT", 2347 + "dependencies": { 2348 + "ansi-regex": "^5.0.1" 2349 + }, 2350 + "engines": { 2351 + "node": ">=8" 2352 + } 2353 + }, 2354 + "node_modules/color-convert": { 2355 + "version": "2.0.1", 2356 + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 2357 + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 2358 + "dev": true, 2359 + "license": "MIT", 2360 + "dependencies": { 2361 + "color-name": "~1.1.4" 2362 + }, 2363 + "engines": { 2364 + "node": ">=7.0.0" 2365 + } 2366 + }, 2367 + "node_modules/color-name": { 2368 + "version": "1.1.4", 2369 + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 2370 + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 2371 + "dev": true, 2372 + "license": "MIT" 2373 + }, 2374 + "node_modules/data-uri-to-buffer": { 2375 + "version": "4.0.1", 2376 + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", 2377 + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", 2378 + "dev": true, 2379 + "license": "MIT", 2380 + "engines": { 2381 + "node": ">= 12" 2382 + } 2383 + }, 2384 + "node_modules/debug": { 2385 + "version": "4.4.3", 2386 + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", 2387 + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", 2388 + "dev": true, 2389 + "license": "MIT", 2390 + "dependencies": { 2391 + "ms": "^2.1.3" 2392 + }, 2393 + "engines": { 2394 + "node": ">=6.0" 2395 + }, 2396 + "peerDependenciesMeta": { 2397 + "supports-color": { 2398 + "optional": true 2399 + } 2400 + } 2401 + }, 2402 + "node_modules/degenerator": { 2403 + "version": "5.0.1", 2404 + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", 2405 + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", 2406 + "dev": true, 2407 + "license": "MIT", 2408 + "dependencies": { 2409 + "ast-types": "^0.13.4", 2410 + "escodegen": "^2.1.0", 2411 + "esprima": "^4.0.1" 2412 + }, 2413 + "engines": { 2414 + "node": ">= 14" 2415 + } 2416 + }, 2417 + "node_modules/diff": { 2418 + "version": "8.0.4", 2419 + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", 2420 + "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", 2421 + "dev": true, 2422 + "license": "BSD-3-Clause", 2423 + "engines": { 2424 + "node": ">=0.3.1" 2425 + } 2426 + }, 2427 + "node_modules/ecdsa-sig-formatter": { 2428 + "version": "1.0.11", 2429 + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", 2430 + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", 2431 + "dev": true, 2432 + "license": "Apache-2.0", 2433 + "dependencies": { 2434 + "safe-buffer": "^5.0.1" 2435 + } 2436 + }, 2437 + "node_modules/emoji-regex": { 2438 + "version": "8.0.0", 2439 + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 2440 + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", 2441 + "dev": true, 2442 + "license": "MIT" 2443 + }, 2444 + "node_modules/end-of-stream": { 2445 + "version": "1.4.5", 2446 + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", 2447 + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", 2448 + "dev": true, 2449 + "license": "MIT", 2450 + "dependencies": { 2451 + "once": "^1.4.0" 2452 + } 2453 + }, 2454 + "node_modules/escalade": { 2455 + "version": "3.2.0", 2456 + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", 2457 + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", 2458 + "dev": true, 2459 + "license": "MIT", 2460 + "engines": { 2461 + "node": ">=6" 2462 + } 2463 + }, 2464 + "node_modules/escodegen": { 2465 + "version": "2.1.0", 2466 + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", 2467 + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", 2468 + "dev": true, 2469 + "license": "BSD-2-Clause", 2470 + "dependencies": { 2471 + "esprima": "^4.0.1", 2472 + "estraverse": "^5.2.0", 2473 + "esutils": "^2.0.2" 2474 + }, 2475 + "bin": { 2476 + "escodegen": "bin/escodegen.js", 2477 + "esgenerate": "bin/esgenerate.js" 2478 + }, 2479 + "engines": { 2480 + "node": ">=6.0" 2481 + }, 2482 + "optionalDependencies": { 2483 + "source-map": "~0.6.1" 2484 + } 2485 + }, 2486 + "node_modules/esprima": { 2487 + "version": "4.0.1", 2488 + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", 2489 + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", 2490 + "dev": true, 2491 + "license": "BSD-2-Clause", 2492 + "bin": { 2493 + "esparse": "bin/esparse.js", 2494 + "esvalidate": "bin/esvalidate.js" 2495 + }, 2496 + "engines": { 2497 + "node": ">=4" 2498 + } 2499 + }, 2500 + "node_modules/estraverse": { 2501 + "version": "5.3.0", 2502 + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", 2503 + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", 2504 + "dev": true, 2505 + "license": "BSD-2-Clause", 2506 + "engines": { 2507 + "node": ">=4.0" 2508 + } 2509 + }, 2510 + "node_modules/esutils": { 2511 + "version": "2.0.3", 2512 + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", 2513 + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", 2514 + "dev": true, 2515 + "license": "BSD-2-Clause", 2516 + "engines": { 2517 + "node": ">=0.10.0" 2518 + } 2519 + }, 2520 + "node_modules/extend": { 2521 + "version": "3.0.2", 2522 + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", 2523 + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", 2524 + "dev": true, 2525 + "license": "MIT" 2526 + }, 2527 + "node_modules/extract-zip": { 2528 + "version": "2.0.1", 2529 + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", 2530 + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", 2531 + "dev": true, 2532 + "license": "BSD-2-Clause", 2533 + "dependencies": { 2534 + "debug": "^4.1.1", 2535 + "get-stream": "^5.1.0", 2536 + "yauzl": "^2.10.0" 2537 + }, 2538 + "bin": { 2539 + "extract-zip": "cli.js" 2540 + }, 2541 + "engines": { 2542 + "node": ">= 10.17.0" 2543 + }, 2544 + "optionalDependencies": { 2545 + "@types/yauzl": "^2.9.1" 2546 + } 2547 + }, 2548 + "node_modules/fast-deep-equal": { 2549 + "version": "3.1.3", 2550 + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", 2551 + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", 2552 + "license": "MIT" 2553 + }, 2554 + "node_modules/fast-uri": { 2555 + "version": "3.1.2", 2556 + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", 2557 + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", 2558 + "funding": [ 2559 + { 2560 + "type": "github", 2561 + "url": "https://github.com/sponsors/fastify" 2562 + }, 2563 + { 2564 + "type": "opencollective", 2565 + "url": "https://opencollective.com/fastify" 2566 + } 2567 + ], 2568 + "license": "BSD-3-Clause" 2569 + }, 2570 + "node_modules/fast-xml-builder": { 2571 + "version": "1.2.0", 2572 + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", 2573 + "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", 2574 + "dev": true, 2575 + "funding": [ 2576 + { 2577 + "type": "github", 2578 + "url": "https://github.com/sponsors/NaturalIntelligence" 2579 + } 2580 + ], 2581 + "license": "MIT", 2582 + "dependencies": { 2583 + "path-expression-matcher": "^1.5.0", 2584 + "xml-naming": "^0.1.0" 2585 + } 2586 + }, 2587 + "node_modules/fast-xml-parser": { 2588 + "version": "5.7.2", 2589 + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.2.tgz", 2590 + "integrity": "sha512-P7oW7tLbYnhOLQk/Gv7cZgzgMPP/XN03K02/Jy6Y/NHzyIAIpxuZIM/YqAkfiXFPxA2CTm7NtCijK9EDu09u2w==", 2591 + "dev": true, 2592 + "funding": [ 2593 + { 2594 + "type": "github", 2595 + "url": "https://github.com/sponsors/NaturalIntelligence" 2596 + } 2597 + ], 2598 + "license": "MIT", 2599 + "dependencies": { 2600 + "@nodable/entities": "^2.1.0", 2601 + "fast-xml-builder": "^1.1.5", 2602 + "path-expression-matcher": "^1.5.0", 2603 + "strnum": "^2.2.3" 2604 + }, 2605 + "bin": { 2606 + "fxparser": "src/cli/cli.js" 2607 + } 2608 + }, 2609 + "node_modules/fd-slicer": { 2610 + "version": "1.1.0", 2611 + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", 2612 + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", 2613 + "dev": true, 2614 + "license": "MIT", 2615 + "dependencies": { 2616 + "pend": "~1.2.0" 2617 + } 2618 + }, 2619 + "node_modules/fetch-blob": { 2620 + "version": "3.2.0", 2621 + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", 2622 + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", 2623 + "dev": true, 2624 + "funding": [ 2625 + { 2626 + "type": "github", 2627 + "url": "https://github.com/sponsors/jimmywarting" 2628 + }, 2629 + { 2630 + "type": "paypal", 2631 + "url": "https://paypal.me/jimmywarting" 2632 + } 2633 + ], 2634 + "license": "MIT", 2635 + "dependencies": { 2636 + "node-domexception": "^1.0.0", 2637 + "web-streams-polyfill": "^3.0.3" 2638 + }, 2639 + "engines": { 2640 + "node": "^12.20 || >= 14.13" 2641 + } 2642 + }, 2643 + "node_modules/file-type": { 2644 + "version": "21.3.4", 2645 + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.4.tgz", 2646 + "integrity": "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==", 2647 + "dev": true, 2648 + "license": "MIT", 2649 + "dependencies": { 2650 + "@tokenizer/inflate": "^0.4.1", 2651 + "strtok3": "^10.3.4", 2652 + "token-types": "^6.1.1", 2653 + "uint8array-extras": "^1.4.0" 2654 + }, 2655 + "engines": { 2656 + "node": ">=20" 2657 + }, 2658 + "funding": { 2659 + "url": "https://github.com/sindresorhus/file-type?sponsor=1" 2660 + } 2661 + }, 2662 + "node_modules/formdata-polyfill": { 2663 + "version": "4.0.10", 2664 + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", 2665 + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", 2666 + "dev": true, 2667 + "license": "MIT", 2668 + "dependencies": { 2669 + "fetch-blob": "^3.1.2" 2670 + }, 2671 + "engines": { 2672 + "node": ">=12.20.0" 2673 + } 2674 + }, 2675 + "node_modules/gaxios": { 2676 + "version": "7.1.4", 2677 + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.4.tgz", 2678 + "integrity": "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==", 2679 + "dev": true, 2680 + "license": "Apache-2.0", 2681 + "dependencies": { 2682 + "extend": "^3.0.2", 2683 + "https-proxy-agent": "^7.0.1", 2684 + "node-fetch": "^3.3.2" 2685 + }, 2686 + "engines": { 2687 + "node": ">=18" 2688 + } 2689 + }, 2690 + "node_modules/gcp-metadata": { 2691 + "version": "8.1.2", 2692 + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", 2693 + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", 2694 + "dev": true, 2695 + "license": "Apache-2.0", 2696 + "dependencies": { 2697 + "gaxios": "^7.0.0", 2698 + "google-logging-utils": "^1.0.0", 2699 + "json-bigint": "^1.0.0" 2700 + }, 2701 + "engines": { 2702 + "node": ">=18" 2703 + } 2704 + }, 2705 + "node_modules/get-caller-file": { 2706 + "version": "2.0.5", 2707 + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", 2708 + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", 2709 + "dev": true, 2710 + "license": "ISC", 2711 + "engines": { 2712 + "node": "6.* || 8.* || >= 10.*" 2713 + } 2714 + }, 2715 + "node_modules/get-east-asian-width": { 2716 + "version": "1.6.0", 2717 + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", 2718 + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", 2719 + "dev": true, 2720 + "license": "MIT", 2721 + "engines": { 2722 + "node": ">=18" 2723 + }, 2724 + "funding": { 2725 + "url": "https://github.com/sponsors/sindresorhus" 2726 + } 2727 + }, 2728 + "node_modules/get-stream": { 2729 + "version": "5.2.0", 2730 + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", 2731 + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", 2732 + "dev": true, 2733 + "license": "MIT", 2734 + "dependencies": { 2735 + "pump": "^3.0.0" 2736 + }, 2737 + "engines": { 2738 + "node": ">=8" 2739 + }, 2740 + "funding": { 2741 + "url": "https://github.com/sponsors/sindresorhus" 2742 + } 2743 + }, 2744 + "node_modules/get-uri": { 2745 + "version": "6.0.5", 2746 + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", 2747 + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", 2748 + "dev": true, 2749 + "license": "MIT", 2750 + "dependencies": { 2751 + "basic-ftp": "^5.0.2", 2752 + "data-uri-to-buffer": "^6.0.2", 2753 + "debug": "^4.3.4" 2754 + }, 2755 + "engines": { 2756 + "node": ">= 14" 2757 + } 2758 + }, 2759 + "node_modules/get-uri/node_modules/data-uri-to-buffer": { 2760 + "version": "6.0.2", 2761 + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", 2762 + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", 2763 + "dev": true, 2764 + "license": "MIT", 2765 + "engines": { 2766 + "node": ">= 14" 2767 + } 2768 + }, 2769 + "node_modules/glob": { 2770 + "version": "13.0.6", 2771 + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", 2772 + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", 2773 + "dev": true, 2774 + "license": "BlueOak-1.0.0", 2775 + "dependencies": { 2776 + "minimatch": "^10.2.2", 2777 + "minipass": "^7.1.3", 2778 + "path-scurry": "^2.0.2" 2779 + }, 2780 + "engines": { 2781 + "node": "18 || 20 || >=22" 2782 + }, 2783 + "funding": { 2784 + "url": "https://github.com/sponsors/isaacs" 2785 + } 2786 + }, 2787 + "node_modules/google-auth-library": { 2788 + "version": "10.6.2", 2789 + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.2.tgz", 2790 + "integrity": "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==", 2791 + "dev": true, 2792 + "license": "Apache-2.0", 2793 + "dependencies": { 2794 + "base64-js": "^1.3.0", 2795 + "ecdsa-sig-formatter": "^1.0.11", 2796 + "gaxios": "^7.1.4", 2797 + "gcp-metadata": "8.1.2", 2798 + "google-logging-utils": "1.1.3", 2799 + "jws": "^4.0.0" 2800 + }, 2801 + "engines": { 2802 + "node": ">=18" 2803 + } 2804 + }, 2805 + "node_modules/google-logging-utils": { 2806 + "version": "1.1.3", 2807 + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", 2808 + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", 2809 + "dev": true, 2810 + "license": "Apache-2.0", 2811 + "engines": { 2812 + "node": ">=14" 2813 + } 2814 + }, 2815 + "node_modules/graceful-fs": { 2816 + "version": "4.2.11", 2817 + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", 2818 + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", 2819 + "dev": true, 2820 + "license": "ISC" 2821 + }, 2822 + "node_modules/has-flag": { 2823 + "version": "4.0.0", 2824 + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 2825 + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 2826 + "dev": true, 2827 + "license": "MIT", 2828 + "engines": { 2829 + "node": ">=8" 2830 + } 2831 + }, 2832 + "node_modules/highlight.js": { 2833 + "version": "10.7.3", 2834 + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", 2835 + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", 2836 + "dev": true, 2837 + "license": "BSD-3-Clause", 2838 + "engines": { 2839 + "node": "*" 2840 + } 2841 + }, 2842 + "node_modules/hosted-git-info": { 2843 + "version": "9.0.3", 2844 + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.3.tgz", 2845 + "integrity": "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==", 2846 + "dev": true, 2847 + "license": "ISC", 2848 + "dependencies": { 2849 + "lru-cache": "^11.1.0" 2850 + }, 2851 + "engines": { 2852 + "node": "^20.17.0 || >=22.9.0" 2853 + } 2854 + }, 2855 + "node_modules/http-proxy-agent": { 2856 + "version": "7.0.2", 2857 + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", 2858 + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", 2859 + "dev": true, 2860 + "license": "MIT", 2861 + "dependencies": { 2862 + "agent-base": "^7.1.0", 2863 + "debug": "^4.3.4" 2864 + }, 2865 + "engines": { 2866 + "node": ">= 14" 2867 + } 2868 + }, 2869 + "node_modules/https-proxy-agent": { 2870 + "version": "7.0.6", 2871 + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", 2872 + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", 2873 + "dev": true, 2874 + "license": "MIT", 2875 + "dependencies": { 2876 + "agent-base": "^7.1.2", 2877 + "debug": "4" 2878 + }, 2879 + "engines": { 2880 + "node": ">= 14" 2881 + } 2882 + }, 2883 + "node_modules/ieee754": { 2884 + "version": "1.2.1", 2885 + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", 2886 + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", 2887 + "dev": true, 2888 + "funding": [ 2889 + { 2890 + "type": "github", 2891 + "url": "https://github.com/sponsors/feross" 2892 + }, 2893 + { 2894 + "type": "patreon", 2895 + "url": "https://www.patreon.com/feross" 2896 + }, 2897 + { 2898 + "type": "consulting", 2899 + "url": "https://feross.org/support" 2900 + } 2901 + ], 2902 + "license": "BSD-3-Clause" 2903 + }, 2904 + "node_modules/ignore": { 2905 + "version": "7.0.5", 2906 + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", 2907 + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", 2908 + "dev": true, 2909 + "license": "MIT", 2910 + "engines": { 2911 + "node": ">= 4" 2912 + } 2913 + }, 2914 + "node_modules/ip-address": { 2915 + "version": "10.2.0", 2916 + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", 2917 + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", 2918 + "dev": true, 2919 + "license": "MIT", 2920 + "engines": { 2921 + "node": ">= 12" 2922 + } 2923 + }, 2924 + "node_modules/is-fullwidth-code-point": { 2925 + "version": "3.0.0", 2926 + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 2927 + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", 2928 + "dev": true, 2929 + "license": "MIT", 2930 + "engines": { 2931 + "node": ">=8" 2932 + } 2933 + }, 2934 + "node_modules/json-bigint": { 2935 + "version": "1.0.0", 2936 + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", 2937 + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", 2938 + "dev": true, 2939 + "license": "MIT", 2940 + "dependencies": { 2941 + "bignumber.js": "^9.0.0" 2942 + } 2943 + }, 2944 + "node_modules/json-schema-to-ts": { 2945 + "version": "3.1.1", 2946 + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", 2947 + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", 2948 + "dev": true, 2949 + "license": "MIT", 2950 + "dependencies": { 2951 + "@babel/runtime": "^7.18.3", 2952 + "ts-algebra": "^2.0.0" 2953 + }, 2954 + "engines": { 2955 + "node": ">=16" 2956 + } 2957 + }, 2958 + "node_modules/json-schema-traverse": { 2959 + "version": "1.0.0", 2960 + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", 2961 + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", 2962 + "license": "MIT" 2963 + }, 2964 + "node_modules/jsonc-parser": { 2965 + "version": "3.3.1", 2966 + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", 2967 + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", 2968 + "license": "MIT" 2969 + }, 2970 + "node_modules/jwa": { 2971 + "version": "2.0.1", 2972 + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", 2973 + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", 2974 + "dev": true, 2975 + "license": "MIT", 2976 + "dependencies": { 2977 + "buffer-equal-constant-time": "^1.0.1", 2978 + "ecdsa-sig-formatter": "1.0.11", 2979 + "safe-buffer": "^5.0.1" 2980 + } 2981 + }, 2982 + "node_modules/jws": { 2983 + "version": "4.0.1", 2984 + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", 2985 + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", 2986 + "dev": true, 2987 + "license": "MIT", 2988 + "dependencies": { 2989 + "jwa": "^2.0.1", 2990 + "safe-buffer": "^5.0.1" 2991 + } 2992 + }, 2993 + "node_modules/koffi": { 2994 + "version": "2.16.2", 2995 + "resolved": "https://registry.npmjs.org/koffi/-/koffi-2.16.2.tgz", 2996 + "integrity": "sha512-owU0MRwv6xkrVqCd+33uw6BaYppkTRXbO/rVdJNI2dvZG0gzyRhYwW25eWtc5pauwK8TGh3AbkFONSezdykfSA==", 2997 + "dev": true, 2998 + "hasInstallScript": true, 2999 + "license": "MIT", 3000 + "optional": true, 3001 + "funding": { 3002 + "url": "https://liberapay.com/Koromix" 3003 + } 3004 + }, 3005 + "node_modules/long": { 3006 + "version": "5.3.2", 3007 + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", 3008 + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", 3009 + "dev": true, 3010 + "license": "Apache-2.0" 3011 + }, 3012 + "node_modules/lru-cache": { 3013 + "version": "11.3.6", 3014 + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz", 3015 + "integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==", 3016 + "dev": true, 3017 + "license": "BlueOak-1.0.0", 3018 + "engines": { 3019 + "node": "20 || >=22" 3020 + } 3021 + }, 3022 + "node_modules/marked": { 3023 + "version": "15.0.12", 3024 + "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", 3025 + "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", 3026 + "dev": true, 3027 + "license": "MIT", 3028 + "bin": { 3029 + "marked": "bin/marked.js" 3030 + }, 3031 + "engines": { 3032 + "node": ">= 18" 3033 + } 3034 + }, 3035 + "node_modules/mime-db": { 3036 + "version": "1.54.0", 3037 + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", 3038 + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", 3039 + "dev": true, 3040 + "license": "MIT", 3041 + "engines": { 3042 + "node": ">= 0.6" 3043 + } 3044 + }, 3045 + "node_modules/mime-types": { 3046 + "version": "3.0.2", 3047 + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", 3048 + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", 3049 + "dev": true, 3050 + "license": "MIT", 3051 + "dependencies": { 3052 + "mime-db": "^1.54.0" 3053 + }, 3054 + "engines": { 3055 + "node": ">=18" 3056 + }, 3057 + "funding": { 3058 + "type": "opencollective", 3059 + "url": "https://opencollective.com/express" 3060 + } 3061 + }, 3062 + "node_modules/minimatch": { 3063 + "version": "10.2.5", 3064 + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", 3065 + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", 3066 + "dev": true, 3067 + "license": "BlueOak-1.0.0", 3068 + "dependencies": { 3069 + "brace-expansion": "^5.0.5" 3070 + }, 3071 + "engines": { 3072 + "node": "18 || 20 || >=22" 3073 + }, 3074 + "funding": { 3075 + "url": "https://github.com/sponsors/isaacs" 3076 + } 3077 + }, 3078 + "node_modules/minipass": { 3079 + "version": "7.1.3", 3080 + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", 3081 + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", 3082 + "dev": true, 3083 + "license": "BlueOak-1.0.0", 3084 + "engines": { 3085 + "node": ">=16 || 14 >=14.17" 3086 + } 3087 + }, 3088 + "node_modules/ms": { 3089 + "version": "2.1.3", 3090 + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 3091 + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 3092 + "dev": true, 3093 + "license": "MIT" 3094 + }, 3095 + "node_modules/mz": { 3096 + "version": "2.7.0", 3097 + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", 3098 + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", 3099 + "dev": true, 3100 + "license": "MIT", 3101 + "dependencies": { 3102 + "any-promise": "^1.0.0", 3103 + "object-assign": "^4.0.1", 3104 + "thenify-all": "^1.0.0" 3105 + } 3106 + }, 3107 + "node_modules/netmask": { 3108 + "version": "2.1.1", 3109 + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", 3110 + "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", 3111 + "dev": true, 3112 + "license": "MIT", 3113 + "engines": { 3114 + "node": ">= 0.4.0" 3115 + } 3116 + }, 3117 + "node_modules/node-domexception": { 3118 + "version": "1.0.0", 3119 + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", 3120 + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", 3121 + "deprecated": "Use your platform's native DOMException instead", 3122 + "dev": true, 3123 + "funding": [ 3124 + { 3125 + "type": "github", 3126 + "url": "https://github.com/sponsors/jimmywarting" 3127 + }, 3128 + { 3129 + "type": "github", 3130 + "url": "https://paypal.me/jimmywarting" 3131 + } 3132 + ], 3133 + "license": "MIT", 3134 + "engines": { 3135 + "node": ">=10.5.0" 3136 + } 3137 + }, 3138 + "node_modules/node-fetch": { 3139 + "version": "3.3.2", 3140 + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", 3141 + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", 3142 + "dev": true, 3143 + "license": "MIT", 3144 + "dependencies": { 3145 + "data-uri-to-buffer": "^4.0.0", 3146 + "fetch-blob": "^3.1.4", 3147 + "formdata-polyfill": "^4.0.10" 3148 + }, 3149 + "engines": { 3150 + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" 3151 + }, 3152 + "funding": { 3153 + "type": "opencollective", 3154 + "url": "https://opencollective.com/node-fetch" 3155 + } 3156 + }, 3157 + "node_modules/object-assign": { 3158 + "version": "4.1.1", 3159 + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 3160 + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", 3161 + "dev": true, 3162 + "license": "MIT", 3163 + "engines": { 3164 + "node": ">=0.10.0" 3165 + } 3166 + }, 3167 + "node_modules/once": { 3168 + "version": "1.4.0", 3169 + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 3170 + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", 3171 + "dev": true, 3172 + "license": "ISC", 3173 + "dependencies": { 3174 + "wrappy": "1" 3175 + } 3176 + }, 3177 + "node_modules/openai": { 3178 + "version": "6.26.0", 3179 + "resolved": "https://registry.npmjs.org/openai/-/openai-6.26.0.tgz", 3180 + "integrity": "sha512-zd23dbWTjiJ6sSAX6s0HrCZi41JwTA1bQVs0wLQPZ2/5o2gxOJA5wh7yOAUgwYybfhDXyhwlpeQf7Mlgx8EOCA==", 3181 + "dev": true, 3182 + "license": "Apache-2.0", 3183 + "bin": { 3184 + "openai": "bin/cli" 3185 + }, 3186 + "peerDependencies": { 3187 + "ws": "^8.18.0", 3188 + "zod": "^3.25 || ^4.0" 3189 + }, 3190 + "peerDependenciesMeta": { 3191 + "ws": { 3192 + "optional": true 3193 + }, 3194 + "zod": { 3195 + "optional": true 3196 + } 3197 + } 3198 + }, 3199 + "node_modules/p-retry": { 3200 + "version": "4.6.2", 3201 + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", 3202 + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", 3203 + "dev": true, 3204 + "license": "MIT", 3205 + "dependencies": { 3206 + "@types/retry": "0.12.0", 3207 + "retry": "^0.13.1" 3208 + }, 3209 + "engines": { 3210 + "node": ">=8" 3211 + } 3212 + }, 3213 + "node_modules/pac-proxy-agent": { 3214 + "version": "7.2.0", 3215 + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", 3216 + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", 3217 + "dev": true, 3218 + "license": "MIT", 3219 + "dependencies": { 3220 + "@tootallnate/quickjs-emscripten": "^0.23.0", 3221 + "agent-base": "^7.1.2", 3222 + "debug": "^4.3.4", 3223 + "get-uri": "^6.0.1", 3224 + "http-proxy-agent": "^7.0.0", 3225 + "https-proxy-agent": "^7.0.6", 3226 + "pac-resolver": "^7.0.1", 3227 + "socks-proxy-agent": "^8.0.5" 3228 + }, 3229 + "engines": { 3230 + "node": ">= 14" 3231 + } 3232 + }, 3233 + "node_modules/pac-resolver": { 3234 + "version": "7.0.1", 3235 + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", 3236 + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", 3237 + "dev": true, 3238 + "license": "MIT", 3239 + "dependencies": { 3240 + "degenerator": "^5.0.0", 3241 + "netmask": "^2.0.2" 3242 + }, 3243 + "engines": { 3244 + "node": ">= 14" 3245 + } 3246 + }, 3247 + "node_modules/parse5": { 3248 + "version": "5.1.1", 3249 + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", 3250 + "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==", 3251 + "dev": true, 3252 + "license": "MIT" 3253 + }, 3254 + "node_modules/parse5-htmlparser2-tree-adapter": { 3255 + "version": "6.0.1", 3256 + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", 3257 + "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", 3258 + "dev": true, 3259 + "license": "MIT", 3260 + "dependencies": { 3261 + "parse5": "^6.0.1" 3262 + } 3263 + }, 3264 + "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": { 3265 + "version": "6.0.1", 3266 + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", 3267 + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", 3268 + "dev": true, 3269 + "license": "MIT" 3270 + }, 3271 + "node_modules/partial-json": { 3272 + "version": "0.1.7", 3273 + "resolved": "https://registry.npmjs.org/partial-json/-/partial-json-0.1.7.tgz", 3274 + "integrity": "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==", 3275 + "dev": true, 3276 + "license": "MIT" 3277 + }, 3278 + "node_modules/path-expression-matcher": { 3279 + "version": "1.5.0", 3280 + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", 3281 + "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", 3282 + "dev": true, 3283 + "funding": [ 3284 + { 3285 + "type": "github", 3286 + "url": "https://github.com/sponsors/NaturalIntelligence" 3287 + } 3288 + ], 3289 + "license": "MIT", 3290 + "engines": { 3291 + "node": ">=14.0.0" 3292 + } 3293 + }, 3294 + "node_modules/path-scurry": { 3295 + "version": "2.0.2", 3296 + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", 3297 + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", 3298 + "dev": true, 3299 + "license": "BlueOak-1.0.0", 3300 + "dependencies": { 3301 + "lru-cache": "^11.0.0", 3302 + "minipass": "^7.1.2" 3303 + }, 3304 + "engines": { 3305 + "node": "18 || 20 || >=22" 3306 + }, 3307 + "funding": { 3308 + "url": "https://github.com/sponsors/isaacs" 3309 + } 3310 + }, 3311 + "node_modules/pend": { 3312 + "version": "1.2.0", 3313 + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", 3314 + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", 3315 + "dev": true, 3316 + "license": "MIT" 3317 + }, 3318 + "node_modules/picomatch": { 3319 + "version": "4.0.4", 3320 + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", 3321 + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", 3322 + "license": "MIT", 3323 + "engines": { 3324 + "node": ">=12" 3325 + }, 3326 + "funding": { 3327 + "url": "https://github.com/sponsors/jonschlinkert" 3328 + } 3329 + }, 3330 + "node_modules/proper-lockfile": { 3331 + "version": "4.1.2", 3332 + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", 3333 + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", 3334 + "dev": true, 3335 + "license": "MIT", 3336 + "dependencies": { 3337 + "graceful-fs": "^4.2.4", 3338 + "retry": "^0.12.0", 3339 + "signal-exit": "^3.0.2" 3340 + } 3341 + }, 3342 + "node_modules/proper-lockfile/node_modules/retry": { 3343 + "version": "0.12.0", 3344 + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", 3345 + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", 3346 + "dev": true, 3347 + "license": "MIT", 3348 + "engines": { 3349 + "node": ">= 4" 3350 + } 3351 + }, 3352 + "node_modules/protobufjs": { 3353 + "version": "7.5.7", 3354 + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.7.tgz", 3355 + "integrity": "sha512-NGnrxS/nLKUo5nkbVQxlC71sB4hdfImdYIbFeSCidxtwATx0AHRPcANSLd0q5Bb2BkoSWo2iisQhGg5/r+ihbA==", 3356 + "dev": true, 3357 + "hasInstallScript": true, 3358 + "license": "BSD-3-Clause", 3359 + "dependencies": { 3360 + "@protobufjs/aspromise": "^1.1.2", 3361 + "@protobufjs/base64": "^1.1.2", 3362 + "@protobufjs/codegen": "^2.0.5", 3363 + "@protobufjs/eventemitter": "^1.1.0", 3364 + "@protobufjs/fetch": "^1.1.0", 3365 + "@protobufjs/float": "^1.0.2", 3366 + "@protobufjs/inquire": "^1.1.1", 3367 + "@protobufjs/path": "^1.1.2", 3368 + "@protobufjs/pool": "^1.1.0", 3369 + "@protobufjs/utf8": "^1.1.1", 3370 + "@types/node": ">=13.7.0", 3371 + "long": "^5.0.0" 3372 + }, 3373 + "engines": { 3374 + "node": ">=12.0.0" 3375 + } 3376 + }, 3377 + "node_modules/proxy-agent": { 3378 + "version": "6.5.0", 3379 + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", 3380 + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", 3381 + "dev": true, 3382 + "license": "MIT", 3383 + "dependencies": { 3384 + "agent-base": "^7.1.2", 3385 + "debug": "^4.3.4", 3386 + "http-proxy-agent": "^7.0.1", 3387 + "https-proxy-agent": "^7.0.6", 3388 + "lru-cache": "^7.14.1", 3389 + "pac-proxy-agent": "^7.1.0", 3390 + "proxy-from-env": "^1.1.0", 3391 + "socks-proxy-agent": "^8.0.5" 3392 + }, 3393 + "engines": { 3394 + "node": ">= 14" 3395 + } 3396 + }, 3397 + "node_modules/proxy-agent/node_modules/lru-cache": { 3398 + "version": "7.18.3", 3399 + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", 3400 + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", 3401 + "dev": true, 3402 + "license": "ISC", 3403 + "engines": { 3404 + "node": ">=12" 3405 + } 3406 + }, 3407 + "node_modules/proxy-from-env": { 3408 + "version": "1.1.0", 3409 + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", 3410 + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", 3411 + "dev": true, 3412 + "license": "MIT" 3413 + }, 3414 + "node_modules/pump": { 3415 + "version": "3.0.4", 3416 + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", 3417 + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", 3418 + "dev": true, 3419 + "license": "MIT", 3420 + "dependencies": { 3421 + "end-of-stream": "^1.1.0", 3422 + "once": "^1.3.1" 3423 + } 3424 + }, 3425 + "node_modules/require-directory": { 3426 + "version": "2.1.1", 3427 + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", 3428 + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", 3429 + "dev": true, 3430 + "license": "MIT", 3431 + "engines": { 3432 + "node": ">=0.10.0" 3433 + } 3434 + }, 3435 + "node_modules/require-from-string": { 3436 + "version": "2.0.2", 3437 + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", 3438 + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", 3439 + "license": "MIT", 3440 + "engines": { 3441 + "node": ">=0.10.0" 3442 + } 3443 + }, 3444 + "node_modules/retry": { 3445 + "version": "0.13.1", 3446 + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", 3447 + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", 3448 + "dev": true, 3449 + "license": "MIT", 3450 + "engines": { 3451 + "node": ">= 4" 3452 + } 3453 + }, 3454 + "node_modules/safe-buffer": { 3455 + "version": "5.2.1", 3456 + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 3457 + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 3458 + "dev": true, 3459 + "funding": [ 3460 + { 3461 + "type": "github", 3462 + "url": "https://github.com/sponsors/feross" 3463 + }, 3464 + { 3465 + "type": "patreon", 3466 + "url": "https://www.patreon.com/feross" 3467 + }, 3468 + { 3469 + "type": "consulting", 3470 + "url": "https://feross.org/support" 3471 + } 3472 + ], 3473 + "license": "MIT" 3474 + }, 3475 + "node_modules/signal-exit": { 3476 + "version": "3.0.7", 3477 + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", 3478 + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", 3479 + "dev": true, 3480 + "license": "ISC" 3481 + }, 3482 + "node_modules/smart-buffer": { 3483 + "version": "4.2.0", 3484 + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", 3485 + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", 3486 + "dev": true, 3487 + "license": "MIT", 3488 + "engines": { 3489 + "node": ">= 6.0.0", 3490 + "npm": ">= 3.0.0" 3491 + } 3492 + }, 3493 + "node_modules/socks": { 3494 + "version": "2.8.9", 3495 + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", 3496 + "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", 3497 + "dev": true, 3498 + "license": "MIT", 3499 + "dependencies": { 3500 + "ip-address": "^10.1.1", 3501 + "smart-buffer": "^4.2.0" 3502 + }, 3503 + "engines": { 3504 + "node": ">= 10.0.0", 3505 + "npm": ">= 3.0.0" 3506 + } 3507 + }, 3508 + "node_modules/socks-proxy-agent": { 3509 + "version": "8.0.5", 3510 + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", 3511 + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", 3512 + "dev": true, 3513 + "license": "MIT", 3514 + "dependencies": { 3515 + "agent-base": "^7.1.2", 3516 + "debug": "^4.3.4", 3517 + "socks": "^2.8.3" 3518 + }, 3519 + "engines": { 3520 + "node": ">= 14" 3521 + } 3522 + }, 3523 + "node_modules/source-map": { 3524 + "version": "0.6.1", 3525 + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 3526 + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 3527 + "dev": true, 3528 + "license": "BSD-3-Clause", 3529 + "optional": true, 3530 + "engines": { 3531 + "node": ">=0.10.0" 3532 + } 3533 + }, 3534 + "node_modules/std-env": { 3535 + "version": "3.10.0", 3536 + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", 3537 + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", 3538 + "dev": true, 3539 + "license": "MIT" 3540 + }, 3541 + "node_modules/string-width": { 3542 + "version": "4.2.3", 3543 + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 3544 + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 3545 + "dev": true, 3546 + "license": "MIT", 3547 + "dependencies": { 3548 + "emoji-regex": "^8.0.0", 3549 + "is-fullwidth-code-point": "^3.0.0", 3550 + "strip-ansi": "^6.0.1" 3551 + }, 3552 + "engines": { 3553 + "node": ">=8" 3554 + } 3555 + }, 3556 + "node_modules/string-width/node_modules/ansi-regex": { 3557 + "version": "5.0.1", 3558 + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 3559 + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 3560 + "dev": true, 3561 + "license": "MIT", 3562 + "engines": { 3563 + "node": ">=8" 3564 + } 3565 + }, 3566 + "node_modules/string-width/node_modules/strip-ansi": { 3567 + "version": "6.0.1", 3568 + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 3569 + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 3570 + "dev": true, 3571 + "license": "MIT", 3572 + "dependencies": { 3573 + "ansi-regex": "^5.0.1" 3574 + }, 3575 + "engines": { 3576 + "node": ">=8" 3577 + } 3578 + }, 3579 + "node_modules/strip-ansi": { 3580 + "version": "7.2.0", 3581 + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", 3582 + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", 3583 + "dev": true, 3584 + "license": "MIT", 3585 + "dependencies": { 3586 + "ansi-regex": "^6.2.2" 3587 + }, 3588 + "engines": { 3589 + "node": ">=12" 3590 + }, 3591 + "funding": { 3592 + "url": "https://github.com/chalk/strip-ansi?sponsor=1" 3593 + } 3594 + }, 3595 + "node_modules/strnum": { 3596 + "version": "2.3.0", 3597 + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz", 3598 + "integrity": "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==", 3599 + "dev": true, 3600 + "funding": [ 3601 + { 3602 + "type": "github", 3603 + "url": "https://github.com/sponsors/NaturalIntelligence" 3604 + } 3605 + ], 3606 + "license": "MIT" 3607 + }, 3608 + "node_modules/strtok3": { 3609 + "version": "10.3.5", 3610 + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz", 3611 + "integrity": "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==", 3612 + "dev": true, 3613 + "license": "MIT", 3614 + "dependencies": { 3615 + "@tokenizer/token": "^0.3.0" 3616 + }, 3617 + "engines": { 3618 + "node": ">=18" 3619 + }, 3620 + "funding": { 3621 + "type": "github", 3622 + "url": "https://github.com/sponsors/Borewit" 3623 + } 3624 + }, 3625 + "node_modules/supports-color": { 3626 + "version": "7.2.0", 3627 + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 3628 + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 3629 + "dev": true, 3630 + "license": "MIT", 3631 + "dependencies": { 3632 + "has-flag": "^4.0.0" 3633 + }, 3634 + "engines": { 3635 + "node": ">=8" 3636 + } 3637 + }, 3638 + "node_modules/thenify": { 3639 + "version": "3.3.1", 3640 + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", 3641 + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", 3642 + "dev": true, 3643 + "license": "MIT", 3644 + "dependencies": { 3645 + "any-promise": "^1.0.0" 3646 + } 3647 + }, 3648 + "node_modules/thenify-all": { 3649 + "version": "1.6.0", 3650 + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", 3651 + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", 3652 + "dev": true, 3653 + "license": "MIT", 3654 + "dependencies": { 3655 + "thenify": ">= 3.1.0 < 4" 3656 + }, 3657 + "engines": { 3658 + "node": ">=0.8" 3659 + } 3660 + }, 3661 + "node_modules/token-types": { 3662 + "version": "6.1.2", 3663 + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", 3664 + "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", 3665 + "dev": true, 3666 + "license": "MIT", 3667 + "dependencies": { 3668 + "@borewit/text-codec": "^0.2.1", 3669 + "@tokenizer/token": "^0.3.0", 3670 + "ieee754": "^1.2.1" 3671 + }, 3672 + "engines": { 3673 + "node": ">=14.16" 3674 + }, 3675 + "funding": { 3676 + "type": "github", 3677 + "url": "https://github.com/sponsors/Borewit" 3678 + } 3679 + }, 3680 + "node_modules/ts-algebra": { 3681 + "version": "2.0.0", 3682 + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", 3683 + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", 3684 + "dev": true, 3685 + "license": "MIT" 3686 + }, 3687 + "node_modules/tslib": { 3688 + "version": "2.8.1", 3689 + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", 3690 + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", 3691 + "dev": true, 3692 + "license": "0BSD" 3693 + }, 3694 + "node_modules/typebox": { 3695 + "version": "1.1.38", 3696 + "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.1.38.tgz", 3697 + "integrity": "sha512-pZ0aQPmMmXoUvSbeuWf/Hzsc+avNw/Zd6VeE8CFgkVGWyuHPJvqeJJDeJqLve+K70LvjYIoleGcoJHPT17cWoA==", 3698 + "dev": true, 3699 + "license": "MIT" 3700 + }, 3701 + "node_modules/typescript": { 3702 + "version": "5.9.3", 3703 + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", 3704 + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", 3705 + "dev": true, 3706 + "license": "Apache-2.0", 3707 + "bin": { 3708 + "tsc": "bin/tsc", 3709 + "tsserver": "bin/tsserver" 3710 + }, 3711 + "engines": { 3712 + "node": ">=14.17" 3713 + } 3714 + }, 3715 + "node_modules/uint8array-extras": { 3716 + "version": "1.5.0", 3717 + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", 3718 + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", 3719 + "dev": true, 3720 + "license": "MIT", 3721 + "engines": { 3722 + "node": ">=18" 3723 + }, 3724 + "funding": { 3725 + "url": "https://github.com/sponsors/sindresorhus" 3726 + } 3727 + }, 3728 + "node_modules/undici": { 3729 + "version": "7.25.0", 3730 + "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", 3731 + "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", 3732 + "dev": true, 3733 + "license": "MIT", 3734 + "engines": { 3735 + "node": ">=20.18.1" 3736 + } 3737 + }, 3738 + "node_modules/undici-types": { 3739 + "version": "6.21.0", 3740 + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", 3741 + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", 3742 + "dev": true, 3743 + "license": "MIT" 3744 + }, 3745 + "node_modules/uuid": { 3746 + "version": "14.0.0", 3747 + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz", 3748 + "integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==", 3749 + "dev": true, 3750 + "funding": [ 3751 + "https://github.com/sponsors/broofa", 3752 + "https://github.com/sponsors/ctavan" 3753 + ], 3754 + "license": "MIT", 3755 + "bin": { 3756 + "uuid": "dist-node/bin/uuid" 3757 + } 3758 + }, 3759 + "node_modules/web-streams-polyfill": { 3760 + "version": "3.3.3", 3761 + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", 3762 + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", 3763 + "dev": true, 3764 + "license": "MIT", 3765 + "engines": { 3766 + "node": ">= 8" 3767 + } 3768 + }, 3769 + "node_modules/wrap-ansi": { 3770 + "version": "7.0.0", 3771 + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", 3772 + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", 3773 + "dev": true, 3774 + "license": "MIT", 3775 + "dependencies": { 3776 + "ansi-styles": "^4.0.0", 3777 + "string-width": "^4.1.0", 3778 + "strip-ansi": "^6.0.0" 3779 + }, 3780 + "engines": { 3781 + "node": ">=10" 3782 + }, 3783 + "funding": { 3784 + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 3785 + } 3786 + }, 3787 + "node_modules/wrap-ansi/node_modules/ansi-regex": { 3788 + "version": "5.0.1", 3789 + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 3790 + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 3791 + "dev": true, 3792 + "license": "MIT", 3793 + "engines": { 3794 + "node": ">=8" 3795 + } 3796 + }, 3797 + "node_modules/wrap-ansi/node_modules/strip-ansi": { 3798 + "version": "6.0.1", 3799 + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 3800 + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 3801 + "dev": true, 3802 + "license": "MIT", 3803 + "dependencies": { 3804 + "ansi-regex": "^5.0.1" 3805 + }, 3806 + "engines": { 3807 + "node": ">=8" 3808 + } 3809 + }, 3810 + "node_modules/wrappy": { 3811 + "version": "1.0.2", 3812 + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 3813 + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", 3814 + "dev": true, 3815 + "license": "ISC" 3816 + }, 3817 + "node_modules/ws": { 3818 + "version": "8.20.0", 3819 + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", 3820 + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", 3821 + "dev": true, 3822 + "license": "MIT", 3823 + "engines": { 3824 + "node": ">=10.0.0" 3825 + }, 3826 + "peerDependencies": { 3827 + "bufferutil": "^4.0.1", 3828 + "utf-8-validate": ">=5.0.2" 3829 + }, 3830 + "peerDependenciesMeta": { 3831 + "bufferutil": { 3832 + "optional": true 3833 + }, 3834 + "utf-8-validate": { 3835 + "optional": true 3836 + } 3837 + } 3838 + }, 3839 + "node_modules/xml-naming": { 3840 + "version": "0.1.0", 3841 + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", 3842 + "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", 3843 + "dev": true, 3844 + "funding": [ 3845 + { 3846 + "type": "github", 3847 + "url": "https://github.com/sponsors/NaturalIntelligence" 3848 + } 3849 + ], 3850 + "license": "MIT", 3851 + "engines": { 3852 + "node": ">=16.0.0" 3853 + } 3854 + }, 3855 + "node_modules/y18n": { 3856 + "version": "5.0.8", 3857 + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", 3858 + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", 3859 + "dev": true, 3860 + "license": "ISC", 3861 + "engines": { 3862 + "node": ">=10" 3863 + } 3864 + }, 3865 + "node_modules/yaml": { 3866 + "version": "2.8.4", 3867 + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.4.tgz", 3868 + "integrity": "sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==", 3869 + "dev": true, 3870 + "license": "ISC", 3871 + "bin": { 3872 + "yaml": "bin.mjs" 3873 + }, 3874 + "engines": { 3875 + "node": ">= 14.6" 3876 + }, 3877 + "funding": { 3878 + "url": "https://github.com/sponsors/eemeli" 3879 + } 3880 + }, 3881 + "node_modules/yargs": { 3882 + "version": "16.2.0", 3883 + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", 3884 + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", 3885 + "dev": true, 3886 + "license": "MIT", 3887 + "dependencies": { 3888 + "cliui": "^7.0.2", 3889 + "escalade": "^3.1.1", 3890 + "get-caller-file": "^2.0.5", 3891 + "require-directory": "^2.1.1", 3892 + "string-width": "^4.2.0", 3893 + "y18n": "^5.0.5", 3894 + "yargs-parser": "^20.2.2" 3895 + }, 3896 + "engines": { 3897 + "node": ">=10" 3898 + } 3899 + }, 3900 + "node_modules/yargs-parser": { 3901 + "version": "20.2.9", 3902 + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", 3903 + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", 3904 + "dev": true, 3905 + "license": "ISC", 3906 + "engines": { 3907 + "node": ">=10" 3908 + } 3909 + }, 3910 + "node_modules/yauzl": { 3911 + "version": "2.10.0", 3912 + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", 3913 + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", 3914 + "dev": true, 3915 + "license": "MIT", 3916 + "dependencies": { 3917 + "buffer-crc32": "~0.2.3", 3918 + "fd-slicer": "~1.1.0" 3919 + } 3920 + }, 3921 + "node_modules/yoctocolors": { 3922 + "version": "2.1.2", 3923 + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", 3924 + "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", 3925 + "dev": true, 3926 + "license": "MIT", 3927 + "engines": { 3928 + "node": ">=18" 3929 + }, 3930 + "funding": { 3931 + "url": "https://github.com/sponsors/sindresorhus" 3932 + } 3933 + }, 3934 + "node_modules/zod": { 3935 + "version": "4.4.3", 3936 + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", 3937 + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", 3938 + "dev": true, 3939 + "license": "MIT", 3940 + "funding": { 3941 + "url": "https://github.com/sponsors/colinhacks" 3942 + } 3943 + }, 3944 + "node_modules/zod-to-json-schema": { 3945 + "version": "3.25.2", 3946 + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", 3947 + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", 3948 + "dev": true, 3949 + "license": "ISC", 3950 + "peerDependencies": { 3951 + "zod": "^3.25.28 || ^4" 3952 + } 3953 + } 3954 + } 3955 + }
+28
package.json
··· 1 + { 2 + "name": "pi-fence", 3 + "private": true, 4 + "version": "0.1.0", 5 + "type": "module", 6 + "scripts": { 7 + "clean": "echo 'nothing to clean'", 8 + "build": "echo 'nothing to build'", 9 + "check": "tsc --noEmit" 10 + }, 11 + "pi": { 12 + "extensions": [ 13 + "./index.ts" 14 + ] 15 + }, 16 + "devDependencies": { 17 + "@mariozechner/pi-coding-agent": "^0.72.0", 18 + "@types/node": "^22.0.0", 19 + "@types/picomatch": "^4.0.3", 20 + "typescript": "^5.5.0" 21 + }, 22 + "dependencies": { 23 + "ajv": "^8.20.0", 24 + "ajv-formats": "^3.0.1", 25 + "jsonc-parser": "^3.3.1", 26 + "picomatch": "^4.0.4" 27 + } 28 + }
+144
policy/decide.ts
··· 1 + /** 2 + * Pure decision function for the FS-policy layer. 3 + * 4 + * Given a tool name, an absolute canonicalized path, and a resolved 5 + * policy, return either `{ allow: true }` or a structured block decision. 6 + * Block decisions carry enough context for the structured payload sent 7 + * back to the LLM (PLAN §6). 8 + * 9 + * Rules (PLAN §5 step 3): 10 + * 11 + * Read tools (read, grep, find, ls): 12 + * - if matchesAny(denyRead) → block (deny overrides allow) 13 + * - else if defaultDenyRead && !matchesAny(allowRead) → block 14 + * - else → allow 15 + * 16 + * Write tools (write, edit): 17 + * - if matchesAny(denyWrite) → block 18 + * - else if !matchesAny(allowWrite) → block (writes are default-deny) 19 + * - else → allow 20 + * 21 + * Mandatory denyWrite paths from PLAN §11 are appended to denyWrite by 22 + * the resolver, so they're caught here without special-casing — except 23 + * that we tag the rule name as `pi-fence.mandatoryDenyWrite` when the 24 + * matched pattern came from the mandatory list, to make the structured 25 + * block message accurate and to signal to the grant flow that this 26 + * deny is non-overridable. 27 + */ 28 + 29 + import { matchesAny } from "./match.js"; 30 + import type { ResolvedPolicy } from "./resolve.js"; 31 + 32 + export type ReadTool = "read" | "grep" | "find" | "ls"; 33 + export type WriteTool = "write" | "edit"; 34 + export type FsTool = ReadTool | WriteTool; 35 + 36 + const READ_TOOLS = new Set<string>(["read", "grep", "find", "ls"]); 37 + const WRITE_TOOLS = new Set<string>(["write", "edit"]); 38 + 39 + export function isFsTool(name: string): name is FsTool { 40 + return READ_TOOLS.has(name) || WRITE_TOOLS.has(name); 41 + } 42 + 43 + export function isWriteTool(name: string): name is WriteTool { 44 + return WRITE_TOOLS.has(name); 45 + } 46 + 47 + export type Decision = 48 + | { allow: true } 49 + | { 50 + allow: false; 51 + rule: string; 52 + pattern: string | undefined; 53 + policySource: string | undefined; 54 + mandatory: boolean; 55 + }; 56 + 57 + export function decide(toolName: FsTool, absPath: string, policy: ResolvedPolicy): Decision { 58 + const fs = policy.resolved.filesystem ?? {}; 59 + const opts = { cwd: policy.cwd, home: policy.home, caseInsensitive: policy.caseInsensitive }; 60 + 61 + if (isWriteTool(toolName)) { 62 + const denied = matchesAny(absPath, fs.denyWrite, opts); 63 + if (denied.matched) { 64 + const mandatory = isMandatoryPattern(denied.pattern, policy.mandatoryDenyWrite); 65 + return { 66 + allow: false, 67 + rule: mandatory ? "pi-fence.mandatoryDenyWrite" : "filesystem.denyWrite", 68 + pattern: denied.pattern, 69 + policySource: mandatory ? "<pi-fence built-in>" : policy.primarySource, 70 + mandatory, 71 + }; 72 + } 73 + const allowed = matchesAny(absPath, fs.allowWrite, opts); 74 + if (!allowed.matched) { 75 + return { 76 + allow: false, 77 + rule: "filesystem.allowWrite", 78 + pattern: undefined, 79 + policySource: policy.primarySource, 80 + mandatory: false, 81 + }; 82 + } 83 + return { allow: true }; 84 + } 85 + 86 + // Read tools. 87 + const denied = matchesAny(absPath, fs.denyRead, opts); 88 + if (denied.matched) { 89 + return { 90 + allow: false, 91 + rule: "filesystem.denyRead", 92 + pattern: denied.pattern, 93 + policySource: policy.primarySource, 94 + mandatory: false, 95 + }; 96 + } 97 + if (fs.defaultDenyRead) { 98 + const allowed = matchesAny(absPath, fs.allowRead, opts); 99 + if (!allowed.matched) { 100 + return { 101 + allow: false, 102 + rule: "filesystem.defaultDenyRead", 103 + pattern: undefined, 104 + policySource: policy.primarySource, 105 + mandatory: false, 106 + }; 107 + } 108 + } 109 + return { allow: true }; 110 + } 111 + 112 + /** 113 + * Is this the pattern that matched one of our injected mandatory denies? 114 + * Compared by string equality; the resolver inserts mandatory entries 115 + * verbatim into denyWrite, and matchesAny returns the original pattern. 116 + */ 117 + function isMandatoryPattern(pattern: string | undefined, mandatory: string[]): boolean { 118 + if (!pattern) return false; 119 + return mandatory.includes(pattern); 120 + } 121 + 122 + /** 123 + * Format a Decision as the structured block payload defined in PLAN §6. 124 + * 125 + * Returns a `reason` string suitable for the `tool_call` event result. 126 + * The leading human prefix means terse error rendering still makes 127 + * sense; the JSON body lets the LLM self-correct or surface a clean 128 + * ask to the user. 129 + */ 130 + export function formatBlockReason(toolName: FsTool, absPath: string, decision: Extract<Decision, { allow: false }>): string { 131 + const body = { 132 + tool: toolName, 133 + path: absPath, 134 + matched_rule: decision.rule, 135 + matched_pattern: decision.pattern ?? null, 136 + policy_source: decision.policySource ?? null, 137 + suggestion: decision.mandatory 138 + ? "This path is on pi-fence's non-overridable deny list (writing here would compromise future Pi sessions). It cannot be granted via /fence or fence.jsonc. Find another path or ask the user to perform this write themselves." 139 + : "If this access is legitimate, ask the user to grant it. They can use /fence or add the path to filesystem." + 140 + (isWriteTool(toolName) ? "allowWrite" : "allowRead") + 141 + " in fence.jsonc.", 142 + }; 143 + return `pi-fence: blocked\n${JSON.stringify(body, null, 2)}`; 144 + }
+168
policy/lint.ts
··· 1 + /** 2 + * One-shot footgun lint (PLAN §10). 3 + * 4 + * Runs once per session, after `resolveConfig`. Probes the resolved 5 + * policy with canary paths and warns on common mis-configurations. 6 + * Never blocks. Each warning fires once per session via `ctx.ui.notify` 7 + * at "warning" level. 8 + * 9 + * Categories: 10 + * - cwd canaries: cwd === $HOME, allowRead matches secret-shaped paths 11 + * - allowWrite over system files 12 + * - top-level `**` patterns (matches everything) 13 + * - brace expansion (picomatch supports it, fence's Go matcher doesn't — 14 + * silent layer disagreement otherwise; PLAN §3) 15 + * - bare `**` segments without slashes (likely a typo for `*`) 16 + */ 17 + 18 + import * as os from "node:os"; 19 + import * as path from "node:path"; 20 + import { canonicalize, matchesAny } from "./match.js"; 21 + import type { ResolvedPolicy } from "./resolve.js"; 22 + 23 + export interface LintWarning { 24 + id: string; 25 + message: string; 26 + } 27 + 28 + /** 29 + * Compute lint warnings for the given resolved policy. Returns a list 30 + * the caller can hand to `ctx.ui.notify`. Pure; doesn't touch the UI. 31 + */ 32 + export function lintPolicy(policy: ResolvedPolicy): LintWarning[] { 33 + const out: LintWarning[] = []; 34 + if (!policy.available) return out; // nothing to lint 35 + 36 + const fs = policy.resolved.filesystem ?? {}; 37 + const matchOpts = { cwd: policy.cwd, home: policy.home, caseInsensitive: policy.caseInsensitive }; 38 + 39 + // 1. cwd === $HOME — running pi from $HOME means everything under home looks 40 + // like "the project" to fence's relative-pattern resolver. Almost never 41 + // what the user wants. 42 + if (path.resolve(policy.cwd) === path.resolve(os.homedir())) { 43 + out.push({ 44 + id: "cwd-is-home", 45 + message: 46 + "pi-fence: working directory is your $HOME. Fence policies that use `.` will be very permissive — consider running pi from a project subdirectory.", 47 + }); 48 + } 49 + 50 + // 2. allowRead matches a secret canary → over-broad allow rule. 51 + const readCanaries = [ 52 + path.join(policy.home, ".ssh/id_rsa"), 53 + path.join(policy.home, ".aws/credentials"), 54 + path.join(policy.home, ".bash_history"), 55 + "/etc/passwd", 56 + "/etc/shadow", 57 + ]; 58 + for (const canary of readCanaries) { 59 + // Canonicalize the canary so platforms with symlinked common paths 60 + // (e.g. /etc → /private/etc on macOS) compare apples to apples with 61 + // the realpath'd pattern prefixes inside matchesAny. 62 + const canonical = canonicalize(canary, policy.cwd); 63 + if (matchesAny(canonical, fs.allowRead, matchOpts).matched) { 64 + out.push({ 65 + id: `allow-read-canary:${canary}`, 66 + message: `pi-fence: filesystem.allowRead matches a sensitive path (${canary}). Tighten the rule.`, 67 + }); 68 + } 69 + } 70 + 71 + // 3. allowWrite matches a system-config canary. 72 + const writeCanaries = [ 73 + "/etc/hosts", 74 + "/etc/passwd", 75 + path.join(policy.home, ".bashrc"), 76 + path.join(policy.home, ".zshrc"), 77 + path.join(policy.home, ".profile"), 78 + ]; 79 + for (const canary of writeCanaries) { 80 + const canonical = canonicalize(canary, policy.cwd); 81 + if (matchesAny(canonical, fs.allowWrite, matchOpts).matched) { 82 + out.push({ 83 + id: `allow-write-canary:${canary}`, 84 + message: `pi-fence: filesystem.allowWrite matches a system-config path (${canary}). Tighten the rule.`, 85 + }); 86 + } 87 + } 88 + 89 + // 4. Top-level `**` patterns — match everything; almost certainly a bug. 90 + for (const [field, list] of [ 91 + ["allowRead", fs.allowRead], 92 + ["allowWrite", fs.allowWrite], 93 + ] as const) { 94 + if (list?.some((p) => p === "**" || p === "**/*")) { 95 + out.push({ 96 + id: `top-level-glob:${field}`, 97 + message: `pi-fence: filesystem.${field} contains a top-level "**" pattern that matches every file. Likely a configuration mistake.`, 98 + }); 99 + } 100 + } 101 + 102 + // 5. Brace expansion — picomatch supports {a,b}, fence's Go matcher does 103 + // not (PLAN §3 + §10). Without this warning the user thinks they've 104 + // denied three paths but only the Pi-tool layer is actually denying 105 + // them; bash sees no rule. 106 + for (const [field, list] of [ 107 + ["allowRead", fs.allowRead], 108 + ["denyRead", fs.denyRead], 109 + ["allowWrite", fs.allowWrite], 110 + ["denyWrite", fs.denyWrite], 111 + ] as const) { 112 + for (const pat of list ?? []) { 113 + if (hasBraceExpansion(pat)) { 114 + out.push({ 115 + id: `brace-pattern:${field}:${pat}`, 116 + message: `pi-fence: filesystem.${field} contains brace expansion (${pat}). Fence's bash-side matcher does not expand braces; only the Pi-tool layer would enforce this. Expand to separate entries.`, 117 + }); 118 + } 119 + if (hasBareDoubleStarSegment(pat)) { 120 + out.push({ 121 + id: `suspicious-glob:${field}:${pat}`, 122 + message: `pi-fence: filesystem.${field} pattern "${pat}" contains a "**" segment that isn't between slashes. Often a typo for "*".`, 123 + }); 124 + } 125 + } 126 + } 127 + 128 + return out; 129 + } 130 + 131 + /** Does the pattern contain `{a,b}` style brace alternation? (Escaped `\{` is ignored.) */ 132 + function hasBraceExpansion(pat: string): boolean { 133 + // Cheap heuristic: an unescaped `{` followed by an unescaped `,` followed by `}`. 134 + // We don't need to be perfect — false positives are warnings, not blocks. 135 + let depth = 0; 136 + let sawComma = false; 137 + for (let i = 0; i < pat.length; i++) { 138 + const c = pat[i]; 139 + const prev = i > 0 ? pat[i - 1] : ""; 140 + if (prev === "\\") continue; 141 + if (c === "{") { 142 + depth++; 143 + sawComma = false; 144 + } else if (c === "}") { 145 + if (depth > 0 && sawComma) return true; 146 + if (depth > 0) depth--; 147 + } else if (c === "," && depth > 0) { 148 + sawComma = true; 149 + } 150 + } 151 + return false; 152 + } 153 + 154 + /** 155 + * `**` only carries its glob meaning when bracketed by slashes (or at 156 + * pattern boundaries). `foo**bar` is just `foo*bar` to most matchers 157 + * and is almost always a typo. 158 + */ 159 + function hasBareDoubleStarSegment(pat: string): boolean { 160 + // Search for `**` that has a non-slash neighbor on at least one side 161 + // and isn't at a pattern boundary. 162 + const idx = pat.indexOf("**"); 163 + if (idx < 0) return false; 164 + const before = idx === 0 ? "/" : pat[idx - 1]; 165 + const afterIdx = idx + 2; 166 + const after = afterIdx >= pat.length ? "/" : pat[afterIdx]; 167 + return (before !== "/" && before !== undefined) || (after !== "/" && after !== undefined); 168 + }
+207
policy/match.ts
··· 1 + /** 2 + * Path canonicalization and glob matching for the FS-policy layer. 3 + * 4 + * Two responsibilities: 5 + * 6 + * 1. `canonicalize(rawPath, cwd)` — turn a user/LLM-supplied path into an 7 + * absolute, symlink-resolved path so the matcher can't be fooled by 8 + * `ln -s ~/.ssh ./innocent` or `..` traversals. We realpath the 9 + * deepest existing ancestor (so `write("./new-file")` still works 10 + * when `new-file` doesn't exist yet). 11 + * 12 + * 2. `matchesAny(absPath, patterns, opts)` — picomatch wrapper that 13 + * expands `~` and `.`/`./` against the cached realpath'd home and cwd, 14 + * handles directory patterns recursively (listing `/tmp` grants access 15 + * to everything under it), and respects the auto-detected case 16 + * sensitivity. Brace expansion is disabled (PLAN §3) — fence's Go 17 + * matcher doesn't support braces; allowing them on the Pi side would 18 + * create silent layer disagreement. 19 + */ 20 + 21 + import * as fs from "node:fs"; 22 + import * as path from "node:path"; 23 + import picomatch from "picomatch"; 24 + 25 + /** 26 + * Cache of realpath'd literal prefixes. Patterns repeat a lot across calls 27 + * (the same `allowRead` list is checked on every tool_call), and realpath 28 + * is a syscall — worth caching for the session. 29 + */ 30 + const prefixCache = new Map<string, string>(); 31 + 32 + /** 33 + * Resolve `rawPath` to an absolute, symlink-followed path. 34 + * 35 + * Algorithm (matches PLAN §5 step 2): 36 + * 1. `path.resolve(cwd, rawPath)` to make it absolute. 37 + * 2. Walk up from the leaf until we find an ancestor that exists. 38 + * 3. realpath that ancestor and re-append the missing tail. 39 + * 40 + * This handles writes to not-yet-existent files without spuriously 41 + * succeeding for symlinks deep in the path. If realpath fails entirely 42 + * (e.g. permission denied even on /), fall back to the lexically 43 + * resolved path — better to enforce on the un-canonicalized path than 44 + * to crash. 45 + */ 46 + export function canonicalize(rawPath: string, cwd: string): string { 47 + const abs = path.isAbsolute(rawPath) ? path.resolve(rawPath) : path.resolve(cwd, rawPath); 48 + const tail: string[] = []; 49 + let cur = abs; 50 + while (true) { 51 + try { 52 + const real = fs.realpathSync(cur); 53 + return tail.length === 0 ? real : path.join(real, ...tail.reverse()); 54 + } catch { 55 + const parent = path.dirname(cur); 56 + if (parent === cur) return abs; // hit filesystem root, give up 57 + tail.push(path.basename(cur)); 58 + cur = parent; 59 + } 60 + } 61 + } 62 + 63 + export interface MatchOptions { 64 + /** Realpath'd cwd; used to expand `.` and `./` patterns. */ 65 + cwd: string; 66 + /** Realpath'd home; used to expand `~` patterns. */ 67 + home: string; 68 + /** Whether the underlying filesystem is case-insensitive. */ 69 + caseInsensitive: boolean; 70 + } 71 + 72 + /** 73 + * Test whether an absolute, canonicalized path matches any pattern in the 74 + * list under the given options. 75 + * 76 + * Each pattern is normalized (tilde, dot, relative→absolute) and then 77 + * tested with picomatch. To honor the convention that listing a directory 78 + * grants access to its contents, we additionally test `<pattern>/**` for 79 + * directory-shaped patterns (no glob metacharacters past the basename). 80 + */ 81 + export function matchesAny(absPath: string, patterns: string[] | undefined, opts: MatchOptions): MatchResult { 82 + if (!patterns || patterns.length === 0) return { matched: false, pattern: undefined }; 83 + const picoOpts: picomatch.PicomatchOptions = { 84 + dot: true, 85 + nocase: opts.caseInsensitive, 86 + strictSlashes: true, 87 + nobrace: true, 88 + }; 89 + for (const raw of patterns) { 90 + const normalized = normalizePattern(raw, opts); 91 + if (matchOne(absPath, normalized, picoOpts)) return { matched: true, pattern: raw }; 92 + } 93 + return { matched: false, pattern: undefined }; 94 + } 95 + 96 + export interface MatchResult { 97 + matched: boolean; 98 + /** The original (un-normalized) pattern that matched, for logging. */ 99 + pattern: string | undefined; 100 + } 101 + 102 + /** 103 + * Test a single normalized pattern against a path. 104 + * 105 + * Three-way test, in order of likelihood: 106 + * 1. Exact string equality (e.g. pattern `/tmp/foo`, path `/tmp/foo`). 107 + * 2. Direct picomatch (handles `**`, `*`, character classes). 108 + * 3. `<pattern>/**` to grant access to a listed directory's contents. 109 + */ 110 + function matchOne(absPath: string, pattern: string, opts: picomatch.PicomatchOptions): boolean { 111 + if (opts.nocase) { 112 + if (absPath.toLowerCase() === pattern.toLowerCase()) return true; 113 + } else if (absPath === pattern) { 114 + return true; 115 + } 116 + if (picomatch.isMatch(absPath, pattern, opts)) return true; 117 + // Grant a listed directory recursive access without forcing users to write `dir/**`. 118 + // Skip this branch when the pattern already ends in `/**` or contains a glob — the 119 + // direct test above already handled those. 120 + if (!hasGlobChars(pattern) && picomatch.isMatch(absPath, joinSlash(pattern, "**"), opts)) return true; 121 + return false; 122 + } 123 + 124 + /** 125 + * Expand `~` and `.`/`./` and resolve relative non-glob patterns against `cwd`. 126 + * 127 + * Globs are left in place (e.g. `**\/.env` stays as-is so it matches anywhere). 128 + * Tilde expansion happens before any glob processing so the home path itself 129 + * can't contain glob metacharacters by accident. 130 + * 131 + * After expansion we realpath the literal *prefix* (everything before the 132 + * first glob metacharacter). This is necessary on platforms where common 133 + * paths are symlinks — e.g. macOS resolves `/tmp` to `/private/tmp`. Without 134 + * this, an `allowWrite: ["/tmp"]` rule would never match a canonicalized 135 + * `/private/tmp/foo` path. 136 + * 137 + * Patterns starting with `**` (intended to match anywhere) skip the 138 + * prefix-realpath step since they have no anchored prefix. 139 + */ 140 + function normalizePattern(raw: string, opts: MatchOptions): string { 141 + let p = raw; 142 + if (p === "~") p = opts.home; 143 + else if (p.startsWith("~/")) p = path.join(opts.home, p.slice(2)); 144 + else if (p === "." || p === "./") p = opts.cwd; 145 + else if (p.startsWith("./")) p = path.join(opts.cwd, p.slice(2)); 146 + else if (!path.isAbsolute(p) && !p.startsWith("**")) p = path.join(opts.cwd, p); 147 + 148 + if (p.startsWith("**")) return p; 149 + return realpathLiteralPrefix(p); 150 + } 151 + 152 + /** 153 + * Walk back from the first glob metacharacter, find the longest existing 154 + * ancestor, realpath it, and re-join the trailing portion. 155 + * 156 + * Pure-literal patterns (no glob chars) are realpath'd whole when they 157 + * exist — this is what makes `/tmp` equivalent to `/private/tmp` on macOS. 158 + */ 159 + function realpathLiteralPrefix(pattern: string): string { 160 + const cached = prefixCache.get(pattern); 161 + if (cached !== undefined) return cached; 162 + 163 + const firstGlob = pattern.search(/[*?\[\]]/); 164 + let head: string; 165 + let tail: string; 166 + if (firstGlob < 0) { 167 + head = pattern; 168 + tail = ""; 169 + } else { 170 + // Walk back to the previous slash so we don't split mid-segment 171 + // (e.g. `/tmp/foo*.txt` → head=`/tmp`, tail=`foo*.txt`). 172 + const lastSlash = pattern.lastIndexOf("/", firstGlob); 173 + if (lastSlash <= 0) { 174 + prefixCache.set(pattern, pattern); 175 + return pattern; 176 + } 177 + head = pattern.slice(0, lastSlash); 178 + tail = pattern.slice(lastSlash + 1); 179 + } 180 + 181 + let realHead = head; 182 + try { 183 + realHead = fs.realpathSync(head); 184 + } catch { 185 + // Path doesn't exist; leave as-is. The matcher will simply not match 186 + // against this pattern, which is the correct behavior (pattern names 187 + // a path that doesn't exist on this system). 188 + } 189 + const joined = tail ? `${realHead}/${tail}` : realHead; 190 + prefixCache.set(pattern, joined); 191 + return joined; 192 + } 193 + 194 + /** Test-only: clear the prefix cache. Production code never needs this. */ 195 + export function _resetPrefixCacheForTests(): void { 196 + prefixCache.clear(); 197 + } 198 + 199 + function hasGlobChars(s: string): boolean { 200 + return /[*?\[\]]/.test(s); 201 + } 202 + 203 + /** Join two path-ish segments with a forward slash, avoiding doubled separators. */ 204 + function joinSlash(a: string, b: string): string { 205 + if (a.endsWith("/")) return a + b; 206 + return a + "/" + b; 207 + }
+239
policy/resolve.ts
··· 1 + /** 2 + * Resolve the active fence policy by shelling out to `fence config show`. 3 + * 4 + * `fence config show` prints: 5 + * - stdout: the fully resolved JSON config (with extends/templates merged) 6 + * - stderr: human-readable chain like: 7 + * Active config chain: 8 + * project config: /path/to/fence.jsonc 9 + * └── builtin template: code 10 + * 11 + * We parse both, augment the policy with our non-overridable mandatory 12 + * denies (PLAN §11), detect filesystem case-sensitivity, and cache the 13 + * result for the session. Callers refresh on session_start and on 14 + * `/fence reload`. 15 + */ 16 + 17 + import { execFile as execFileCb } from "node:child_process"; 18 + import { promisify } from "node:util"; 19 + import { realpathSync, statSync } from "node:fs"; 20 + import * as os from "node:os"; 21 + import * as path from "node:path"; 22 + 23 + const execFile = promisify(execFileCb); 24 + 25 + /** A subset of the fence schema we actually consume. Everything else is preserved as-is. */ 26 + export interface FenceFilesystem { 27 + defaultDenyRead?: boolean; 28 + allowRead?: string[]; 29 + denyRead?: string[]; 30 + allowWrite?: string[]; 31 + denyWrite?: string[]; 32 + } 33 + 34 + export interface FenceConfig { 35 + filesystem?: FenceFilesystem; 36 + [k: string]: unknown; 37 + } 38 + 39 + export interface ResolvedPolicy { 40 + /** Fence binary present and `fence config show` succeeded. */ 41 + available: boolean; 42 + /** The merged config after our mandatory denies are appended. */ 43 + resolved: FenceConfig; 44 + /** Files in the resolution chain (project config, user global, etc.). Empty if none. */ 45 + chain: string[]; 46 + /** First project config file in the chain, if any — points at the file the user should edit. */ 47 + primarySource: string | undefined; 48 + /** True when the working directory's filesystem is case-insensitive (typical macOS APFS). */ 49 + caseInsensitive: boolean; 50 + /** Working directory used to resolve relative patterns (realpath'd). */ 51 + cwd: string; 52 + /** Home directory used to expand `~` in patterns (realpath'd). */ 53 + home: string; 54 + /** Mandatory denyWrite paths we appended (PLAN §11). Tagged so decide.ts can identify them. */ 55 + mandatoryDenyWrite: string[]; 56 + /** True when the resolved config carries any user-defined filesystem rules. Drives footer state. */ 57 + hasFsRules: boolean; 58 + /** Raw fence binary version string, when available. */ 59 + fenceVersion?: string; 60 + } 61 + 62 + export interface ResolveOptions { 63 + /** Working directory. Defaults to `process.cwd()`. */ 64 + cwd?: string; 65 + /** Override the fence binary name. Defaults to `"fence"` (looked up on PATH). */ 66 + bin?: string; 67 + /** Subprocess timeout in ms. Defaults to 5000. */ 68 + timeoutMs?: number; 69 + } 70 + 71 + /** 72 + * Run `fence config show` and produce a `ResolvedPolicy`. 73 + * 74 + * Returns `{ available: false }` (with empty fields) when the fence binary 75 + * is missing, errors out, or emits invalid JSON. Callers treat that as 76 + * fail-open per the symmetric-fail-open decision in PLAN §3. 77 + */ 78 + export async function resolveConfig(opts: ResolveOptions = {}): Promise<ResolvedPolicy> { 79 + const cwd = opts.cwd ?? process.cwd(); 80 + const bin = opts.bin ?? "fence"; 81 + const timeout = opts.timeoutMs ?? 5000; 82 + 83 + const home = realpathOrSelf(os.homedir()); 84 + const cwdReal = realpathOrSelf(cwd); 85 + const caseInsensitive = detectCaseInsensitive(cwdReal); 86 + const mandatoryDenyWrite = buildMandatoryDenyWrite(home); 87 + 88 + const empty: ResolvedPolicy = { 89 + available: false, 90 + resolved: {}, 91 + chain: [], 92 + primarySource: undefined, 93 + caseInsensitive, 94 + cwd: cwdReal, 95 + home, 96 + mandatoryDenyWrite, 97 + hasFsRules: false, 98 + }; 99 + 100 + let stdout: string; 101 + let stderr: string; 102 + try { 103 + const r = await execFile(bin, ["config", "show"], { cwd: cwdReal, timeout, encoding: "utf-8" }); 104 + stdout = r.stdout; 105 + stderr = r.stderr; 106 + } catch { 107 + return empty; 108 + } 109 + 110 + let parsed: FenceConfig; 111 + try { 112 + parsed = JSON.parse(stdout) as FenceConfig; 113 + } catch { 114 + return empty; 115 + } 116 + 117 + const chain = parseChain(stderr); 118 + const primarySource = chain.find((p) => p.endsWith(".jsonc") || p.endsWith(".json")); 119 + 120 + const hasFsRules = !!parsed.filesystem && Object.keys(parsed.filesystem).length > 0; 121 + 122 + // Append mandatory denies to denyWrite. denyWrite overrides allowWrite in 123 + // decide.ts, so this is enough to make them non-overridable on the FS layer. 124 + // Bash-side enforcement caveat is documented in PLAN §11. 125 + const fs0 = parsed.filesystem ?? {}; 126 + const mergedDenyWrite = uniq([...(fs0.denyWrite ?? []), ...mandatoryDenyWrite]); 127 + const merged: FenceConfig = { 128 + ...parsed, 129 + filesystem: { ...fs0, denyWrite: mergedDenyWrite }, 130 + }; 131 + 132 + return { 133 + available: true, 134 + resolved: merged, 135 + chain, 136 + primarySource, 137 + caseInsensitive, 138 + cwd: cwdReal, 139 + home, 140 + mandatoryDenyWrite, 141 + hasFsRules, 142 + }; 143 + } 144 + 145 + /** 146 + * Parse `fence config show` stderr into a list of source paths. 147 + * 148 + * Format observed in fence v0.1.x: 149 + * Active config chain: 150 + * project config: /tmp/foo/fence.jsonc 151 + * └── builtin template: code 152 + * builtin default (no config loaded from /Users/.../fence.json) 153 + * 154 + * We only extract paths from `project config:` and `user config:` lines. 155 + * Built-in templates have no path. The "no config loaded" line is metadata, 156 + * not an actual source. 157 + */ 158 + function parseChain(stderr: string): string[] { 159 + const out: string[] = []; 160 + for (const lineRaw of stderr.split("\n")) { 161 + const line = lineRaw.trim(); 162 + // Strip any tree-drawing prefix. 163 + const cleaned = line.replace(/^[└├│─\s]+/, ""); 164 + const m = /^(?:project|user|local|global) config:\s*(.+)$/i.exec(cleaned); 165 + if (m && m[1]) out.push(m[1].trim()); 166 + } 167 + return out; 168 + } 169 + 170 + /** 171 + * Mandatory deny-write paths (PLAN §11). These are non-overridable; the 172 + * resolver appends them to every loaded policy's `denyWrite`. 173 + * 174 + * We expand `~` here to absolute paths so the matcher can do simple 175 + * comparisons without re-running tilde expansion. 176 + */ 177 + function buildMandatoryDenyWrite(home: string): string[] { 178 + const j = (...p: string[]) => path.join(home, ...p); 179 + return [ 180 + // Pi extensions auto-load TS at startup → arbitrary code execution. 181 + j(".pi/agent/extensions/**"), 182 + // Skills become part of the system prompt → prompt injection. 183 + j(".pi/agent/skills/**"), 184 + j(".agents/skills/**"), 185 + // Prompt templates same effect as skills. 186 + j(".pi/agent/prompts/**"), 187 + // Editing the global fence config silently broadens policy machine-wide. 188 + j(".config/fence/**"), 189 + // Audit log integrity (covers rotated files via `*` suffix). 190 + j(".pi/agent/pi-fence.log"), 191 + j(".pi/agent/pi-fence.log.*"), 192 + ]; 193 + } 194 + 195 + /** 196 + * Detect a case-insensitive filesystem at `cwd` by stat'ing both the 197 + * lowercased and uppercased cwd path and comparing inodes. 198 + * 199 + * Falls back to a platform default when the probe is inconclusive (e.g. 200 + * cwd has no alphabetic characters, stat fails on the uppercase variant). 201 + * Tracks PLAN §3 decision: auto-detected, default-true on macOS APFS. 202 + */ 203 + function detectCaseInsensitive(cwd: string): boolean { 204 + const lower = cwd.toLowerCase(); 205 + const upper = cwd.toUpperCase(); 206 + if (lower === upper) { 207 + // No alpha characters to vary; default by platform. 208 + return process.platform === "darwin" || process.platform === "win32"; 209 + } 210 + try { 211 + const a = statSync(lower); 212 + const b = statSync(upper); 213 + if (a.ino === b.ino && a.dev === b.dev) return true; 214 + return false; 215 + } catch { 216 + return process.platform === "darwin" || process.platform === "win32"; 217 + } 218 + } 219 + 220 + /** Realpath if the path exists, otherwise return the input unchanged. */ 221 + function realpathOrSelf(p: string): string { 222 + try { 223 + return realpathSync(p); 224 + } catch { 225 + return p; 226 + } 227 + } 228 + 229 + function uniq<T>(arr: T[]): T[] { 230 + const seen = new Set<T>(); 231 + const out: T[] = []; 232 + for (const v of arr) { 233 + if (!seen.has(v)) { 234 + seen.add(v); 235 + out.push(v); 236 + } 237 + } 238 + return out; 239 + }
+283
schema/fence.schema.json
··· 1 + { 2 + "$id": "https://raw.githubusercontent.com/Use-Tusk/fence/main/docs/schema/fence.schema.json", 3 + "$schema": "https://json-schema.org/draft/2020-12/schema", 4 + "additionalProperties": false, 5 + "properties": { 6 + "$schema": { 7 + "format": "uri", 8 + "type": "string" 9 + }, 10 + "allowPty": { 11 + "description": "Allow the sandboxed process to allocate a pseudo-terminal (PTY). Required for interactive programs that need terminal control (e.g. vim, less, top).", 12 + "type": "boolean" 13 + }, 14 + "command": { 15 + "additionalProperties": false, 16 + "description": "Command execution restrictions. Controls which commands are blocked or allowed at preflight and runtime.", 17 + "properties": { 18 + "acceptSharedBinaryCannotRuntimeDeny": { 19 + "description": "Commands for which the shared-binary skip warning is silenced. Add a command here after investigating a collision and accepting that it cannot be blocked on this system.", 20 + "items": { 21 + "type": "string" 22 + }, 23 + "type": "array" 24 + }, 25 + "allow": { 26 + "description": "Commands that override a matching deny rule. Use to carve out specific exceptions from a broad deny pattern (e.g. allow \"git push origin docs\" when \"git push\" is denied).", 27 + "items": { 28 + "type": "string" 29 + }, 30 + "type": "array" 31 + }, 32 + "deny": { 33 + "description": "Commands or command prefixes the sandbox will refuse to run. Matched at preflight and, depending on runtimeExecPolicy, at runtime for child execs.", 34 + "items": { 35 + "type": "string" 36 + }, 37 + "type": "array" 38 + }, 39 + "runtimeExecPolicy": { 40 + "description": "Runtime child-process exec enforcement mode. \"path\" (default) uses executable-path masking for single-token denies. \"argv\" enables Linux-only argv-aware exec interception for child processes.", 41 + "enum": [ 42 + "path", 43 + "argv" 44 + ], 45 + "type": "string" 46 + }, 47 + "useDefaults": { 48 + "description": "Whether to include the built-in default deny list (shutdown, reboot, insmod, mkfs, etc.). Defaults to true when omitted. Set to false to manage the deny list entirely yourself.", 49 + "type": [ 50 + "boolean", 51 + "null" 52 + ] 53 + } 54 + }, 55 + "type": "object" 56 + }, 57 + "devices": { 58 + "additionalProperties": false, 59 + "properties": { 60 + "allow": { 61 + "items": { 62 + "pattern": "^/dev/.+", 63 + "type": "string" 64 + }, 65 + "type": "array" 66 + }, 67 + "mode": { 68 + "enum": [ 69 + "auto", 70 + "minimal", 71 + "host" 72 + ], 73 + "type": "string" 74 + } 75 + }, 76 + "type": "object" 77 + }, 78 + "extends": { 79 + "description": "Path or built-in template name to inherit base settings from (e.g. \"code\" or \"./base.json\"). Settings in this file are merged on top of the extended config.", 80 + "type": "string" 81 + }, 82 + "filesystem": { 83 + "additionalProperties": false, 84 + "description": "Filesystem access restrictions. Controls which paths may be read, written, or executed inside the sandbox.", 85 + "properties": { 86 + "allowExecute": { 87 + "description": "Paths the sandbox may execute (grants read and execute permission, but not directory listing). Use for binaries that must be reachable but whose parent directories should not be browsable.", 88 + "items": { 89 + "type": "string" 90 + }, 91 + "type": "array" 92 + }, 93 + "allowGitConfig": { 94 + "description": "If true, allow read access to ~/.gitconfig and ~/.config/git. Enable when git operations inside the sandbox need the user's identity or settings.", 95 + "type": "boolean" 96 + }, 97 + "allowRead": { 98 + "description": "Additional filesystem paths the sandbox may read. Accepts absolute paths and glob patterns.", 99 + "items": { 100 + "type": "string" 101 + }, 102 + "type": "array" 103 + }, 104 + "allowWrite": { 105 + "description": "Filesystem paths the sandbox may write to. Accepts absolute paths and glob patterns.", 106 + "items": { 107 + "type": "string" 108 + }, 109 + "type": "array" 110 + }, 111 + "defaultDenyRead": { 112 + "description": "If true, deny all filesystem reads by default. Only paths listed in allowRead (and essential system paths) remain readable. Use for strict read isolation.", 113 + "type": "boolean" 114 + }, 115 + "denyRead": { 116 + "description": "Paths explicitly blocked from reading, even if they would otherwise be permitted by allowRead or system defaults.", 117 + "items": { 118 + "type": "string" 119 + }, 120 + "type": "array" 121 + }, 122 + "denyWrite": { 123 + "description": "Paths explicitly blocked from writing, even if they would otherwise be permitted by allowWrite.", 124 + "items": { 125 + "type": "string" 126 + }, 127 + "type": "array" 128 + }, 129 + "strictDenyRead": { 130 + "description": "If true, suppress the default readable system paths that are normally added when defaultDenyRead is enabled. Only paths in allowRead will be readable. Implies defaultDenyRead.", 131 + "type": "boolean" 132 + }, 133 + "wslInterop": { 134 + "description": "Controls access to the WSL interop binary on Windows Subsystem for Linux. If omitted, auto-detected: WSL environments allow /init, non-WSL environments do not.", 135 + "type": [ 136 + "boolean", 137 + "null" 138 + ] 139 + } 140 + }, 141 + "type": "object" 142 + }, 143 + "forceNewSession": { 144 + "type": [ 145 + "boolean", 146 + "null" 147 + ] 148 + }, 149 + "macos": { 150 + "additionalProperties": false, 151 + "description": "macOS-specific advanced sandbox controls. Ignored on non-macOS platforms.", 152 + "properties": { 153 + "mach": { 154 + "additionalProperties": false, 155 + "description": "Mach and XPC permissions for the macOS Seatbelt backend.", 156 + "properties": { 157 + "lookup": { 158 + "description": "Additional Mach/XPC services the macOS sandbox may look up. Supports exact service names, trailing-wildcard prefixes like \"org.chromium.*\", and \"*\" to allow all Mach lookups.", 159 + "items": { 160 + "pattern": "^(\\*|[^*]+\\*?)$", 161 + "type": "string" 162 + }, 163 + "type": "array" 164 + }, 165 + "register": { 166 + "description": "Additional Mach/XPC services the macOS sandbox may register. Supports exact service names, trailing-wildcard prefixes like \"org.chromium.*\", and \"*\" to allow all Mach registrations.", 167 + "items": { 168 + "pattern": "^(\\*|[^*]+\\*?)$", 169 + "type": "string" 170 + }, 171 + "type": "array" 172 + } 173 + }, 174 + "type": "object" 175 + } 176 + }, 177 + "type": "object" 178 + }, 179 + "network": { 180 + "additionalProperties": false, 181 + "description": "Network access restrictions. Controls which domains the sandbox may connect to and how local networking is handled.", 182 + "properties": { 183 + "allowAllUnixSockets": { 184 + "description": "If true, allow connections to any Unix socket path. Overrides allowUnixSockets.", 185 + "type": "boolean" 186 + }, 187 + "allowLocalBinding": { 188 + "description": "Allow the sandbox to bind to local network ports. Enable this when the sandboxed process needs to run a local server.", 189 + "type": "boolean" 190 + }, 191 + "allowLocalOutbound": { 192 + "description": "Allow outbound connections to localhost and loopback addresses. If omitted, inherits the value of allowLocalBinding.", 193 + "type": [ 194 + "boolean", 195 + "null" 196 + ] 197 + }, 198 + "allowLocalOutboundPorts": { 199 + "description": "Linux-only. TCP ports on the host's 127.0.0.1 that the sandbox may connect to when allowLocalOutbound is true. Each listed port is forwarded from sandbox loopback to host loopback via a per-port bridge. Ignored on macOS (which allows arbitrary localhost ports when allowLocalOutbound is true).", 200 + "items": { 201 + "type": "integer" 202 + }, 203 + "type": "array" 204 + }, 205 + "allowUnixSockets": { 206 + "description": "Unix socket paths the sandbox may connect to (e.g. /var/run/docker.sock).", 207 + "items": { 208 + "type": "string" 209 + }, 210 + "type": "array" 211 + }, 212 + "allowedDomains": { 213 + "description": "Domains the sandbox may connect to. Supports wildcards (e.g. *.example.com). Use \"*\" to allow all outbound connections. If empty, all outbound connections are blocked.", 214 + "items": { 215 + "type": "string" 216 + }, 217 + "type": "array" 218 + }, 219 + "deniedDomains": { 220 + "description": "Domains explicitly blocked even if they match allowedDomains. Evaluated before allowedDomains.", 221 + "items": { 222 + "type": "string" 223 + }, 224 + "type": "array" 225 + }, 226 + "httpProxyPort": { 227 + "description": "Port for the internal HTTP proxy used to enforce domain filtering. Set automatically by fence; only override for advanced configurations.", 228 + "type": "integer" 229 + }, 230 + "socksProxyPort": { 231 + "description": "Port for the internal SOCKS proxy used to enforce domain filtering. Set automatically by fence; only override for advanced configurations.", 232 + "type": "integer" 233 + } 234 + }, 235 + "type": "object" 236 + }, 237 + "ssh": { 238 + "additionalProperties": false, 239 + "description": "SSH command and host restrictions. Applies only to ssh invocations; does not affect other network access.", 240 + "properties": { 241 + "allowAllCommands": { 242 + "description": "If true, switch SSH command filtering to denylist mode: all remote commands are permitted except those in deniedCommands. When false (the default), allowedCommands acts as an allowlist.", 243 + "type": "boolean" 244 + }, 245 + "allowedCommands": { 246 + "description": "Commands permitted over SSH (allowlist mode). Only the listed commands may be executed on remote hosts. An empty list allows interactive sessions only.", 247 + "items": { 248 + "type": "string" 249 + }, 250 + "type": "array" 251 + }, 252 + "allowedHosts": { 253 + "description": "Host patterns the sandbox may SSH to. Supports wildcards (e.g. *.example.com, prod-*). SSH connections to hosts not matching any pattern are blocked.", 254 + "items": { 255 + "type": "string" 256 + }, 257 + "type": "array" 258 + }, 259 + "deniedCommands": { 260 + "description": "Commands blocked over SSH (denylist mode). Only meaningful when allowAllCommands is true.", 261 + "items": { 262 + "type": "string" 263 + }, 264 + "type": "array" 265 + }, 266 + "deniedHosts": { 267 + "description": "Host patterns explicitly blocked for SSH, even if they match allowedHosts. Evaluated before allowedHosts.", 268 + "items": { 269 + "type": "string" 270 + }, 271 + "type": "array" 272 + }, 273 + "inheritDeny": { 274 + "description": "If true, also apply the global command.deny rules to SSH remote commands.", 275 + "type": "boolean" 276 + } 277 + }, 278 + "type": "object" 279 + } 280 + }, 281 + "title": "Fence configuration schema", 282 + "type": "object" 283 + }
+12
tsconfig.json
··· 1 + { 2 + "compilerOptions": { 3 + "target": "ES2022", 4 + "module": "ES2022", 5 + "moduleResolution": "bundler", 6 + "strict": true, 7 + "skipLibCheck": true, 8 + "noEmit": true, 9 + "esModuleInterop": true 10 + }, 11 + "include": ["index.ts", "audit.ts", "policy/**/*.ts", "grants/**/*.ts"] 12 + }