specpi 0.19.1 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/README.md +4 -2
- package/SECURITY_MODEL.md +11 -1
- package/THIRD_PARTY.md +4 -0
- package/browser-runtime/package-lock.json +23 -0
- package/browser-runtime/package.json +2 -0
- package/docs/browser-testing.md +13 -1
- package/docs/structural-search.md +40 -0
- package/extensions/browser/accessibility.ts +155 -0
- package/extensions/browser/index.ts +40 -4
- package/extensions/browser/smoke.mjs +28 -0
- package/extensions/command-guard/index.ts +33 -1
- package/extensions/delegation/snapshot.mjs +20 -0
- package/extensions/structural-search/config.d.mts +7 -0
- package/extensions/structural-search/config.mjs +64 -0
- package/extensions/structural-search/core.d.mts +18 -0
- package/extensions/structural-search/core.mjs +383 -0
- package/extensions/structural-search/index.ts +228 -0
- package/extensions/structural-search/smoke.mjs +23 -0
- package/package.json +9 -2
- package/scripts/specpi.mjs +203 -5
- package/scripts/structural-runtime.mjs +211 -0
- package/structural-runtime/package-lock.json +172 -0
- package/structural-runtime/package.json +9 -0
- package/templates/AGENTS.md +6 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.20.0 - 2026-09-09
|
|
4
|
+
|
|
5
|
+
- Add opt-in `structural_search` with pinned ast-grep 0.45.3, protected explicit source selection, bounded output and subprocess cleanup, and exact-call approval in Strict mode. Enable with `specpi update --structural-search=on`, then restart Pi.
|
|
6
|
+
- Add `browser_accessibility` with axe-core 4.13.0 for the current browser state, fixed WCAG profiles, bounded violations and incomplete findings, and existing browser cancellation. Update the managed browser runtime to provision the scanner.
|
|
7
|
+
- Stage, smoke, verify and roll back structural runtime changes with the installer; preserve enablement and modified runtimes, qualify native hosts in CI, and keep downloaded binaries out of the npm artifact. An unparseable owned configuration is reported with its path instead of aborting `plan`, and only an explicit `--structural-search` selection rewrites it.
|
|
8
|
+
- Fix structural-search merge findings: preserve non-binary runtime changes using full-tree ownership checks, restore prior runtimes before failed cleanup, bound serialized enablement configuration, and dismiss expired/cancelled Strict approvals.
|
|
9
|
+
|
|
3
10
|
## 0.19.1 - 2026-09-08
|
|
4
11
|
|
|
5
12
|
- Allow Pi's exact public Copilot catalog identification headers during delegation, fixing Copilot → Anthropic → Copilot switches that were incorrectly rejected as runtime provider overrides. Reconstruct headers independently in the child; retain runtime-auth, extension-provider and other header restrictions. Restart Pi after updating the harness.
|
package/README.md
CHANGED
|
@@ -32,6 +32,8 @@ SpecPi extends Pi with chat, review tools, and focused delegation. Its principle
|
|
|
32
32
|
- **Focused delegation.** One agent makes changes. Up to two read-only subagents help investigate and review. [Research and design](https://tannermidd.github.io/SpecPi/single-agent/)
|
|
33
33
|
- **Background tasks.** Start an approved dev server, test suite, or watch build, inspect bounded output, and stop it without blocking other work.
|
|
34
34
|
- **Review as you work.** Track changed files, inspect diffs, check pages in a browser, and review risky commands.
|
|
35
|
+
- **Structural search.** Opt into bounded ast-grep patterns over selected code with `specpi update --structural-search=on`. [Usage and limits](docs/structural-search.md)
|
|
36
|
+
- **Accessibility checks.** Scan the current browser state for automated WCAG and optional best-practice findings. [Browser verification](docs/browser-testing.md)
|
|
35
37
|
- **Improvements you choose.** Record recurring problems, select one with `/harness-improvement`, and test the change before calling it done.
|
|
36
38
|
|
|
37
39
|
Problem collection is off by default. Its records stay on your machine. Pi connects to your chosen model provider.
|
|
@@ -49,7 +51,7 @@ specpi doctor
|
|
|
49
51
|
|
|
50
52
|
Preview the changes, confirm the install, then check the setup. Restart Pi when finished.
|
|
51
53
|
|
|
52
|
-
To pin this release, use `npm install --global specpi@0.
|
|
54
|
+
To pin this release, use `npm install --global specpi@0.20.0`.
|
|
53
55
|
|
|
54
56
|
Delegation is enabled at startup. Use `/delegate off` to turn it off.
|
|
55
57
|
|
|
@@ -63,7 +65,7 @@ Chat beside your code, attach files, and switch conversations.
|
|
|
63
65
|
|
|
64
66
|
In VS Code, find **SpecPi Chat** by **tannermidd** in Extensions and install it. Open **SpecPi** in the Activity Bar, then **Connect Pi**.
|
|
65
67
|
|
|
66
|
-
Chat 0.4.
|
|
68
|
+
Chat 0.4.4 shows a compact delegation strip only while workers run or settle, and opens workspace image links in the image viewer. SpecPi 0.19.1 fixes delegation when switching GitHub Copilot models in the same chat, including Luna → Opus → Luna. Update the harness and use **Restart Pi** to load the fix; this patch does not require a Chat update. Later model switches do not require a restart. New Chat sessions start with Guard off; use `/guard guard` or `/guard strict` to enable it.
|
|
67
69
|
|
|
68
70
|
## Go further
|
|
69
71
|
|
package/SECURITY_MODEL.md
CHANGED
|
@@ -183,6 +183,8 @@ Completion Challenge activation and structured results are bounded to the curren
|
|
|
183
183
|
|
|
184
184
|
## Browser isolation
|
|
185
185
|
|
|
186
|
+
The fixed `browser_accessibility` operation loads pinned axe-core/Playwright packages from the private browser runtime and scans an already open page. It exposes no model-supplied JavaScript, custom rules or exclusions. Results retain bounded rule guidance and target examples, not raw HTML/check payloads; selectors and URL paths can still contain private data and normal tool results may persist in Pi history. Findings include incomplete checks, profile/tags, viewport and scan timestamps. Navigation invalidates the result; dynamic DOM changes are not an atomic snapshot. Cancellation uses the existing browser teardown path. No automatic accessibility artifact or new external scanning service is introduced. The historical browser capability validator retains its original contract; installer/doctor request a separate additional accessibility smoke within the same browser launch.
|
|
187
|
+
|
|
186
188
|
SpecPi launches managed Chromium in a fresh Playwright context. It does not attach to a personal browser profile or load the user's cookies, saved passwords, or extensions. Browser pages still execute untrusted content and can reach URLs available to the host.
|
|
187
189
|
|
|
188
190
|
Snapshots, screenshots, page text, downloads, console output, and visual baselines may contain sensitive information. Default artifacts remain in SpecPi's private state directory. Explicit output publication is bounded and atomic, and existing artifacts or baselines are not replaced without explicit overwrite authorization.
|
|
@@ -193,6 +195,14 @@ Keyboard, native-selection, and condition-wait tools accept bounded declarative
|
|
|
193
195
|
|
|
194
196
|
The browser is not an operating-system or network sandbox. Use a container or VM for hostile applications and dedicated test accounts instead of personal authenticated sessions.
|
|
195
197
|
|
|
198
|
+
## Structural search
|
|
199
|
+
|
|
200
|
+
Structural search is disabled until explicitly selected. The owned `specpi/tool-integrations.json` file stores nonsecret enablement; plan does not mutate it, updates back it up and roll it back with managed state, and uninstall preserves it. Unparseable content does not abort the non-mutating plan and does not silently enable anything: install and update stop and name the file unless an explicit `--structural-search` selection rewrites it, and the prior bytes go to the operation backup first. A configuration that is a link or not a bounded regular file stays a hard failure that no selection rewrites. The extension itself treats any unreadable configuration as disabled. Runtime acquisition is private, pinned, script-free and transactional. Configuration output is checked against the same 16 KiB bound as reads before mutation, with compact JSON fallback. Doctor checks source/runtime integrity before its offline fixture; unverified or modified prior runtime directories are preserved rather than deleted. Runtime ownership now binds the acquired tree's contents, paths, directories, permissions and link targets; the tree fingerprint does not traverse links, and the marker is checked separately. Legacy binary-only markers remain unverified. Retirement rechecks the tree before deletion. Rollback quarantines the new runtime and restores the prior directory before attempting recursive cleanup, reporting any retained quarantine. No global executable search or PATH injection is used.
|
|
201
|
+
|
|
202
|
+
The parent captures explicit selected source with the existing snapshot path/private-store/link/descriptor/digest checks. A parent-only byte callback rechecks the original digest and clears its buffer after parsing. Existing workers gain no tool or byte API route. Files are capped at 1 MiB, selection at 8 MiB/200 files, pattern at 4 KiB, returned evidence at 24 KiB, raw stdout at 2 MiB and stderr at 8 KiB per call. Source bytes go to a reviewed native parser over stdin; only a neutral configuration is written to private scratch. Files containing secrets under ordinary source names are not automatically detectable.
|
|
203
|
+
|
|
204
|
+
Native execution uses a fixed argument array, `shell: false`, an absolute private binary, explicit neutral ast-grep configuration and minimal environment. No rewrite, rule import, custom grammar or arbitrary executable is exposed. Guard admission is source-bound and repeated before parsing; Strict uses exact-call approval, Off does not remove tool restrictions, and Locked/ambiguous state denies. Session/policy changes cancel queued work and invalidate pending authorization; approval dialogs receive the operation signal and remaining timeout. Pi 0.84.4 releases cancelled RPC approvals locally but does not send remote dismissal events; clients expire them using the forwarded timeout, and late replies cannot authorize cancelled work. A parser that does not confirm termination blocks further starts until cleanup and reload. These controls are not OS containment: trusted same-user native code and extensions retain host privileges.
|
|
205
|
+
|
|
196
206
|
## Website and automation
|
|
197
207
|
|
|
198
208
|
The GitHub Pages workflow publishes the checked-in `site/` directory only after the shared browser test job succeeds for that checked-out revision. The loopback rendered-site server rejects traversal and symlink escapes. Tests block third-party requests, use a synthetic clipboard, and upload only public-site failure screenshots with three-day retention; no visual baselines are created automatically. Required browser commands fail rather than skip when prerequisites are unavailable. Strict no-emit browser type checking uses local pinned development declarations; third-party declaration bodies and other extension implementations are outside that scoped gate.
|
|
@@ -211,7 +221,7 @@ Chat allows up to 90 seconds for an initial read-only RPC readiness probe before
|
|
|
211
221
|
|
|
212
222
|
The webview loads only packaged scripts/styles under a strict content security policy, forbids network resources, and renders model/tool/file text without interpreting embedded HTML. Images use validated data URLs under `img-src data:`; remote image URLs are never fetched by the webview. HTTP(S) links, including external image links, require a human click and pass through VS Code's external-link API. Clicked code references use a separate host action: paths are parsed, checked against the selected workspace, canonicalized, and restricted to regular non-sensitive files before VS Code opens the file and reveals a clamped line/range. Clicked workspace Markdown image references, and ordinary file links or inline references ending in PNG, JPEG, GIF or WebP extensions, use the same path boundary and the image validator before a preview is returned. The extension suffix chooses the viewer, not whether the file is safe or valid. Arbitrary command and URI handlers are not executed. Raw paths preserve literal percent signs; Markdown link destinations and explicit local file URLs are decoded once before filesystem validation.
|
|
213
223
|
|
|
214
|
-
Text/code context
|
|
224
|
+
Text/code context comes from an explicit attachment action or the enabled editor-selection offer when the user sends a message. The offer carries positions and document version, not source text; send-time collection preserves the exact selected characters and rejects a changed document. Context is size-bounded, rejects binary and known sensitive paths, and checks canonical workspace containment. Workspace file suggestions enumerate a bounded set of names; choosing a suggestion invokes the same attachment validation before reading contents. Root `.gitignore` filtering uses the bounded 64 KiB UTF-8 attachment reader with canonical containment, private-path, hard-link, and identity checks, and refuses symlinks and special files before opening. An unsafe or unavailable ignore file stops the operation; it does not silently disable filtering. Glob matching uses dynamic programming with bounded work rather than backtracking glob regexes. Malformed character classes invalidate only their own rule. Folder attachments contain directory metadata only, capped at 200 displayed entries / 16 KiB and 1,000 scanned entries across the snapshot (including hidden entries), with an explicit truncation notice. These output/work bounds do not guarantee filesystem I/O latency. Dropped workspace URIs are validated independently of their browser representation. Native image selection, clipboard images, and dropped image files are explicit attachment actions and may supply images from outside the selected workspace; filesystem image sources still require ordinary files and pass sensitive-path checks. Pi-state names such as `auth`, `trust`, `sessions`, `missions`, and `history` are reserved inside `.pi`, Chat-owned storage, and the configured Pi agent directory (including its canonical target), not arbitrary source directories. Global credential/key patterns remain blocked, including `auth.json`. Relative `PI_CODING_AGENT_DIR` overrides retain conservative name blocking because they resolve in the child workspace; use an absolute path or `~/…` to obtain directory-scoped filtering. These filename checks and format validators are not content-based secret detectors. Manually typed messages, ordinary files, and images may contain sensitive content, and the extension does not promise image metadata removal. Prompts and attachments are sent to the provider selected in Pi when the user sends them.
|
|
215
225
|
|
|
216
226
|
Image content is checked for supported PNG, JPEG, GIF, or WebP structure, declared MIME agreement, dimensions, and bounded base64 size. Each image is limited to 5 MiB, 16,384 pixels per side, and 40 million pixels. A prompt permits eight mixed attachments and 20 MiB of image bytes. The display retains at most 32 images / 20 MiB and reports omitted content; this is separate from Pi's persisted history. RPC records and buffered writes have independent 64 MiB caps. Recognized oversized history responses with a matching pending request are drained without accumulating the remaining bytes, then rejected without terminating Pi. The sidebar reports omitted history and remains usable; other oversized protocol records terminate the connection. A large historical transcript can exceed the RPC cap even when its original prompts were individually valid. Media caches remain in memory, are referenced by opaque identifiers while streaming, and are released as content is no longer retained. No remote image fetch, image processing service, or new image dependency is introduced. Pi/provider image limits and behavior remain upstream contracts.
|
|
217
227
|
|
package/THIRD_PARTY.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Third-party components
|
|
2
2
|
|
|
3
|
+
Structural search privately installs `@ast-grep/cli` **0.45.3**, its matching platform-specific native binary package, and `detect-libc` **2.1.2**, under the exact `structural-runtime/package-lock.json` graph. These packages use MIT licenses. Installation uses `npm ci --ignore-scripts`; SpecPi resolves the pinned platform binary directly and does not execute the CLI's postinstall copier, invoke a global `sg`, download a fallback or modify PATH. Source: [ast-grep](https://github.com/ast-grep/ast-grep). Windows x64, macOS x64/arm64 and Linux glibc x64 are the implemented platform mappings; CI and offline fixture smokes qualify the corresponding hosts.
|
|
4
|
+
|
|
5
|
+
Browser accessibility adds `@axe-core/playwright` **4.13.0** and `axe-core` **4.13.0** to the existing locked browser runtime, retaining Playwright **1.62.1**. Both Deque packages are MPL-2.0, remain unmodified, and retain their installed license/source notices. SpecPi's npm artifact ships dependency manifests/locks, not vendored scanner code; npm acquires the packages during confirmed browser setup. Source: [axe-core](https://github.com/dequelabs/axe-core), [Playwright integration](https://github.com/dequelabs/axe-core-npm). The fixed scanner runs in the existing isolated Chromium context, with no new external accessibility service.
|
|
6
|
+
|
|
3
7
|
SpecPi Chat is a separate VS Code extension built with the public VS Code Webview View API and Pi `0.84.4`'s documented RPC protocol. VS Code and the installed Pi runtime retain their upstream licenses; neither is bundled in the VSIX. The extension and its ZIP/VSIX packager use Node built-ins and first-party source only, with no added runtime or development dependency. Its tests reuse the repository's pinned Playwright and Pi packages. Provider authentication, requests, billing, and normal Pi background behavior remain owned by the configured Pi runtime.
|
|
4
8
|
|
|
5
9
|
Chat's provider Limits row recognizes the public RPC `setStatus` presentation contracts of `@llblab/pi-codex-usage` **0.9.3** (`aa-codex-usage`) and `@sreetej510/pi-usage` **0.9.9** (`provider-usage`). Public source was checked at commits `1a97ee5560c5d7dd3b99ce34e2dcfe456592f8a0` and `9dee3b590f736c723011e3bc3717b3f0d6e8072b`, respectively. Tests reproduce their status/notification contracts through synthetic local Pi RPC, not live accounts or upstream provider requests. Text retains each plugin's quota semantics rather than converting or aggregating measurements. Neither plugin is bundled in the VSIX; this compatibility change does not install pi-usage or change SpecPi's existing package pins. Plugins retain ownership of authentication, caching, refresh/backoff, and explicit reset-consumption confirmation.
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
"version": "0.1.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
|
+
"@axe-core/playwright": "4.13.0",
|
|
13
|
+
"axe-core": "4.13.0",
|
|
12
14
|
"pixelmatch": "7.2.0",
|
|
13
15
|
"playwright": "1.62.1",
|
|
14
16
|
"pngjs": "7.0.0"
|
|
@@ -17,6 +19,27 @@
|
|
|
17
19
|
"node": ">=22.19.0"
|
|
18
20
|
}
|
|
19
21
|
},
|
|
22
|
+
"node_modules/@axe-core/playwright": {
|
|
23
|
+
"version": "4.13.0",
|
|
24
|
+
"resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.13.0.tgz",
|
|
25
|
+
"integrity": "sha512-6YLx+kxXu5GJceG4ozFg+33a2EMTdjYwWGloJ3sb9Kta5pp+ZNS53uxGVog5JetIY8s++P5UrtX+cri+u0VAVg==",
|
|
26
|
+
"license": "MPL-2.0",
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"axe-core": "~4.13.0"
|
|
29
|
+
},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"playwright-core": ">= 1.0.0"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"node_modules/axe-core": {
|
|
35
|
+
"version": "4.13.0",
|
|
36
|
+
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.13.0.tgz",
|
|
37
|
+
"integrity": "sha512-UzGt8zg7Ny8djbYMhxl2zuEevVa7r2gJjYY5Lwr1xM7+XU2nd6CkIWFTVcCIbAP63vSz71NaVyyuSk9lHKcy0A==",
|
|
38
|
+
"license": "MPL-2.0",
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=4"
|
|
41
|
+
}
|
|
42
|
+
},
|
|
20
43
|
"node_modules/fsevents": {
|
|
21
44
|
"version": "2.3.2",
|
|
22
45
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
package/docs/browser-testing.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Browser application testing
|
|
2
2
|
|
|
3
|
+
## Accessibility
|
|
4
|
+
|
|
5
|
+
`browser_accessibility` scans the already open isolated page using `@axe-core/playwright` and axe-core 4.13.0. Open the page, perform the relevant interaction, wait for the intended visible state, scan, repair and repeat the same scan. It does not launch a blank browser. Continue keyboard, focus and functional checks; automated results are not certification.
|
|
6
|
+
|
|
7
|
+
Optional input: `include` (CSS selector matching exactly one region), `profile` (`wcag22aa`, the default, or `best-practice` to add those checks), `maxFindings` (default 50, maximum 100 per category) and `timeoutMs` (default 15000, 1000–30000 including queueing). The WCAG profile uses `wcag2a`, `wcag2aa`, `wcag21a`, `wcag21aa` and `wcag22aa`. There are no custom scripts/checks, rule suppressions or exclusion lists. Invalid or ambiguous scope fails without broadening the scan.
|
|
8
|
+
|
|
9
|
+
Results separate violations and incomplete checks, identify profile/tags, scan ID/timestamps, sanitized location and viewport, and return total counts plus explicit truncation within 24 KiB. Navigation invalidates the scan. Dynamic content is not an atomic snapshot; inaccessible frames and closed shadow roots can limit coverage. No raw HTML/check payloads or automatic scan files are retained, but selectors/paths can contain sensitive data and normal results may remain in Pi history. Cancellation uses the existing browser teardown/recovery path.
|
|
10
|
+
|
|
11
|
+
The managed browser lock includes the scanner. Run `specpi update` without browser/package skip flags to provision it. Missing scanner packages affect accessibility availability; existing screenshots and browser interactions can still work. Installer/doctor extend their offline browser smoke with a known unnamed button and a corrected control, without changing the historical wishlist validator's default smoke. Real Chromium tests exercise label, contrast, ARIA and repaired dynamic states at desktop/tablet/mobile sizes.
|
|
12
|
+
|
|
13
|
+
The offline fixtures also verify unnamed controls in a same-origin iframe and an open shadow root. The closed-root control is not reported and remains an explicit coverage limitation; cross-origin frame coverage is not qualified by these fixtures. Queue expiry and cancellation discard the browser, and reopening restores scanning. These seeded-page results do not establish coverage of arbitrary applications or replace keyboard and assistive-technology review.
|
|
14
|
+
|
|
3
15
|
## Agent workflow
|
|
4
16
|
|
|
5
17
|
1. `browser_open` opens an HTTP(S) page in isolated Chromium; `browser_snapshot` exposes bounded rendered text and namespaced control references.
|
|
@@ -47,7 +59,7 @@ npm run check:pi-package
|
|
|
47
59
|
|
|
48
60
|
### Scoped types
|
|
49
61
|
|
|
50
|
-
`tsconfig.browser.json` checks
|
|
62
|
+
`tsconfig.browser.json` checks browser `index.ts`, `accessibility.ts`, `diagnostics.ts`, `interactions.ts` and `lifecycle.ts`, plus the background and structural registration files and their narrow declaration boundaries, with strict checking and no emit. It uses real pinned Pi, TypeBox, Playwright and Node declarations. Browser runtime imports remain lazy; Playwright imports in the extension are type-only. The `.mjs` implementations have focused runtime tests and are **not** fully type-checked. `skipLibCheck` skips third-party declarations, not the selected first-party implementations. Other extensions remain syntax-checked, not advertised as type-checked. A negative fixture proves invalid key and Playwright API argument types fail without generated JavaScript.
|
|
51
63
|
|
|
52
64
|
### Rendered site
|
|
53
65
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Structural search
|
|
2
|
+
|
|
3
|
+
`structural_search` is an opt-in, read-only ast-grep 0.45.3 tool. It matches code structures across formatting changes. It does not resolve types/references, edit code or replace literal search for comments, strings and filenames.
|
|
4
|
+
|
|
5
|
+
Enable it through the installer, then restart Pi:
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
specpi plan --structural-search=on
|
|
9
|
+
specpi update --structural-search=on
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
For a fresh installation use `install`. `--yes` confirms installation but does not opt into structural search by itself. Omission preserves the existing choice. `--skip-package-install` and `--skip-tool-install` skip acquisition without disabling an existing runtime. Direct `pi install npm:specpi` does not provision the runtime; use the SpecPi installer for setup.
|
|
13
|
+
|
|
14
|
+
```json
|
|
15
|
+
{
|
|
16
|
+
"language": "typescript",
|
|
17
|
+
"pattern": "pi.registerTool($OPTIONS)",
|
|
18
|
+
"paths": ["extensions/browser/index.ts"],
|
|
19
|
+
"maxResults": 50,
|
|
20
|
+
"timeoutMs": 10000
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
JavaScript (`.js`, `.mjs`, `.cjs`), JSX, TypeScript (`.ts`, `.mts`, `.cts`), TSX and Python are supported. Select explicit relative files under the active working root, using ordinary discovery tools first. Directories, globs, custom grammars/configuration and rewriting are unavailable. A pattern with multiple independent statements may be rejected; syntax recovery can also accept malformed patterns, so inspect matches and retain project-native checks.
|
|
25
|
+
|
|
26
|
+
Limits are 200 files, 8 MiB of selected source in aggregate, 1 MiB per file, 6 KiB encoded path selection, 4 KiB pattern, 100 returned matches, 512 bytes per snippet and 24 KiB per result. Selection metadata must fit half the result budget; narrow long path lists when necessary. The default deadline is 10 seconds, configurable from 1–30 seconds including queueing and approval. There is one active parser and at most one queued request. Raw parser output is capped at 2 MiB, diagnostics at 8 KiB per call. Cleanup may take one additional second; unconfirmed cleanup blocks further parser starts until resolved and the extension is reloaded.
|
|
27
|
+
|
|
28
|
+
Results include source digests, searched/requested counts, ranges, engine version and explicit truncation. Lines and UTF-8 byte columns are one-based; end positions are exclusive. Columns are calculated from original source bytes, not the CLI's character-oriented display columns. A partial, timed-out, cancelled or unavailable result is not proof that other matches do not exist. Captured on-disk bytes exclude unsaved editor buffers and are not an atomic filesystem snapshot.
|
|
29
|
+
|
|
30
|
+
The parser receives selected source over stdin, a fixed argument list, a neutral configuration and a minimal environment. It runs from private temporary scratch containing configuration only. Source and results are not written to that scratch. The existing selected-source boundary rejects private namespaces, credentials, path escapes, links and hardlinks. It cannot detect secrets embedded in ordinary source. Returned snippets can persist in normal Pi conversation history.
|
|
31
|
+
|
|
32
|
+
Guard and Off permit these bounded reads; Strict asks for the exact call. The approval receives the operation cancellation signal and remaining timeout. Terminal prompts dismiss on cancellation; Pi 0.84.4 releases cancelled RPC approvals server-side but sends no remote dismissal event, so clients may display them until the forwarded timeout expires. Late answers cannot authorize the cancelled call. Locked or ambiguous Guard state denies. If Guard is absent, the same source/input limits remain. Policy, root and enablement are rechecked before parsing. A denial must not be retried through a different tool. Existing read-only workers do not gain structural-search access.
|
|
33
|
+
|
|
34
|
+
Enablement lives in `<agentDir>/specpi/tool-integrations.json`. The installer preserves unrelated fields, backs up the owned file before changes and rolls it back on failure. Serialized configuration must also fit the 16 KiB reader limit: the installer uses compact JSON when needed and rejects output that still exceeds the limit before changing configuration or runtimes. If that file becomes unparseable, `plan` reports it and continues without mutating anything, while `install` and `update` stop and name the file; repair or remove it, or pass `--structural-search=on` or `--structural-search=off` to rewrite it from the backed-up original. A configuration that is a link or not a bounded regular file remains a hard failure that no selection rewrites. The private runtime has an exact npm lockfile, script-free acquisition and binary/full-tree checksums; no global PATH entry is added. Doctor checks integrity and runs an offline fixture smoke. The initial native platforms are Windows x64, macOS x64/arm64 and Linux glibc x64; other combinations report unavailable.
|
|
35
|
+
|
|
36
|
+
Disable with `specpi update --structural-search=off`, then restart Pi. Intact owned runtimes are removed transactionally. Modified/unverified runtimes are preserved with a warning; replacement preserves them outside the active runtime path. The tree check covers file contents, paths, directories, permissions and link targets without traversing links, and is repeated before retired-tree deletion. Older ownership markers without a tree checksum are unverified and preserved. Rollback quarantines the new runtime and restores the prior directory before recursive cleanup; a cleanup failure reports the retained quarantine path. Uninstall preserves enablement configuration and user evidence. Trusted native executables still run with the user's permissions; these controls are not an OS sandbox.
|
|
37
|
+
|
|
38
|
+
Development verification uses `npm run setup:structural` followed by `npm run test:structural`. This installs only the locked native test runtime in `.specpi-test/structural-runtime/`. Installer tests use disposable Pi directories and skip external acquisition.
|
|
39
|
+
|
|
40
|
+
Six synthetic lookup trials on Windows x64 / Node 24.18.0 on September 9, 2026 covered call sites, constructors, filesystem calls, JSX/TSX components and Python calls. Each fixture contained two valid structures with different formatting plus one comment/string decoy. All 12 expected matches were found with no false positives; each response was 669–707 bytes and took 32–48 ms in that single run. A simple fixed-string `rg` query per fixture found six valid matches, missed six formatting variants and returned six decoys (37–85 bytes, 24–33 ms per query). This demonstrates the intended syntax-sensitive use case, not superiority over carefully constructed regex/manual inspection or measured agent productivity. Literal search remains faster and smaller in these trials; structural search remains opt-in.
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import type { Page } from "playwright";
|
|
6
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
7
|
+
import { Type } from "typebox";
|
|
8
|
+
import { sanitizeDiagnostic, sanitizeUrl } from "./diagnostics.ts";
|
|
9
|
+
|
|
10
|
+
export const ACCESSIBILITY_VERSION = "4.13.0";
|
|
11
|
+
export const MAX_ACCESSIBILITY_BYTES = 24 * 1024;
|
|
12
|
+
export class AccessibilityError extends Error {}
|
|
13
|
+
const tags = ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa", "wcag22aa"];
|
|
14
|
+
export const AccessibilityParams = Type.Object(
|
|
15
|
+
{
|
|
16
|
+
include: Type.Optional(Type.String({ minLength: 1, maxLength: 1000 })),
|
|
17
|
+
profile: Type.Optional(StringEnum(["wcag22aa", "best-practice"] as const)),
|
|
18
|
+
maxFindings: Type.Optional(Type.Integer({ minimum: 1, maximum: 100 })),
|
|
19
|
+
timeoutMs: Type.Optional(Type.Integer({ minimum: 1000, maximum: 30000 })),
|
|
20
|
+
},
|
|
21
|
+
{ additionalProperties: false },
|
|
22
|
+
);
|
|
23
|
+
type Finding = {
|
|
24
|
+
id: string;
|
|
25
|
+
impact?: string | null;
|
|
26
|
+
help: string;
|
|
27
|
+
helpUrl: string;
|
|
28
|
+
nodes: Array<{ target: unknown }>;
|
|
29
|
+
};
|
|
30
|
+
type Analysis = {
|
|
31
|
+
testEngine: { version: string };
|
|
32
|
+
violations: Finding[];
|
|
33
|
+
incomplete: Finding[];
|
|
34
|
+
passes: unknown[];
|
|
35
|
+
inapplicable: unknown[];
|
|
36
|
+
};
|
|
37
|
+
type Options = { include?: string; profile?: "wcag22aa" | "best-practice"; maxFindings?: number };
|
|
38
|
+
type Builder = { withTags(tags: string[]): Builder; include(selector: string): Builder; analyze(): Promise<Analysis> };
|
|
39
|
+
|
|
40
|
+
export function reduceAccessibility(raw: Analysis, maxFindings = 50) {
|
|
41
|
+
const severity = ["critical", "serious", "moderate", "minor", "unknown"];
|
|
42
|
+
// Rank an unrecognized impact as "unknown" so ordering matches the value that is reported.
|
|
43
|
+
const rank = (impact?: string | null) => {
|
|
44
|
+
const index = severity.indexOf(impact ?? "unknown");
|
|
45
|
+
|
|
46
|
+
return index === -1 ? severity.indexOf("unknown") : index;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const reduce = (findings: Finding[]) =>
|
|
50
|
+
[...findings]
|
|
51
|
+
.sort((a, b) => rank(a.impact) - rank(b.impact) || a.id.localeCompare(b.id))
|
|
52
|
+
.slice(0, maxFindings)
|
|
53
|
+
.map((finding) => ({
|
|
54
|
+
rule: sanitizeDiagnostic(finding.id, 100),
|
|
55
|
+
impact: severity.includes(finding.impact ?? "") ? finding.impact : "unknown",
|
|
56
|
+
guidance: sanitizeDiagnostic(finding.help, 300),
|
|
57
|
+
helpUrl: sanitizeUrl(finding.helpUrl),
|
|
58
|
+
affectedNodes: finding.nodes.length,
|
|
59
|
+
targets: finding.nodes.slice(0, 3).map((node) => sanitizeDiagnostic(JSON.stringify(node.target), 200)),
|
|
60
|
+
}));
|
|
61
|
+
const result = {
|
|
62
|
+
violations: reduce(raw.violations),
|
|
63
|
+
incomplete: reduce(raw.incomplete),
|
|
64
|
+
totals: {
|
|
65
|
+
violations: raw.violations.length,
|
|
66
|
+
incomplete: raw.incomplete.length,
|
|
67
|
+
passes: raw.passes.length,
|
|
68
|
+
inapplicable: raw.inapplicable.length,
|
|
69
|
+
},
|
|
70
|
+
truncated: false,
|
|
71
|
+
};
|
|
72
|
+
// Reserve space for page/state metadata. Keep both categories represented.
|
|
73
|
+
while (Buffer.byteLength(JSON.stringify(result)) > MAX_ACCESSIBILITY_BYTES - 4096) {
|
|
74
|
+
const list = result.violations.length >= result.incomplete.length ? result.violations : result.incomplete;
|
|
75
|
+
if (!list.length) {
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
list.pop();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
result.truncated =
|
|
83
|
+
result.violations.length < raw.violations.length || result.incomplete.length < raw.incomplete.length;
|
|
84
|
+
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function scanAccessibility(page: Page, runtimeDir: string, options: Options = {}) {
|
|
89
|
+
let BuilderClass: new (options: { page: Page }) => Builder;
|
|
90
|
+
try {
|
|
91
|
+
const require = createRequire(path.join(runtimeDir, "package.json"));
|
|
92
|
+
if (
|
|
93
|
+
require("axe-core/package.json").version !== ACCESSIBILITY_VERSION ||
|
|
94
|
+
JSON.parse(
|
|
95
|
+
fs.readFileSync(
|
|
96
|
+
path.join(runtimeDir, "node_modules", "@axe-core", "playwright", "package.json"),
|
|
97
|
+
"utf8",
|
|
98
|
+
),
|
|
99
|
+
).version !== ACCESSIBILITY_VERSION
|
|
100
|
+
) {
|
|
101
|
+
throw new Error("Unexpected scanner version");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
BuilderClass = require("@axe-core/playwright").default;
|
|
105
|
+
} catch {
|
|
106
|
+
throw new AccessibilityError(
|
|
107
|
+
"Accessibility scanner unavailable. Run specpi update without --skip-browser-install.",
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const scanId = crypto.randomUUID();
|
|
112
|
+
let navigated = false;
|
|
113
|
+
const onNavigation = () => {
|
|
114
|
+
navigated = true;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
page.on("framenavigated", onNavigation);
|
|
118
|
+
const startedAt = new Date().toISOString();
|
|
119
|
+
try {
|
|
120
|
+
if (options.include && (await page.locator(`css=${options.include}`).count()) !== 1) {
|
|
121
|
+
throw new AccessibilityError("Accessibility scope must match exactly one region.");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const effectiveTags = options.profile === "best-practice" ? [...tags, "best-practice"] : [...tags];
|
|
125
|
+
const builder = new BuilderClass({ page }).withTags(effectiveTags);
|
|
126
|
+
if (options.include) {
|
|
127
|
+
builder.include(options.include);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const result = await builder.analyze();
|
|
131
|
+
if (navigated || page.isClosed()) {
|
|
132
|
+
throw new AccessibilityError(
|
|
133
|
+
"Accessibility scan became stale during navigation. Scan the current state again.",
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
status: "complete",
|
|
139
|
+
scanId,
|
|
140
|
+
scannerVersion: result.testEngine.version,
|
|
141
|
+
url: sanitizeUrl(page.url()),
|
|
142
|
+
viewport: page.viewportSize(),
|
|
143
|
+
scope: options.include ? sanitizeDiagnostic(options.include, 500) : "page",
|
|
144
|
+
profile: options.profile ?? "wcag22aa",
|
|
145
|
+
tags: effectiveTags,
|
|
146
|
+
startedAt,
|
|
147
|
+
finishedAt: new Date().toISOString(),
|
|
148
|
+
...reduceAccessibility(result, options.maxFindings),
|
|
149
|
+
limitations:
|
|
150
|
+
"Automated checks of the observed DOM state only; dynamic changes, inaccessible frames and closed shadow roots may limit coverage. Incomplete checks need review. A clean result is not accessibility certification.",
|
|
151
|
+
};
|
|
152
|
+
} finally {
|
|
153
|
+
page.off("framenavigated", onNavigation);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -6,6 +6,7 @@ import type { Browser, BrowserContext, Page } from "playwright";
|
|
|
6
6
|
import { Check } from "typebox/value";
|
|
7
7
|
import { BrowserDiagnostics, DIAGNOSTIC_CATEGORIES } from "./diagnostics.ts";
|
|
8
8
|
import { BrowserCleanupError, settleBrowserCleanup } from "./lifecycle.ts";
|
|
9
|
+
import { AccessibilityError, AccessibilityParams, scanAccessibility } from "./accessibility.ts";
|
|
9
10
|
import {
|
|
10
11
|
PressParams,
|
|
11
12
|
SelectionParams,
|
|
@@ -417,9 +418,12 @@ export default function browserExtension(pi: ExtensionAPI) {
|
|
|
417
418
|
throw new Error("Invalid browser tool parameters.");
|
|
418
419
|
}
|
|
419
420
|
|
|
420
|
-
const bounded = [
|
|
421
|
-
|
|
422
|
-
|
|
421
|
+
const bounded = [
|
|
422
|
+
"browser_press",
|
|
423
|
+
"browser_select_option",
|
|
424
|
+
"browser_wait_for",
|
|
425
|
+
"browser_accessibility",
|
|
426
|
+
].includes(definition.name);
|
|
423
427
|
const controller = new AbortController();
|
|
424
428
|
const originalSignal = args[2];
|
|
425
429
|
const signal = AbortSignal.any([
|
|
@@ -430,7 +434,10 @@ export default function browserExtension(pi: ExtensionAPI) {
|
|
|
430
434
|
const timer = bounded
|
|
431
435
|
? setTimeout(
|
|
432
436
|
() => controller.abort(),
|
|
433
|
-
interactionTimeout(
|
|
437
|
+
interactionTimeout(
|
|
438
|
+
(args[1] as { timeoutMs?: number }).timeoutMs ??
|
|
439
|
+
(definition.name === "browser_accessibility" ? 15000 : undefined),
|
|
440
|
+
),
|
|
434
441
|
)
|
|
435
442
|
: undefined;
|
|
436
443
|
try {
|
|
@@ -506,6 +513,35 @@ export default function browserExtension(pi: ExtensionAPI) {
|
|
|
506
513
|
}
|
|
507
514
|
}
|
|
508
515
|
|
|
516
|
+
register({
|
|
517
|
+
name: "browser_accessibility",
|
|
518
|
+
label: "Browser Accessibility",
|
|
519
|
+
description:
|
|
520
|
+
"Check the current isolated browser page for automated accessibility violations and incomplete checks. Open and interact with the page first; this does not certify accessibility.",
|
|
521
|
+
parameters: AccessibilityParams,
|
|
522
|
+
async execute(_id, params, signal) {
|
|
523
|
+
if (!state.page || state.page.isClosed()) {
|
|
524
|
+
throw new Error("Open a page with browser_open before running accessibility checks.");
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
try {
|
|
528
|
+
const result = await cancellable(() => scanAccessibility(state.page!, runtimeDir, params), signal);
|
|
529
|
+
|
|
530
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }], details: {} };
|
|
531
|
+
} catch (error) {
|
|
532
|
+
if (error instanceof BrowserCleanupError || signal?.aborted) {
|
|
533
|
+
throw error;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (error instanceof AccessibilityError) {
|
|
537
|
+
throw error;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
throw new Error("Accessibility analysis failed. Check the page, scope and runtime, then retry.");
|
|
541
|
+
}
|
|
542
|
+
},
|
|
543
|
+
});
|
|
544
|
+
|
|
509
545
|
register({
|
|
510
546
|
name: "browser_open",
|
|
511
547
|
label: "Browser Open",
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
5
6
|
import { comparePngBuffers, loadBrowserRuntime } from "./core.mjs";
|
|
6
7
|
|
|
7
8
|
const runtimeDir = process.argv[2];
|
|
@@ -40,7 +41,34 @@ try {
|
|
|
40
41
|
throw new Error("Changed browser visual comparison was not detected.");
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
if (process.argv.includes("--accessibility")) {
|
|
45
|
+
const require = createRequire(path.join(path.resolve(runtimeDir), "package.json"));
|
|
46
|
+
const AxeBuilder = require("@axe-core/playwright").default;
|
|
47
|
+
if (require("axe-core/package.json").version !== "4.13.0") {
|
|
48
|
+
throw new Error("Accessibility runtime version mismatch.");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
await page.setContent(
|
|
52
|
+
'<!doctype html><html lang="en"><title>Fixture</title><main><button></button></main></html>',
|
|
53
|
+
);
|
|
54
|
+
const broken = await new AxeBuilder({ page }).withTags(["wcag2a"]).analyze();
|
|
55
|
+
if (!broken.violations.some((finding) => finding.id === "button-name")) {
|
|
56
|
+
throw new Error("Accessibility smoke missed an unnamed button.");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
await page.setContent(
|
|
60
|
+
'<!doctype html><html lang="en"><title>Fixture</title><main><button>Save</button></main></html>',
|
|
61
|
+
);
|
|
62
|
+
const fixed = await new AxeBuilder({ page }).withTags(["wcag2a"]).analyze();
|
|
63
|
+
if (fixed.violations.some((finding) => finding.id === "button-name")) {
|
|
64
|
+
throw new Error("Accessibility smoke did not recognize the repair.");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
43
68
|
console.log(`Browser smoke passed: ${baseline.length} byte PNG; exact and changed visual comparisons verified`);
|
|
69
|
+
if (process.argv.includes("--accessibility")) {
|
|
70
|
+
console.log("ACCESSIBILITY_SMOKE=passed");
|
|
71
|
+
}
|
|
44
72
|
} finally {
|
|
45
73
|
await browser?.close().catch(() => {});
|
|
46
74
|
await fs.rm(tempDir, { recursive: true, force: true });
|
|
@@ -185,7 +185,20 @@ export default function registerCommandGuard(
|
|
|
185
185
|
state.onModeChanged = () => pi.events?.emit("specpi:guard-policy-changed", { reason: "guard policy changed" });
|
|
186
186
|
let backgroundSubscription: (() => void) | undefined;
|
|
187
187
|
let guardStateSubscription: (() => void) | undefined;
|
|
188
|
+
let structuralSubscription: (() => void) | undefined;
|
|
188
189
|
const subscribeGuardState = () => {
|
|
190
|
+
structuralSubscription ??= pi.events?.on?.("specpi:structural-admission", (request: any) => {
|
|
191
|
+
request?.reply?.({
|
|
192
|
+
mode: state.mode,
|
|
193
|
+
generation: state.generation,
|
|
194
|
+
action:
|
|
195
|
+
!state.ready || state.startupFailed || state.mode === "locked"
|
|
196
|
+
? "deny"
|
|
197
|
+
: state.mode === "strict"
|
|
198
|
+
? "ask"
|
|
199
|
+
: "allow",
|
|
200
|
+
});
|
|
201
|
+
});
|
|
189
202
|
if (!backgroundSubscription) {
|
|
190
203
|
backgroundSubscription = pi.events?.on?.("specpi:background-admission", (request: any) => {
|
|
191
204
|
if (typeof request?.reply !== "function") {
|
|
@@ -240,6 +253,23 @@ export default function registerCommandGuard(
|
|
|
240
253
|
|
|
241
254
|
subscribeGuardState();
|
|
242
255
|
const backgroundSource = fileURLToPath(new URL("../background-tasks/index.ts", import.meta.url));
|
|
256
|
+
const structuralSource = fileURLToPath(new URL("../structural-search/index.ts", import.meta.url));
|
|
257
|
+
const ownsStructuralTool = (name: string): boolean => {
|
|
258
|
+
if (name !== "structural_search") {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const matches = pi.getAllTools?.().filter((tool) => tool.name === name) ?? [];
|
|
263
|
+
const source = matches.length === 1 ? matches[0].sourceInfo?.path : undefined;
|
|
264
|
+
if (typeof source !== "string" || !path.isAbsolute(source)) {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return process.platform === "win32"
|
|
269
|
+
? path.resolve(source).toLowerCase() === path.resolve(structuralSource).toLowerCase()
|
|
270
|
+
: path.resolve(source) === path.resolve(structuralSource);
|
|
271
|
+
};
|
|
272
|
+
|
|
243
273
|
const ownsBackgroundTool = (name: string): boolean => {
|
|
244
274
|
if (!["background_start", "background_list", "background_logs", "background_stop"].includes(name)) {
|
|
245
275
|
return false;
|
|
@@ -352,6 +382,8 @@ export default function registerCommandGuard(
|
|
|
352
382
|
reset();
|
|
353
383
|
backgroundSubscription?.();
|
|
354
384
|
backgroundSubscription = undefined;
|
|
385
|
+
structuralSubscription?.();
|
|
386
|
+
structuralSubscription = undefined;
|
|
355
387
|
if (typeof guardStateSubscription === "function") {
|
|
356
388
|
guardStateSubscription();
|
|
357
389
|
guardStateSubscription = undefined;
|
|
@@ -513,7 +545,7 @@ export default function registerCommandGuard(
|
|
|
513
545
|
// Check Pi's current registration provenance, not a tool name or cached handshake.
|
|
514
546
|
// Genuine background tools validate again in execute; cleanup remains usable under a lock.
|
|
515
547
|
// A missing or replaced registration retains normal Strict/locked enforcement.
|
|
516
|
-
if (ownsBackgroundTool(name)) {
|
|
548
|
+
if (ownsBackgroundTool(name) || ownsStructuralTool(name)) {
|
|
517
549
|
return;
|
|
518
550
|
}
|
|
519
551
|
|
|
@@ -457,6 +457,26 @@ export function createSnapshot(root, paths, options = {}) {
|
|
|
457
457
|
|
|
458
458
|
return Object.freeze({
|
|
459
459
|
sources,
|
|
460
|
+
// Parent-only parser seam. Worker brokers still expose only list/read/search.
|
|
461
|
+
// Read through the same bindings and require the originally captured digest.
|
|
462
|
+
async withSourceBytes(id, consume) {
|
|
463
|
+
ensureOpen();
|
|
464
|
+
const record = byId.get(id);
|
|
465
|
+
if (!record || typeof consume !== "function") {
|
|
466
|
+
fail("byte reader rejected");
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const bytes = readBoundFile(canonicalRoot, record.relative, record.original, MAX_BYTES);
|
|
470
|
+
try {
|
|
471
|
+
if (hash(bytes) !== record.digest) {
|
|
472
|
+
fail("source changed");
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return await consume(bytes);
|
|
476
|
+
} finally {
|
|
477
|
+
bytes.fill(0);
|
|
478
|
+
}
|
|
479
|
+
},
|
|
460
480
|
read(id, startLine = 1, maxLines = 200) {
|
|
461
481
|
ensureOpen();
|
|
462
482
|
const record = byId.get(id);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function integrationsFile(agentDir: string): string;
|
|
2
|
+
export declare function serializeIntegrations(value: unknown, file: string): string;
|
|
3
|
+
export declare function readIntegrations(agentDir: string): {
|
|
4
|
+
schema: 1;
|
|
5
|
+
structuralSearch: { enabled: boolean };
|
|
6
|
+
[key: string]: unknown;
|
|
7
|
+
};
|