specpi 0.34.0 → 0.35.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 +9 -0
- package/NPM_RELEASE.md +2 -2
- package/README.md +1 -1
- package/SECURITY_MODEL.md +8 -10
- package/THIRD_PARTY.md +8 -4
- package/extensions/jev-advisor/config.mjs +2 -2
- package/package.json +3 -2
- package/scripts/lancet-guard.mjs +35 -0
- package/scripts/packages.mjs +4 -0
- package/scripts/specpi.mjs +10 -19
- package/templates/settings.json +1 -1
- package/scripts/jev-guard.mjs +0 -154
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.35.0 - 2026-09-26
|
|
4
|
+
|
|
5
|
+
- Replace the Jev command guard with a local one. `specpi-lancet-guard` 0.1.0, new in `packages/lancet-guard`, takes the eighth pin from `specpi-jev-guard` 0.4.0. It scores shell commands with LANCET Nano v0.3.0, a 110M-parameter CodeT5-base classifier, on the CPU through ONNX Runtime: no API key, and commands never leave the machine. It keeps the Jev guard's local rules in front of the model. A risky verdict asks, Nano's unsure band asks, PowerShell and commands LANCET cannot read ask, and protected writes ask. A missing or damaged model fails closed.
|
|
6
|
+
- Ship it off. `/lancet-guard setup` downloads the model once as the release ZIP from the pinned LANCET-model GitHub release, which works where Hugging Face is blocked. The ZIP and each of the three files taken from it are refused unless their SHA-256 matches. `/lancet-guard on` switches the guard on for a session and `--global` saves that. SpecPi no longer writes any guard configuration, so a saved choice survives updates. An update retires the old Jev guard pin, and says so if that guard was switched on.
|
|
7
|
+
- Gate background jobs with the guard instead of refusing them while it is on: it checks the `background` tool's command as it checks `bash`.
|
|
8
|
+
- Match LANCET Nano's Python runtime exactly. A Node port of its tokenizer and scoring gave identical token ids and bands on 7,170 commands, with scores within 2.2e-16 on the same machine (ONNX Runtime's INT8 kernels move scores by up to 0.03 on some other CPUs, with the same bands on every reference command), at about 23 ms a command and 275 MB once loaded.
|
|
9
|
+
- Skip onnxruntime-node's CUDA download during package installation (`ONNXRUNTIME_NODE_INSTALL=skip`). The guard never uses the GPU.
|
|
10
|
+
- Pair this release with SpecPi Chat 0.18.0, whose footer counter reads the LANCET guard's `lancet-guard` status key in place of the retired Jev guard's.
|
|
11
|
+
|
|
3
12
|
## 0.34.0 - 2026-09-24
|
|
4
13
|
|
|
5
14
|
- Steer long waits into background jobs instead of relying on the agent to remember. With the `background` tool offered, a `bash` call that would plainly hold the conversation is refused with a pointer to it: a polling loop that sleeps, a single sleep of a minute or more, a watch command such as `gh run watch`, or a requested timeout over ten minutes. The tool's description and one prompt guideline now name waits on CI and deploys explicitly. Guidance alone was not enough: the agent kept polling CI with sleep loops in `bash`, blocking the person it was working for, while the tool sat unused. Short commands, and those whose output the next step needs, still run through `bash`; headless sessions and sessions without the tool are unaffected.
|
package/NPM_RELEASE.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# npm release
|
|
2
2
|
|
|
3
|
-
This guide covers the root `specpi` package. Each package under `packages/` releases independently, with its own release process, tag prefix and publish workflow: [Browser QA](packages/browser-qa/NPM_RELEASE.md) (`browser-qa-v*`), [Delegation](packages/delegation/NPM_RELEASE.md) (`delegation-v*`)
|
|
3
|
+
This guide covers the root `specpi` package. Each package under `packages/` releases independently, with its own release process, tag prefix and publish workflow: [Browser QA](packages/browser-qa/NPM_RELEASE.md) (`browser-qa-v*`), [Delegation](packages/delegation/NPM_RELEASE.md) (`delegation-v*`), [Experiments](packages/experiments/NPM_RELEASE.md) (`experiments-v*`) and [LANCET guard](packages/lancet-guard/NPM_RELEASE.md) (`lancet-guard-v*`). Package-only releases do not invoke the root publisher.
|
|
4
4
|
|
|
5
5
|
Publishing, tags, deprecation, ownership changes and GitHub Releases require explicit human approval. Publication is a post-merge operation: never publish from an unmerged commit. npm versions are immutable.
|
|
6
6
|
|
|
7
7
|
## Prepare
|
|
8
8
|
|
|
9
|
-
1. This candidate is SpecPi **0.
|
|
9
|
+
1. This candidate is SpecPi **0.35.0**, paired with the Chat **0.18.0** VSIX. Browser QA **0.3.0** and LANCET guard **0.1.0** are already independently published; do not republish or edit their source as part of this release. Choose an unused version; update `package.json`, the dated `CHANGELOG.md` entry, README, and the website. Check Chat's version and VSIX download links. Preserve historical changelog entries.
|
|
10
10
|
2. Validate:
|
|
11
11
|
|
|
12
12
|
```sh
|
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
|
|
29
29
|
---
|
|
30
30
|
|
|
31
|
-
SpecPi 0.
|
|
31
|
+
SpecPi 0.35.0 is a small starting point for the [Pi coding agent](https://pi.dev/). It is one opinionated setup for how the agent should work, not a marketplace of plugins.
|
|
32
32
|
|
|
33
33
|
At the center are two built-in extensions. **Scope control** keeps each task to the files it said it would touch. The **improvement loop** turns repeated friction into small, tested changes to the setup, instead of letting prompts and workarounds pile up. Around those are eight hand-picked packages, each locked to an exact version and checked before anything installs, plus **SpecPi Chat**, a VS Code panel for working alongside the agent.
|
|
34
34
|
|
package/SECURITY_MODEL.md
CHANGED
|
@@ -19,7 +19,7 @@ Optional tool groups ship withdrawn. The `request_capability` tool lets the mode
|
|
|
19
19
|
The `background` tool starts a shell command and returns at once; the job reports its exit code and last lines of output as a follow-up message when it ends. It runs through Pi's own local shell runner with the session's `shellPath` and `shellCommandPrefix`, so the shell, working directory and process-tree cleanup on Pi's exit match the `bash` tool. It adds no new authority, and every start fails closed:
|
|
20
20
|
|
|
21
21
|
- **Permission parity.** The installer records `shellTools.background = { commandArgument: "command" }` in the permission system's global config, so its full `bash` stack — command decomposition, wrapper flooring, path and outside-directory checks, and every `bash:` rule — applies to the command. The entry is merged into the existing file, backed up and rolled back with the rest of the transaction, left alone when the file carries comments, and removed at uninstall only if it is still SpecPi's. Before every start the extension reads the effective mapping (global, then a trusted project's, which could remap it) and refuses unless it gates `command`. An unreadable config refuses too.
|
|
22
|
-
- **Command guard.** `specpi-jev-guard`
|
|
22
|
+
- **Command guard.** `specpi-lancet-guard` gates the `background` tool's command exactly as it gates `bash`, so background jobs run under it with no extra condition. The retired `specpi-jev-guard` checked `bash`, `powershell`, `write` and `edit` only, so while a `jev-guard` command is present -- someone who still installs that package themselves -- background jobs stay off unless its saved state resolves off, read the way it reads it: its default on, the global file, then a trusted project's. Its session-only switch lives in its own memory and cannot be read, so save that choice with `--global`.
|
|
23
23
|
- **Interactive only.** Headless sessions are not offered the tool and are refused if they call it; there is nobody to report back to. A Pi that cannot list its commands is refused, because the guard and permission system cannot be detected.
|
|
24
24
|
- **Bounded.** At most four jobs run at once. Each log is capped at 8 MiB under `<agent-dir>/specpi/background/`, owner-only, and deleted when the session ends; directories left by a crashed session are pruned after a day. A completion message carries at most 40 lines and 4,000 characters of output with control characters removed. Output is untrusted text, like any tool result.
|
|
25
25
|
- **Session-owned.** `/jobs stop` and session end kill each job's process tree; Pi's own exit handling also kills tracked children. Process cleanup is best effort: a program that detaches itself from its parent, or a daemon a job starts, can outlive it, exactly as with `bash`.
|
|
@@ -50,7 +50,7 @@ One of the five systems can add fixed advice to model context, and it is off by
|
|
|
50
50
|
|
|
51
51
|
**Progress detection, formerly system 5, has been withdrawn.** It was the only system that could steer the model mid-session, and replayed over recorded runs its stuck verdict did not predict failure. Nothing in the layer now appends a message to the conversation. Schema 6 settings drop its switch, budget and `progressNudge` mode.
|
|
52
52
|
|
|
53
|
-
The command guard is **not** part of this layer. It is a separate pinned package with its own gate, its own configuration file, its own commands and a fail-closed posture, and it has its own section below. It is named here because a reader who took "the advisor holds no authority" to cover everything SpecPi installs would be wrong about exactly one component.
|
|
53
|
+
The command guard is **not** part of this layer. It is a separate pinned package, `specpi-lancet-guard`, with its own gate, its own configuration file, its own commands and a fail-closed posture, and it has its own section below. It is named here because a reader who took "the advisor holds no authority" to cover everything SpecPi installs would be wrong about exactly one component. It sends nothing anywhere: it scores commands locally. `/jev` does not mention it and the advisor imports nothing from it.
|
|
54
54
|
|
|
55
55
|
**System 7, untrusted-content classification**, prepends a fixed warning line to externally fetched content that confidently reads as instructions addressed to an agent. It applies to web and browser tool results and to shell commands whose output is a fetched document -- `curl`, `wget`, `gh api`, HTTPie and PowerShell's web cmdlets -- and never to other shell output or to file reads. That shell exception is decided by the command, locally, before anything is sent; the result is then sampled and redacted like any other. It is defence in depth and explicitly not a control: it never blocks, it has no authority over what the model then does, and it should not be relied on to stop prompt injection. Its value is that untrusted content in a fetched page is currently owned by nothing at all.
|
|
56
56
|
|
|
@@ -58,19 +58,17 @@ An earlier version of this document said its false-positive rate was measurable
|
|
|
58
58
|
|
|
59
59
|
Settings live in `<agent-dir>/specpi/jev/settings.json`, written atomically with owner-only permissions. Links, irregular files, oversize files, an unrecognised schema and unparseable contents all read as off. The one exception is the layer's own previous schemas: an older file is migrated forward rather than read as unrecognised, because "collapse to all-off" is a rule for corrupt input and applying it to our own earlier version would silently disable a layer the user had switched on. Keys for withdrawn systems -- compaction guidance in schema 5, progress detection in schema 6 -- are dropped rather than migrated, and two more keys are dropped for a different reason. A `schema: 2` file kept a `guard` pair and a `schema: 3` file kept `systems.guard`; schema 4 keeps neither, because whether the command guard runs is not this file's answer to hold. The package keeps its own switch in its own configuration, and a stale copy here could only ever disagree with it. Call budgets are per system under a session total, so one busy system cannot exhaust the allowance of the others and leave them dead for the rest of the session with event ordering deciding which one won. Turning the layer on or off writes that preference, because a switch that forgets is not a switch; `--session` is how a one-off change is kept out of the file. The key is resolved the way Pi resolves every provider credential, in Pi's own documented order: the `openrouter` entry that `/login openrouter` writes to `<agent-dir>/auth.json`, then the environment variable. That is a deliberate, narrow exception to the rule that Pi state is never read, and it is bounded to exactly one question — the one `api_key` entry for the one provider this layer calls. Be precise about when: the key's *value* is read only on a call path the master switch already gates, but whether an entry *exists* is checked whenever status is displayed, including with the layer off, because that is the answer someone needs in order to configure it in the first place. A presence check reports a source name and nothing else. An `oauth` entry is ignored rather than unwrapped, because Pi refreshes those under its own lock and a second reader would race a rotation. The value is returned by a single function straight into a request header; nothing else in the layer receives it, and status output, the ledger and the Chat panel carry source names only. Pi trust decisions, sessions, missions and history are never read. `JEV_KEY_SOURCE=environment` restricts resolution to the environment, which is what this repository's own eval scripts set so a measured run cannot silently bill a developer's personal account.
|
|
60
60
|
|
|
61
|
-
## Command policy and the
|
|
61
|
+
## Command policy and the LANCET guard
|
|
62
62
|
|
|
63
|
-
`specpi-
|
|
63
|
+
`specpi-lancet-guard` is pinned in the base set and ships **off**. SpecPi writes nothing into its configuration: the package's own default is off, and only its own `/lancet-guard on` (this session) or `/lancet-guard on --global` (saved in `<home>/.pi/lancet-guard.json`) turns it on. An installer run never arms or disarms it, so a saved choice survives every update. `@gotgenes/pi-permission-system` stays pinned and decides every tool call whether the guard is on or off; the guard is a second, independent check: either one can block a call, so the guard never allows what the permission system would refuse.
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
When on, local rules copied from `specpi-jev-guard` decide first -- hard-deny patterns block, provably read-only commands and the user's allow globs pass -- and LANCET Nano v0.3.0, a 110M-parameter CodeT5-base classifier, scores the rest of `bash`, `powershell` and `background` commands in-process on the CPU through ONNX Runtime. `risky` asks (or blocks, if configured), `review` -- the band Nano is unsure about -- asks, `not_flagged` runs, and what LANCET cannot read (PowerShell, over-long input) asks. Writes and edits to protected or out-of-workspace paths ask without a model call. With no UI, an ask becomes a block unless `uncertain` is `allow`. A missing, damaged or unloadable model blocks what the rules leave open: switching the guard on accepts that, and `/lancet-guard on` is refused until the model is installed. `not_flagged` is not a safety claim; on LANCET's own 793-command release benchmark the classifier alone caught 85.8% of risky commands and 66% of risky secrets commands, against 96.8% and 98% for Jev.
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
Commands never leave the machine and are never executed by the guard. The only network access is `/lancet-guard setup`, run by a human: it downloads one ZIP from the pinned `TannerMidd/LANCET-model` GitHub release into a private staging directory, capped at its pinned size, and refuses it unless its SHA-256 matches the digest compiled into the package. Only then does the package's own small ZIP reader extract the three model files, each inflated to its pinned size and refused unless its own SHA-256 matches. GitHub is the only host involved, so it works where Hugging Face is blocked. The digests are checked again on every load. The package's one production dependency, `onnxruntime-node` 1.30.0, is pinned exactly; its install script would fetch CUDA libraries on Linux x64, so the installer sets `ONNXRUNTIME_NODE_INSTALL=skip`.
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
The guard publishes a per-session counter as a Pi status item under the key `lancet-guard`: the number of commands LANCET scored, the number blocked once there are any, and the last verdict. SpecPi Chat renders that line in its footer and does nothing else with it. The key is absent whenever the guard is off or its `auditDisplay` is. Every decision is also written to Pi's own session file by the package.
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
0.4.0 publishes a per-session counter as a Pi status item under the key `jev-guard` — the number of classifier calls, the number of blocked calls once there are any, and the last verdict. SpecPi Chat renders that line in its session footer and does nothing else with it: it does not recount from the session records, reformat the text, or offer a control. The key is absent whenever the guard is off or its `auditDisplay` is, so the readout appears exactly when the guard is gating. Every judged call is also written to Pi's own session file by the package, which is the durable audit trail and is not SpecPi's to manage.
|
|
71
|
+
`specpi-jev-guard`, which this replaced, is no longer pinned. An update retires its package entry like any other SpecPi-added pin and leaves its downloaded bytes and `jev-guard.json` in place. When that file had it switched on, the update says so, because retiring a gate someone had armed is not a change to make quietly. The LANCET guard does not inherit that setting; it starts off.
|
|
74
72
|
|
|
75
73
|
## Improvement authority and evidence
|
|
76
74
|
|
package/THIRD_PARTY.md
CHANGED
|
@@ -11,7 +11,7 @@ Development formatting uses Prettier **3.9.6** (MIT), ESLint **10.9.1** (MIT), `
|
|
|
11
11
|
|
|
12
12
|
## Default packages
|
|
13
13
|
|
|
14
|
-
Reviewed on 2026-09-16 against published npm metadata and integrity-verified source archives, and `specpi-
|
|
14
|
+
Reviewed on 2026-09-16 against published npm metadata and integrity-verified source archives, and `specpi-lancet-guard` 0.1.0, which replaces `specpi-jev-guard` 0.4.0, from source in this repository. All eight top-level packages declare the MIT license. Pins are authoritative in `templates/settings.json`.
|
|
15
15
|
|
|
16
16
|
SpecPi requests exact npm dependency saves for these pins and checks installed top-level versions before completing installation. The override applies to package acquisition without changing the user's global npm configuration.
|
|
17
17
|
|
|
@@ -24,22 +24,26 @@ SpecPi requests exact npm dependency saves for these pins and checks installed t
|
|
|
24
24
|
| pi-goal-x | 0.31.2 | [tmonk/pi-goal-x](https://github.com/tmonk/pi-goal-x) |
|
|
25
25
|
| @sreetej510/pi-usage | 0.10.0 | [Sreetej510/pi-extensions](https://github.com/Sreetej510/pi-extensions) |
|
|
26
26
|
| @gotgenes/pi-permission-system | 32.0.2 | [gotgenes/pi-packages](https://github.com/gotgenes/pi-packages) |
|
|
27
|
-
| specpi-
|
|
27
|
+
| specpi-lancet-guard | 0.1.0 | [TannerMidd/SpecPi](https://github.com/TannerMidd/SpecPi/tree/main/packages/lancet-guard) |
|
|
28
28
|
|
|
29
29
|
Transitive dependencies and their notices remain in Pi's npm installation tree. Top-level pins do not freeze upstream dependency ranges or constitute a full transitive security audit. Pi invokes npm with its upstream package-management semantics, including dependency lifecycle scripts. Browser QA has no install hook: confirmed SpecPi install/update explicitly invokes its installed Node setup bin, unless acquisition or browser setup is skipped. It downloads package-pinned Chromium without installing OS libraries. BetterWright is optional/manual and retains its own Bun-based setup; SpecPi neither removes Bun nor deletes user-owned tools. Usage reporting and web packages make their own provider/service connections. Consult upstream licenses and security policies before redistributing their components.
|
|
30
30
|
|
|
31
31
|
Pi Lens is no longer a default package. Normal updates retire only unchanged entries added by SpecPi; independent or modified entries and downloaded bytes remain, with their upstream notices. Restart Pi to unload Lens.
|
|
32
32
|
|
|
33
|
-
The combined base is tested with Pi 0.84.4. Pi Goal X declares Pi `>=0.83.0 <0.85.0`; compatibility with newer hosts is not assumed. SpecPi's former custom browser, structural-search, background-task, and native command-guard implementations, DonSeTch, and Pi themes have been removed; command gating is the pinned `specpi-
|
|
33
|
+
The combined base is tested with Pi 0.84.4. Pi Goal X declares Pi `>=0.83.0 <0.85.0`; compatibility with newer hosts is not assumed. SpecPi's former custom browser, structural-search, background-task, and native command-guard implementations, DonSeTch, and Pi themes have been removed; command gating is the pinned `specpi-lancet-guard` package's. Its delegation and experiment implementations were not discarded: they now ship as the independent `specpi-delegation` and `specpi-experiments` packages described below. Removal restores owned package settings but does not delete downloaded upstream packages or tools. Retired private runtimes remain in local backups with their notices.
|
|
34
34
|
|
|
35
35
|
## Network services
|
|
36
36
|
|
|
37
|
-
The optional Jev advisor
|
|
37
|
+
The optional Jev advisor reaches TypeSafe's System One endpoint. It is not a bundled dependency: it is first-party source that uses Node's built-in `fetch` and declares no package. It resolves its key the way every other Pi provider credential is resolved, in Pi's own documented order: the `openrouter` entry that `/login openrouter` writes to Pi's `auth.json`, then `OPENROUTER_API_KEY` from the environment (`TYPESAFE_API_KEY` when `JEV_BACKEND=typesafe` selects the direct API). The two keys are not interchangeable: the other service rejects the wrong one with a bare 401. SpecPi never provisions or stores a provider credential, and reads one only to place it in the request header of the call the user has switched on.
|
|
38
38
|
|
|
39
39
|
Requests go to `openrouter.ai` by default, or to `api.typesafe.ai` on the direct backend, unless `TYPESAFE_BASE_URL` redirects them, which exists so tests never reach the network and the eval proxy can price advisor traffic. Retention and processing of what is sent are governed by TypeSafe and, on the default route, by OpenRouter as well -- not by SpecPi; the advisor ships off, asks before its first transmission and records a hash of every payload locally. The frozen eval price list carries `jev-1.13.0` at $0.042/MTok input and free output so a measured SpecPi + Jev row never reports a cost that excludes its own advisor.
|
|
40
40
|
|
|
41
41
|
Worth stating plainly, because it is the part a reader is most likely to assume otherwise: what leaves the machine is a bounded sample of the material being judged, not only counts about it. A tool result contributes up to twelve short lines of its own text, redacted and inside a 1 KB budget, because whether a result is spent or whether a fetched page is addressing the agent cannot be decided from byte counts. When the untrusted-content system is enabled, that sample is drawn from externally fetched pages and browser snapshots as well as from local output. [`SECURITY_MODEL.md`](SECURITY_MODEL.md#jev-advisor) states the exact bound and where it is enforced.
|
|
42
42
|
|
|
43
|
+
## LANCET command guard
|
|
44
|
+
|
|
45
|
+
`packages/lancet-guard` is first-party source, released independently as `specpi-lancet-guard`. Its one production dependency is `onnxruntime-node` 1.30.0 (MIT, Microsoft), pinned exactly, which bundles prebuilt ONNX Runtime CPU libraries; SpecPi's installer sets `ONNXRUNTIME_NODE_INSTALL=skip` so its install script does not fetch the CUDA libraries it would otherwise download on Linux x64. Its local rules are copied under MIT from `specpi-jev-guard` 0.4.0. The model is not bundled: `/lancet-guard setup` downloads LANCET Nano v0.3.0 (Apache-2.0) from the `TannerMidd/LANCET-model` GitHub release, pinned by SHA-256. It was fine-tuned from Salesforce CodeT5-base (Apache-2.0); its model card and notices in the release record the training-data sources. The guard makes no other network connection. See its [dependency notices](packages/lancet-guard/THIRD_PARTY.md) and [security boundary](packages/lancet-guard/SECURITY.md).
|
|
46
|
+
|
|
43
47
|
## Standalone delegation and experiments
|
|
44
48
|
|
|
45
49
|
`packages/delegation` and `packages/experiments` are first-party packages extracted from SpecPi's own retired harness code under MIT. Neither bundles third-party runtime code or declares a production dependency; every runtime import is a Node builtin or a Pi-supplied optional peer. Experiments invokes the user's own `git` as an external program through Pi's `exec` seam; Git is not bundled, vendored or version-pinned. Delegation's extraction drops the Command Guard admission path, which it does not integrate with, and reports its guard posture as `absent`. See their dependency notices and security boundaries: [delegation](packages/delegation/THIRD_PARTY.md) / [boundary](packages/delegation/SECURITY.md), [experiments](packages/experiments/THIRD_PARTY.md) / [boundary](packages/experiments/SECURITY.md).
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
// anything -- see key-source.mjs.
|
|
7
7
|
//
|
|
8
8
|
// The command guard is not here at all. It is a separate pinned package that keeps its own
|
|
9
|
-
// configuration and its own switch, and nothing in this file tracks it
|
|
10
|
-
//
|
|
9
|
+
// configuration and its own switch, and nothing in this file tracks it. It is specpi-lancet-guard,
|
|
10
|
+
// which scores commands locally and does not use Jev.
|
|
11
11
|
//
|
|
12
12
|
// The file is SpecPi's own, hardened the same way as web-access and capability-policy: atomic
|
|
13
13
|
// write, mode 0600, symlinks refused, and a missing or unreadable file read as off.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "specpi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.35.0",
|
|
4
4
|
"description": "Scope control and a human-selected harness improvement loop for Pi",
|
|
5
5
|
"author": "Tanner Middleton",
|
|
6
6
|
"repository": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"scripts/lib.mjs",
|
|
29
29
|
"scripts/lock.mjs",
|
|
30
30
|
"scripts/packages.mjs",
|
|
31
|
-
|
|
31
|
+
"scripts/lancet-guard.mjs",
|
|
32
32
|
"scripts/permission-shell-tools.mjs",
|
|
33
33
|
"templates",
|
|
34
34
|
"extensions",
|
|
@@ -88,6 +88,7 @@
|
|
|
88
88
|
"check:browser-qa": "npm --prefix packages/browser-qa run check",
|
|
89
89
|
"check:delegation": "npm --prefix packages/delegation run check",
|
|
90
90
|
"check:experiments": "npm --prefix packages/experiments run check",
|
|
91
|
+
"check:lancet-guard": "npm --prefix packages/lancet-guard run check",
|
|
91
92
|
"check:vscode": "npm --prefix vscode run check",
|
|
92
93
|
"prepublishOnly": "npm run check",
|
|
93
94
|
"check": "npm run format:check && npm run check:syntax && npm test && npm run check:package && npm run check:vscode"
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Install-time notes for specpi-lancet-guard, the local command guard in the base set.
|
|
2
|
+
//
|
|
3
|
+
// SpecPi writes nothing into the guard's configuration. The package ships off, downloads its model
|
|
4
|
+
// only when someone runs `/lancet-guard setup`, and owns its own switch: `/lancet-guard on` for a
|
|
5
|
+
// session, `--global` to save it. An installer run therefore never arms or disarms it, and a
|
|
6
|
+
// saved preference survives every update.
|
|
7
|
+
//
|
|
8
|
+
// It replaced specpi-jev-guard, which SpecPi used to pin and write inert. An update retires that
|
|
9
|
+
// pin like any other SpecPi-added package entry. The one thing worth saying out loud is when that
|
|
10
|
+
// takes away a gate someone had switched on, so this reads the old package's saved switch -- only
|
|
11
|
+
// that field -- to decide whether to say it.
|
|
12
|
+
|
|
13
|
+
import fs from "node:fs";
|
|
14
|
+
import os from "node:os";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
|
|
17
|
+
/** Must match the pin in templates/settings.json. */
|
|
18
|
+
export const GUARD_PIN = "npm:specpi-lancet-guard@0.1.0";
|
|
19
|
+
|
|
20
|
+
export const RETIRED_GUARD_PIN = "npm:specpi-jev-guard@0.4.0";
|
|
21
|
+
|
|
22
|
+
/** Whether the retired Jev guard's saved global setting had it switched on. */
|
|
23
|
+
export function retiredGuardWasOn(home = os.homedir()) {
|
|
24
|
+
try {
|
|
25
|
+
const file = path.join(home, ".pi", "jev-guard.json");
|
|
26
|
+
const parsed = JSON.parse(fs.readFileSync(file, "utf8").replace(/^\uFEFF/u, ""));
|
|
27
|
+
|
|
28
|
+
return parsed?.enabled === true;
|
|
29
|
+
} catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const RETIRED_GUARD_NOTICE =
|
|
35
|
+
"The Jev command guard was on and is no longer in SpecPi's default packages. Its replacement, the local LANCET guard, starts off: run /lancet-guard setup in Pi to download its model and turn it on.";
|
package/scripts/packages.mjs
CHANGED
|
@@ -94,6 +94,10 @@ export function installBasePackages(agentDir) {
|
|
|
94
94
|
PI_CODING_AGENT_DIR: agentDir,
|
|
95
95
|
npm_config_save_exact: "true",
|
|
96
96
|
NPM_CONFIG_SAVE_EXACT: "true",
|
|
97
|
+
// onnxruntime-node (the LANCET guard's runtime) bundles its CPU binaries, but on
|
|
98
|
+
// Linux x64 its install script would also fetch CUDA libraries from NuGet. The guard
|
|
99
|
+
// is CPU-only, so skip that download.
|
|
100
|
+
ONNXRUNTIME_NODE_INSTALL: "skip",
|
|
97
101
|
},
|
|
98
102
|
stdio: "inherit",
|
|
99
103
|
windowsHide: true,
|
package/scripts/specpi.mjs
CHANGED
|
@@ -23,7 +23,7 @@ import { validateCapabilityRegistry } from "../extensions/tool-wishlist/registry
|
|
|
23
23
|
import { runValidator } from "../extensions/tool-wishlist/validators.mjs";
|
|
24
24
|
import { acquireSpecPiLock } from "./lock.mjs";
|
|
25
25
|
import { basePackages, checkBasePackages, installBasePackages, packageChanges, runBrowserQA } from "./packages.mjs";
|
|
26
|
-
import {
|
|
26
|
+
import { RETIRED_GUARD_NOTICE, RETIRED_GUARD_PIN, retiredGuardWasOn } from "./lancet-guard.mjs";
|
|
27
27
|
import {
|
|
28
28
|
applyShellToolMapping,
|
|
29
29
|
manualInstruction as shellToolInstruction,
|
|
@@ -366,24 +366,15 @@ async function mutate(options, operation) {
|
|
|
366
366
|
runBrowserQA(agentDir, "setup");
|
|
367
367
|
}
|
|
368
368
|
|
|
369
|
-
//
|
|
370
|
-
//
|
|
371
|
-
//
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
// The one case where establishing the default takes something away. An
|
|
379
|
-
// installer run that silently switched off a gate the user had switched on is
|
|
380
|
-
// exactly the kind of quiet security change this file refuses to make.
|
|
381
|
-
warnings.push(
|
|
382
|
-
`Switched the Jev command guard off in ${guardConfigPath()}. SpecPi installs it inert and asserts that on every install and update; run /jev-guard on --global in Pi to turn it back on.`,
|
|
383
|
-
);
|
|
384
|
-
}
|
|
385
|
-
} catch (error) {
|
|
386
|
-
console.log(`SpecPi: could not write the Jev guard's inert settings: ${error.message}`);
|
|
369
|
+
// The LANCET guard ships off and owns its own switch, so SpecPi writes nothing into its
|
|
370
|
+
// configuration. Retiring the Jev guard it replaced is the one change here that can take
|
|
371
|
+
// a gate away from someone, so that case is said out loud rather than done quietly.
|
|
372
|
+
if (
|
|
373
|
+
previous?.basePackages?.includes(RETIRED_GUARD_PIN) &&
|
|
374
|
+
!basePackages.includes(RETIRED_GUARD_PIN) &&
|
|
375
|
+
retiredGuardWasOn()
|
|
376
|
+
) {
|
|
377
|
+
warnings.push(RETIRED_GUARD_NOTICE);
|
|
387
378
|
}
|
|
388
379
|
|
|
389
380
|
packageState = {
|
package/templates/settings.json
CHANGED
package/scripts/jev-guard.mjs
DELETED
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
// Install-time seam for specpi-jev-guard, the Jev-scored command gate.
|
|
2
|
-
//
|
|
3
|
-
// The guard is pinned in the base set but ships **inert**. Its own `DEFAULT_SETTINGS.enabled` is
|
|
4
|
-
// `true`, so leaving it alone would mean a fresh SpecPi install started gating shell and file calls
|
|
5
|
-
// through a third-party service on day one. SpecPi writes `enabled: false` instead.
|
|
6
|
-
//
|
|
7
|
-
// That is the whole of SpecPi's involvement, and the file lives under `scripts/` to say so. The
|
|
8
|
-
// guard is its own thing: it ships its own `/jev-guard setup | on | off [--global] | check | model
|
|
9
|
-
// | backend`, keeps its own configuration, resolves its own key, and is not part of the Jev layer.
|
|
10
|
-
// The advisor imports nothing from here, `/jev` does not mention it, and no switch in SpecPi turns
|
|
11
|
-
// it on. One command does -- the package's own.
|
|
12
|
-
//
|
|
13
|
-
// Be precise about what this cannot do. The guard is fail-closed by design: with no key, an
|
|
14
|
-
// unreachable endpoint, or an answer it cannot parse, the call does not go through, and in a
|
|
15
|
-
// session with no UI its `uncertain` default blocks the middle band too. There is no setting that
|
|
16
|
-
// hands the decision back to @gotgenes/pi-permission-system instead. So the honest posture is:
|
|
17
|
-
//
|
|
18
|
-
// - off (what SpecPi installs) -> the guard is not in the tool path at all, and the permission
|
|
19
|
-
// system decides every call exactly as it did before this package existed;
|
|
20
|
-
// - on -> the guard decides first, asks a human in the middle band when there is a UI, and
|
|
21
|
-
// blocks when it cannot reach Jev. An outage stops gated work until it is switched off.
|
|
22
|
-
//
|
|
23
|
-
// That trade is the user's to make, which is why SpecPi only ever writes the off side of it.
|
|
24
|
-
//
|
|
25
|
-
// ONE KEY, and not one SpecPi supplies. Since 0.3.0 the package reads Pi's saved login first and
|
|
26
|
-
// the environment second -- the same order the Jev advisor uses for the same provider entry -- so
|
|
27
|
-
// `/login openrouter` serves both without either knowing about the other.
|
|
28
|
-
//
|
|
29
|
-
// `auditDisplay` is deliberately not asserted below, although SpecPi Chat renders the counter that
|
|
30
|
-
// setting controls. It defaults to `status`, which is what publishes the line, and it is the user's
|
|
31
|
-
// to change: pinning it here would take a display preference away from them to guarantee a readout
|
|
32
|
-
// in one frontend. A session with it set to `off` simply shows no counter, which is what they
|
|
33
|
-
// asked for.
|
|
34
|
-
|
|
35
|
-
import fs from "node:fs";
|
|
36
|
-
import os from "node:os";
|
|
37
|
-
import path from "node:path";
|
|
38
|
-
import { agentDirectory, regularFile, writeFileAtomic } from "../extensions/jev-advisor/config.mjs";
|
|
39
|
-
|
|
40
|
-
export const GUARD_PACKAGE = "specpi-jev-guard";
|
|
41
|
-
|
|
42
|
-
/** Must match the pin in templates/settings.json. */
|
|
43
|
-
export const GUARD_PIN = "npm:specpi-jev-guard@0.4.0";
|
|
44
|
-
|
|
45
|
-
// The guard reads `<homedir>/.pi/jev-guard.json` globally, and a project copy under `<cwd>/.pi/`
|
|
46
|
-
// when the project is trusted. SpecPi writes only the global file: a project-local override is the
|
|
47
|
-
// user's to make, and writing one would put a security setting inside whatever repository happened
|
|
48
|
-
// to be open at the time.
|
|
49
|
-
const CONFIG_DIR_NAME = ".pi";
|
|
50
|
-
const SETTINGS_FILE = "jev-guard.json";
|
|
51
|
-
|
|
52
|
-
function guardRoot() {
|
|
53
|
-
return path.join(agentDirectory(), "npm", "node_modules", GUARD_PACKAGE);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function guardConfigFile() {
|
|
57
|
-
return path.join(os.homedir(), CONFIG_DIR_NAME, SETTINGS_FILE);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export function installed() {
|
|
61
|
-
try {
|
|
62
|
-
const manifest = path.join(guardRoot(), "package.json");
|
|
63
|
-
if (!fs.existsSync(manifest)) {
|
|
64
|
-
return { installed: false };
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const parsed = JSON.parse(fs.readFileSync(manifest, "utf8"));
|
|
68
|
-
if (parsed?.name !== GUARD_PACKAGE) {
|
|
69
|
-
return { installed: false };
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return { installed: true, version: typeof parsed.version === "string" ? parsed.version : "unknown" };
|
|
73
|
-
} catch {
|
|
74
|
-
return { installed: false };
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* The fields SpecPi owns, in the guard's own schema. Anything absent here keeps the package's
|
|
80
|
-
* default, so its thresholds, command lists, protected paths and model settings stay its business.
|
|
81
|
-
*
|
|
82
|
-
* There is no `enabled: true` form of this, deliberately. SpecPi has no command that arms the
|
|
83
|
-
* guard, so a function that could produce an armed configuration would have no caller and one
|
|
84
|
-
* obvious wrong use.
|
|
85
|
-
*/
|
|
86
|
-
export function desiredConfig() {
|
|
87
|
-
return {
|
|
88
|
-
// Off means the guard never enters the tool path, so no key is needed and nothing is sent.
|
|
89
|
-
enabled: false,
|
|
90
|
-
// Left at the guard's own default, and asserted so the two halves of the Jev story resolve
|
|
91
|
-
// the same credential: one `/login openrouter` serves the advisor and the guard.
|
|
92
|
-
backend: "openrouter",
|
|
93
|
-
// With a UI, a middle-band verdict asks rather than deciding on its own. Without one the
|
|
94
|
-
// guard fails closed; that is the package's design, disclosed rather than configured away.
|
|
95
|
-
uncertain: "ask",
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export function readConfig() {
|
|
100
|
-
try {
|
|
101
|
-
const file = guardConfigFile();
|
|
102
|
-
if (!regularFile(file, "Jev guard settings")) {
|
|
103
|
-
return undefined;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
107
|
-
} catch {
|
|
108
|
-
return undefined;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Write the inert posture, merged into whatever is already there.
|
|
114
|
-
*
|
|
115
|
-
* Merged, not replaced: a user's own thresholds, safe-command globs and protected paths survive,
|
|
116
|
-
* and only the three fields above are asserted. Idempotent, and it reports what it changed so the
|
|
117
|
-
* installer can say so.
|
|
118
|
-
*
|
|
119
|
-
* It runs on install and on update, and it always asserts `enabled: false`. Not only when the file
|
|
120
|
-
* is absent, which is the version of this that looks safer and is not: an install carrying a stale
|
|
121
|
-
* `enabled: true` from before this package was last unpinned would arm a fail-closed gate the
|
|
122
|
-
* moment the package came back, with nobody present to be told. So the rule is the blunt one --
|
|
123
|
-
* SpecPi never leaves an armed gate behind an installer run -- and `disarmed` is returned so the
|
|
124
|
-
* run can report the one case where that took something away from someone.
|
|
125
|
-
*
|
|
126
|
-
* This is an occasional, human-initiated act, which is what makes the blunt rule affordable. It
|
|
127
|
-
* deliberately does not run at session start: the package owns its switch between installs, and a
|
|
128
|
-
* per-session rewrite would mean `/jev-guard on --global` never survived a restart.
|
|
129
|
-
*/
|
|
130
|
-
export function applyInertConfig() {
|
|
131
|
-
if (!installed().installed) {
|
|
132
|
-
return { applied: false, reason: "not-installed" };
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const desired = desiredConfig();
|
|
136
|
-
const current = readConfig();
|
|
137
|
-
if (current && Object.entries(desired).every(([key, value]) => current[key] === value)) {
|
|
138
|
-
return { applied: false, reason: "already-current" };
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
writeFileAtomic(guardConfigFile(), `${JSON.stringify({ ...(current ?? {}), ...desired }, null, 4)}\n`);
|
|
142
|
-
|
|
143
|
-
return {
|
|
144
|
-
applied: true,
|
|
145
|
-
reason: current ? "updated" : "created",
|
|
146
|
-
// The one change worth announcing: everything else here is establishing a default, and this
|
|
147
|
-
// is switching off a gate a human had switched on.
|
|
148
|
-
disarmed: current?.enabled === true,
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
export function configPath() {
|
|
153
|
-
return guardConfigFile();
|
|
154
|
-
}
|