@andares/pi-toolkits 0.9.6 → 0.9.8
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/README.md +22 -16
- package/README.zh-CN.md +20 -14
- package/package.json +8 -6
- package/src/features/ask/constants.ts +12 -0
- package/src/features/ask/index.test.ts +158 -15
- package/src/features/ask/index.ts +151 -27
- package/src/features/ask/prompt.test.ts +34 -22
- package/src/features/ask/prompt.ts +26 -39
- package/src/features/ask/reasons.test.ts +42 -3
- package/src/features/ask/reasons.ts +69 -24
- package/src/features/ask/scratch-tool.test.ts +161 -0
- package/src/features/ask/scratch-tool.ts +195 -0
- package/src/features/ask/scratch.test.ts +172 -0
- package/src/features/ask/scratch.ts +324 -0
- package/src/features/autostart/autostart.ts +1 -1
- package/src/features/autostart/index.ts +1 -1
- package/src/features/compact/compact.test.ts +33 -10
- package/src/features/compact/index.ts +27 -34
- package/src/features/compact/prompt.ts +23 -12
- package/src/features/favorites/constants.ts +6 -5
- package/src/features/favorites/patch-cycle.ts +4 -3
- package/src/features/favorites/patch-selector.ts +2 -2
- package/src/features/permission/index.test.ts +27 -3
- package/src/features/permission/index.ts +6 -0
- package/src/features/stash/stash.ts +1 -1
- package/src/lib/gates.ts +4 -0
- package/src/lib/model-config.ts +2 -2
- package/src/lib/model-picker.ts +1 -1
- package/src/lib/tools.ts +48 -0
|
@@ -8,13 +8,18 @@
|
|
|
8
8
|
import { mkdtempSync, realpathSync } from "node:fs";
|
|
9
9
|
import { tmpdir } from "node:os";
|
|
10
10
|
import { join } from "node:path";
|
|
11
|
-
import { describe, expect, it } from "vitest";
|
|
11
|
+
import { beforeEach, describe, expect, it } from "vitest";
|
|
12
12
|
import type {
|
|
13
13
|
ExtensionAPI,
|
|
14
14
|
ExtensionContext,
|
|
15
15
|
ToolCallEvent,
|
|
16
16
|
} from "@earendil-works/pi-coding-agent";
|
|
17
17
|
import type { AskDecision, GateHub, ToolGate } from "../../lib/gates.js";
|
|
18
|
+
import {
|
|
19
|
+
clearJailedTool,
|
|
20
|
+
markJailedTool,
|
|
21
|
+
resetJailedTools,
|
|
22
|
+
} from "../../lib/tools.js";
|
|
18
23
|
import { markStartupCommand } from "../../lib/startup.js";
|
|
19
24
|
import { registerPermission } from "./index.js";
|
|
20
25
|
|
|
@@ -391,8 +396,7 @@ describe("model-facing banner", () => {
|
|
|
391
396
|
});
|
|
392
397
|
});
|
|
393
398
|
|
|
394
|
-
describe("session grants cover repeat asks", () => {
|
|
395
|
-
it("stops asking for a destructive in-project command once allowed", async () => {
|
|
399
|
+
describe("session grants cover repeat asks", () => { it("stops asking for a destructive in-project command once allowed", async () => {
|
|
396
400
|
const h = makeHarness();
|
|
397
401
|
await h.command("allow allow allow ask");
|
|
398
402
|
const first = asAsk(await h.evaluate(bashEvent("rm -rf .")));
|
|
@@ -411,3 +415,23 @@ describe("session grants cover repeat asks", () => {
|
|
|
411
415
|
expect(await h.evaluate(bashEvent(command))).toBeUndefined();
|
|
412
416
|
});
|
|
413
417
|
});
|
|
418
|
+
|
|
419
|
+
describe("self-jailed tools", () => {
|
|
420
|
+
beforeEach(() => {
|
|
421
|
+
resetJailedTools();
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
it("skips the boundary check only while the owning feature marks the tool", async () => {
|
|
425
|
+
const h = makeHarness();
|
|
426
|
+
// Make the judgement observable: with in-project writes set to ask, an
|
|
427
|
+
// unmarked pathless tool would raise a dialog.
|
|
428
|
+
await h.command("ask ask ask ask");
|
|
429
|
+
expect(asAsk(await h.evaluate(customEvent("scratchpad"))).kind).toBe("ask");
|
|
430
|
+
|
|
431
|
+
markJailedTool("scratchpad");
|
|
432
|
+
expect(await h.evaluate(customEvent("scratchpad"))).toBeUndefined();
|
|
433
|
+
|
|
434
|
+
clearJailedTool("scratchpad");
|
|
435
|
+
expect(asAsk(await h.evaluate(customEvent("scratchpad"))).kind).toBe("ask");
|
|
436
|
+
});
|
|
437
|
+
});
|
|
@@ -23,6 +23,7 @@ import type {
|
|
|
23
23
|
} from "@earendil-works/pi-coding-agent";
|
|
24
24
|
import { Text } from "@earendil-works/pi-tui";
|
|
25
25
|
import type { AskRequest, GateHub } from "../../lib/gates.js";
|
|
26
|
+
import { isJailedTool } from "../../lib/tools.js";
|
|
26
27
|
import { consumeStartupCommand } from "../../lib/startup.js";
|
|
27
28
|
import {
|
|
28
29
|
DEFAULT_MODES,
|
|
@@ -356,6 +357,11 @@ export function registerPermission(pi: ExtensionAPI, gates: GateHub): void {
|
|
|
356
357
|
order: 20,
|
|
357
358
|
evaluate(event, ctx) {
|
|
358
359
|
if (!enabled) return undefined;
|
|
360
|
+
// A self-jailed tool (ask's `scratchpad`) can only reach the temp directory
|
|
361
|
+
// it owns: there is no boundary to judge, and the operator sanctioned that
|
|
362
|
+
// space by entering the mode that activates the tool. The owning feature
|
|
363
|
+
// marks it only while active, so an inactive tool is judged normally.
|
|
364
|
+
if (isJailedTool(event.toolName)) return undefined;
|
|
359
365
|
const units = unitsFor(event, ctx.cwd);
|
|
360
366
|
if (units.length === 0) return undefined;
|
|
361
367
|
const decision = decideProgram(units, {
|
|
@@ -21,7 +21,7 @@ import type { KeyId } from "@earendil-works/pi-tui";
|
|
|
21
21
|
/**
|
|
22
22
|
* Stash hotkey.
|
|
23
23
|
*
|
|
24
|
-
* Compatibility rationale (verified against pi
|
|
24
|
+
* Compatibility rationale (verified against pi 0.99.0–1.4.0 keybindings, live
|
|
25
25
|
* matchesKey probes and terminal/OS behavior):
|
|
26
26
|
* - pi: no default binding for ctrl+alt+<letter> (only ctrl+alt+] is taken);
|
|
27
27
|
* extension shortcuts dispatch first while the editor is focused
|
package/src/lib/gates.ts
CHANGED
|
@@ -17,6 +17,10 @@
|
|
|
17
17
|
* slot; a second presentation would mount over the first and strand its
|
|
18
18
|
* promise. The queue serializes our own presentations; the host slot is
|
|
19
19
|
* still shared with other extensions (pi does not arbitrate globally).
|
|
20
|
+
* pi 1.4.0 stacks extension dialogs so a second one no longer strands the
|
|
21
|
+
* first (#11166), but the 1.1–1.3 hosts in the supported window still
|
|
22
|
+
* replace it — the queue stays, and it also keeps one confirmation visible
|
|
23
|
+
* at a time instead of a pile of stacked dialogs on 1.4.0.
|
|
20
24
|
* - A gate that throws blocks the call (fail closed), matching pi's own
|
|
21
25
|
* `tool_call` handler contract.
|
|
22
26
|
*/
|
package/src/lib/model-config.ts
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* (review, compact-model — favorites keeps its own store-local key logic).
|
|
5
5
|
*
|
|
6
6
|
* Types are derived from pi-coding-agent's own exports (ModelRegistry)
|
|
7
|
-
* rather than imported from @earendil-works/pi-ai: pi-ai's index.d.ts (1.
|
|
8
|
-
* through 1.
|
|
7
|
+
* rather than imported from @earendil-works/pi-ai: pi-ai's index.d.ts (1.1.0
|
|
8
|
+
* through 1.4.0 alike) re-exports `Model`/`Api` via extensioned
|
|
9
9
|
* `./types.ts` specifiers, which tsc 5.9 bundler resolution substitutes to
|
|
10
10
|
* `.d.ts` but some tsserver builds do not. Deriving from ModelRegistry keeps
|
|
11
11
|
* every resolver in agreement with the exact type
|
package/src/lib/model-picker.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* The component is driven through a small ModelRuntime facade backed by
|
|
11
11
|
* ctx.modelRegistry (pi's extension-facing wrapper). SAFETY: verified
|
|
12
|
-
* against pi 1.
|
|
12
|
+
* against pi 1.1.0–1.4.0 dist (modes/interactive/components/model-selector.js +
|
|
13
13
|
* model-catalog-refresh.js — byte-identical across that span) — the
|
|
14
14
|
* component only calls getAvailableSnapshot, getModel, getError and refresh
|
|
15
15
|
* on modelRuntime, and ModelRegistry provides exactly those
|
package/src/lib/tools.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared tool-set management helpers (feature-agnostic).
|
|
3
|
+
*
|
|
4
|
+
* Two concerns live here because both are about tool identity rather than any
|
|
5
|
+
* one feature: snapshotting/restoring the active set, and the registry of
|
|
6
|
+
* self-jailed tools that the boundary gate consults.
|
|
3
7
|
*/
|
|
4
8
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
5
9
|
|
|
@@ -23,3 +27,47 @@ export function withoutTools(
|
|
|
23
27
|
): string[] {
|
|
24
28
|
return active.filter((name) => !removed.has(name));
|
|
25
29
|
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Self-jailed tools — a registry the boundary gate consults.
|
|
33
|
+
*
|
|
34
|
+
* A tool can be *jailed*: its own implementation only reaches a place it
|
|
35
|
+
* created and owns, so there is no boundary for `permission` to judge and no
|
|
36
|
+
* dialog to raise. Example: ask mode's `scratchpad` writes into the session's
|
|
37
|
+
* temp directory through a relative, validated name; running it through the
|
|
38
|
+
* boundary analysis would ask "outside the project, write?" on every note even
|
|
39
|
+
* though the operator already sanctioned that space by entering ask mode.
|
|
40
|
+
*
|
|
41
|
+
* This is intentionally a registry rather than a bypass baked into
|
|
42
|
+
* `permission`: the owning feature marks the tool only while it is active, and
|
|
43
|
+
* `permission` does not need to know which feature that is. A tool that is not
|
|
44
|
+
* marked is judged normally, so forgetting to clear the mark would widen the
|
|
45
|
+
* boundary — hence the owning feature clears it on exit.
|
|
46
|
+
*
|
|
47
|
+
* Narrow on purpose: the registry covers tool *names*, not paths. A tool only
|
|
48
|
+
* belongs here when its caller cannot point it at a path.
|
|
49
|
+
*/
|
|
50
|
+
const jailed = new Set<string>();
|
|
51
|
+
|
|
52
|
+
/** Mark a tool as self-jailed (call while it is active). */
|
|
53
|
+
export function markJailedTool(name: string): void {
|
|
54
|
+
jailed.add(name);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Remove the mark (call when the tool is deactivated). */
|
|
58
|
+
export function clearJailedTool(name: string): void {
|
|
59
|
+
jailed.delete(name);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Whether `name` is a self-jailed tool that is currently active. */
|
|
63
|
+
export function isJailedTool(name: string): boolean {
|
|
64
|
+
return jailed.has(name);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Drop every mark. Tests call this between cases; production code clears one
|
|
69
|
+
* tool at a time when the owning feature exits.
|
|
70
|
+
*/
|
|
71
|
+
export function resetJailedTools(): void {
|
|
72
|
+
jailed.clear();
|
|
73
|
+
}
|