specpi 0.26.0 → 0.28.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 +80 -0
- package/README.md +37 -3
- package/SECURITY_MODEL.md +44 -4
- package/THIRD_PARTY.md +9 -1
- package/extensions/jev-advisor/broker.mjs +277 -0
- package/extensions/jev-advisor/client.mjs +172 -0
- package/extensions/jev-advisor/config.mjs +270 -0
- package/extensions/jev-advisor/consent.mjs +133 -0
- package/extensions/jev-advisor/gate.mjs +263 -0
- package/extensions/jev-advisor/index.ts +999 -0
- package/extensions/jev-advisor/key-source.mjs +252 -0
- package/extensions/jev-advisor/layer.mjs +169 -0
- package/extensions/jev-advisor/ledger.mjs +138 -0
- package/extensions/jev-advisor/questions/capabilities.mjs +124 -0
- package/extensions/jev-advisor/questions/compaction.mjs +153 -0
- package/extensions/jev-advisor/questions/gap.mjs +140 -0
- package/extensions/jev-advisor/questions/guard.mjs +168 -0
- package/extensions/jev-advisor/questions/progress.mjs +195 -0
- package/extensions/jev-advisor/questions/retention.mjs +188 -0
- package/extensions/jev-advisor/questions/sources.mjs +91 -0
- package/extensions/jev-advisor/questions/untrusted.mjs +69 -0
- package/extensions/jev-advisor/risk.mjs +442 -0
- package/extensions/jev-advisor/sanitize.mjs +0 -0
- package/extensions/jev-advisor/usage.mjs +92 -0
- package/extensions/tool-wishlist/authoring-tools.mjs +42 -0
- package/extensions/tool-wishlist/index.ts +11 -0
- package/extensions/workflow-controls/capabilities.mjs +26 -0
- package/extensions/workflow-controls/index.ts +2 -2
- package/package.json +1 -1
- package/scripts/packages.mjs +56 -0
- package/scripts/specpi.mjs +73 -4
|
@@ -884,7 +884,7 @@ export default function workflowControls(pi: ExtensionAPI) {
|
|
|
884
884
|
pi.registerTool({
|
|
885
885
|
name: "request_capability",
|
|
886
886
|
label: "Request Capability",
|
|
887
|
-
description: `Ask the user to restore a withdrawn SpecPi tool group for this session. Available groups — ${describeCapabilities()}. Their tools are hidden to keep each request small, so request a group only when the current task actually needs it, and continue without it if the user declines. The restored tools are usable from your next message and stay available until the session ends. Delegation is not requestable here; ask the user to run /delegate on.`,
|
|
887
|
+
description: `Ask the user to restore a withdrawn SpecPi tool group for this session. Available groups — ${describeCapabilities()}. Their tools are hidden to keep each request small, and restoring one mid-session also discards the provider's cached prompt prefix, which measured about 20% of a mid-length attempt's cost in this project's own testing — so request a group only when the current task actually needs it, and continue without it if the user declines. The restored tools are usable from your next message and stay available until the session ends. Delegation is not requestable here; ask the user to run /delegate on.`,
|
|
888
888
|
parameters: Type.Object(
|
|
889
889
|
{
|
|
890
890
|
capability: StringEnum(CAPABILITY_NAMES, {
|
|
@@ -959,7 +959,7 @@ export default function workflowControls(pi: ExtensionAPI) {
|
|
|
959
959
|
standing ||
|
|
960
960
|
(await ctx.ui.confirm(
|
|
961
961
|
`Allow ${capability.label} for this session?`,
|
|
962
|
-
`The agent asked to ${capability.summary}. Reason given: ${safeMessage(params.reason)}\n\nThis offers ${pending.length} tool${pending.length === 1 ? "" : "s"} for the rest of this session
|
|
962
|
+
`The agent asked to ${capability.summary}. Reason given: ${safeMessage(params.reason)}\n\nThis offers ${pending.length} tool${pending.length === 1 ? "" : "s"} for the rest of this session, adds ${capability.schemaCost}, ${capability.activationCost}. Withdraw them again with ${capability.command} off.`,
|
|
963
963
|
));
|
|
964
964
|
if (!accepted) {
|
|
965
965
|
return {
|
package/package.json
CHANGED
package/scripts/packages.mjs
CHANGED
|
@@ -48,6 +48,54 @@ export function runBrowserQA(agentDir, command) {
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Packages a past SpecPi version pinned and this one no longer does.
|
|
53
|
+
*
|
|
54
|
+
* Dropping an entry from `templates/settings.json` stops new installs getting it and does nothing at
|
|
55
|
+
* all to a machine that already has it: the entry stays in `settings.json` and Pi keeps loading it.
|
|
56
|
+
* That is tolerable for a package that merely stopped being useful, and not tolerable for
|
|
57
|
+
* `specpi-jev-guard`, which fails closed -- an install left holding it after SpecPi deleted both the
|
|
58
|
+
* code that kept it inert and the `/jev guard off` command that could disarm it would block every
|
|
59
|
+
* shell call the moment its key or its endpoint went away, with nothing left to turn it off.
|
|
60
|
+
*
|
|
61
|
+
* So retirement is explicit, and it removes the entry rather than waiting for the restore path to.
|
|
62
|
+
*/
|
|
63
|
+
export const retiredPackages = Object.freeze(["npm:specpi-jev-guard"]);
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Drop retired entries from a settings object, in place, returning what was removed.
|
|
67
|
+
*
|
|
68
|
+
* Every entry goes, whatever shape it has. That is a deliberate exception to the ownership rule the
|
|
69
|
+
* restore path applies elsewhere -- "a user-modified entry is theirs" -- and the exception is the
|
|
70
|
+
* reason the list is not open to additions. Preserving a modified `specpi-jev-guard` entry preserved
|
|
71
|
+
* a fail-closed gate that this release removed the controls for: the code that rewrote its global
|
|
72
|
+
* configuration to `enabled: false` at every session start is gone, and so is `/jev guard off`, so a
|
|
73
|
+
* preserved entry is a gate that refuses every shell call the moment its key or endpoint goes away,
|
|
74
|
+
* with nothing left to turn it off. Filters are worth less than that.
|
|
75
|
+
*
|
|
76
|
+
* The removed entry is returned verbatim so the caller can print it back, and the write it belongs
|
|
77
|
+
* to is inside the installer's transaction and backed up with everything else. Version is not
|
|
78
|
+
* consulted: the reason for retirement is the package.
|
|
79
|
+
*/
|
|
80
|
+
export function removeRetiredPackages(settings) {
|
|
81
|
+
if (!Array.isArray(settings?.packages)) {
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const removed = [];
|
|
86
|
+
settings.packages = settings.packages.filter((entry) => {
|
|
87
|
+
if (!retiredPackages.includes(packageIdentity(entry))) {
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
removed.push(packageSource(entry));
|
|
92
|
+
|
|
93
|
+
return false;
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return removed;
|
|
97
|
+
}
|
|
98
|
+
|
|
51
99
|
export function packageChanges(before, after) {
|
|
52
100
|
return basePackages.map((source) => {
|
|
53
101
|
const identity = packageIdentity(source);
|
|
@@ -134,6 +182,14 @@ export function installBasePackages(agentDir) {
|
|
|
134
182
|
|
|
135
183
|
export function checkBasePackages(agentDir, settings) {
|
|
136
184
|
const errors = [];
|
|
185
|
+
for (const entry of Array.isArray(settings.packages) ? settings.packages : []) {
|
|
186
|
+
if (retiredPackages.includes(packageIdentity(entry))) {
|
|
187
|
+
errors.push(
|
|
188
|
+
`Retired base package still configured: ${packageSource(entry)}. Run specpi update to unpin it.`,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
137
193
|
for (const source of basePackages) {
|
|
138
194
|
const identity = packageIdentity(source);
|
|
139
195
|
const entry =
|
package/scripts/specpi.mjs
CHANGED
|
@@ -22,7 +22,14 @@ import {
|
|
|
22
22
|
import { validateCapabilityRegistry } from "../extensions/tool-wishlist/registry.mjs";
|
|
23
23
|
import { runValidator } from "../extensions/tool-wishlist/validators.mjs";
|
|
24
24
|
import { acquireSpecPiLock } from "./lock.mjs";
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
basePackages,
|
|
27
|
+
checkBasePackages,
|
|
28
|
+
installBasePackages,
|
|
29
|
+
packageChanges,
|
|
30
|
+
removeRetiredPackages,
|
|
31
|
+
runBrowserQA,
|
|
32
|
+
} from "./packages.mjs";
|
|
26
33
|
|
|
27
34
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
28
35
|
const VERSION = JSON.parse(fs.readFileSync(path.join(repoRoot, "package.json"), "utf8")).version;
|
|
@@ -46,6 +53,27 @@ const resourcePaths = [
|
|
|
46
53
|
"extensions/tool-wishlist/registry.mjs",
|
|
47
54
|
"extensions/tool-wishlist/validators.mjs",
|
|
48
55
|
"extensions/tool-wishlist/capabilities.json",
|
|
56
|
+
"extensions/tool-wishlist/authoring-tools.mjs",
|
|
57
|
+
"extensions/jev-advisor/index.ts",
|
|
58
|
+
"extensions/jev-advisor/config.mjs",
|
|
59
|
+
"extensions/jev-advisor/consent.mjs",
|
|
60
|
+
"extensions/jev-advisor/ledger.mjs",
|
|
61
|
+
"extensions/jev-advisor/sanitize.mjs",
|
|
62
|
+
"extensions/jev-advisor/usage.mjs",
|
|
63
|
+
"extensions/jev-advisor/client.mjs",
|
|
64
|
+
"extensions/jev-advisor/key-source.mjs",
|
|
65
|
+
"extensions/jev-advisor/layer.mjs",
|
|
66
|
+
"extensions/jev-advisor/risk.mjs",
|
|
67
|
+
"extensions/jev-advisor/questions/guard.mjs",
|
|
68
|
+
"extensions/jev-advisor/broker.mjs",
|
|
69
|
+
"extensions/jev-advisor/gate.mjs",
|
|
70
|
+
"extensions/jev-advisor/questions/retention.mjs",
|
|
71
|
+
"extensions/jev-advisor/questions/compaction.mjs",
|
|
72
|
+
"extensions/jev-advisor/questions/gap.mjs",
|
|
73
|
+
"extensions/jev-advisor/questions/sources.mjs",
|
|
74
|
+
"extensions/jev-advisor/questions/progress.mjs",
|
|
75
|
+
"extensions/jev-advisor/questions/untrusted.mjs",
|
|
76
|
+
"extensions/jev-advisor/questions/capabilities.mjs",
|
|
49
77
|
"skills/specpi-improve/SKILL.md",
|
|
50
78
|
];
|
|
51
79
|
|
|
@@ -59,7 +87,7 @@ Usage:
|
|
|
59
87
|
specpi doctor
|
|
60
88
|
specpi uninstall [--yes]
|
|
61
89
|
|
|
62
|
-
Installs /scope, the harness improvement loop, and
|
|
90
|
+
Installs /scope, the harness improvement loop, and seven pinned packages.
|
|
63
91
|
The base is tested with Pi 0.84.4. Run specpi plan to see package versions.
|
|
64
92
|
--skip-package-install installs only the core, or preserves an existing base on update.
|
|
65
93
|
--skip-browser-install skips Chromium setup, not package acquisition or doctor checks.
|
|
@@ -142,7 +170,9 @@ function validateManifestPaths(manifest) {
|
|
|
142
170
|
assertLocalPath(target, agentDir);
|
|
143
171
|
const relative = path.relative(agentDir, target).replaceAll("\\", "/");
|
|
144
172
|
if (
|
|
145
|
-
|
|
173
|
+
// jev-advisor is the one managed extension with a subdirectory, so it is listed
|
|
174
|
+
// separately rather than loosening the single-segment rule for every other family.
|
|
175
|
+
!/^(extensions\/(?:workflow-controls|tool-wishlist|browser|command-guard|delegation|background-tasks|structural-search|files|spec|specpi-ui-refresh)\/[^/]+|extensions\/jev-advisor\/(?:questions\/)?[^/]+|extensions\/spec\.ts|skills\/(?:specpi-improve|donsetch)\/SKILL\.md|themes\/(?:tea-house|specpi-spec)\.json|specpi\/pi-profiles\.sh)$/.test(
|
|
146
176
|
relative,
|
|
147
177
|
)
|
|
148
178
|
) {
|
|
@@ -230,6 +260,32 @@ function restoreLegacySettings(manifest, warnings) {
|
|
|
230
260
|
writeJson(settingsPath, settings, existingMode(settingsPath, 0o600));
|
|
231
261
|
}
|
|
232
262
|
|
|
263
|
+
/**
|
|
264
|
+
* Unpin a package a past version installed and this one has retired.
|
|
265
|
+
*
|
|
266
|
+
* Deliberately outside the `--skip-packages` guard. Skipping package acquisition means not
|
|
267
|
+
* downloading or re-pinning anything, and it has never meant leaving an entry SpecPi itself wrote
|
|
268
|
+
* pointing at code SpecPi has since removed the controls for -- `specpi-jev-guard` is fail-closed,
|
|
269
|
+
* so the machine that skipped packages is exactly the machine that would keep it armed forever.
|
|
270
|
+
*
|
|
271
|
+
* `settingsPath` is in the transaction's watched set for every non-uninstall operation, so this
|
|
272
|
+
* write is snapshotted, backed up and rolled back with everything else.
|
|
273
|
+
*/
|
|
274
|
+
function retireBasePackages(warnings) {
|
|
275
|
+
const settings = readJson(settingsPath, {});
|
|
276
|
+
const removed = removeRetiredPackages(settings);
|
|
277
|
+
if (removed.length === 0) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
writeJson(settingsPath, settings, existingMode(settingsPath, 0o600));
|
|
282
|
+
for (const source of removed) {
|
|
283
|
+
warnings.push(
|
|
284
|
+
`Unpinned retired package: ${source}. Its downloaded files stay in the agent npm directory, Pi no longer loads it, and any settings of your own on that entry are in this run's backup.`,
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
233
289
|
function removeLegacyShell(manifest) {
|
|
234
290
|
if (manifest?.shellRc && fs.existsSync(manifest.shellRc)) {
|
|
235
291
|
const result = removeManagedBlock(fs.readFileSync(manifest.shellRc, "utf8"), SHELL_START, SHELL_END);
|
|
@@ -284,7 +340,16 @@ async function mutate(options, operation) {
|
|
|
284
340
|
...files.map(([, target]) => target),
|
|
285
341
|
...Object.keys(previous?.files || {}),
|
|
286
342
|
];
|
|
287
|
-
|
|
343
|
+
// Watched whenever anything in this run can write it. `retireBasePackages` runs on every
|
|
344
|
+
// non-uninstall operation, including under `--skip-package-install`, so the narrower
|
|
345
|
+
// condition that used to guard this left that write outside the snapshot -- unbacked up, and
|
|
346
|
+
// not rolled back by a later failure in the same transaction.
|
|
347
|
+
if (
|
|
348
|
+
operation !== "uninstall" ||
|
|
349
|
+
!options.skipPackages ||
|
|
350
|
+
previous?.settingsChanges?.length ||
|
|
351
|
+
previous?.packageChanges?.length
|
|
352
|
+
) {
|
|
288
353
|
watched.push(settingsPath);
|
|
289
354
|
}
|
|
290
355
|
|
|
@@ -305,6 +370,10 @@ async function mutate(options, operation) {
|
|
|
305
370
|
const warnings = [];
|
|
306
371
|
const preserveBase = operation !== "uninstall" && options.skipPackages && previous?.basePackages?.length;
|
|
307
372
|
restoreLegacySettings(preserveBase ? { ...previous, packageChanges: [] } : previous, warnings);
|
|
373
|
+
if (operation !== "uninstall") {
|
|
374
|
+
retireBasePackages(warnings);
|
|
375
|
+
}
|
|
376
|
+
|
|
308
377
|
removeLegacyShell(previous);
|
|
309
378
|
let packageState = preserveBase
|
|
310
379
|
? {
|