@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.
@@ -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 1.0.0–1.3.0 keybindings, live
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
  */
@@ -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.0.0
8
- * through 1.3.0 alike) re-exports `Model`/`Api` via extensioned
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
@@ -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.0.0–1.3.0 dist (modes/interactive/components/model-selector.js +
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
+ }