pi-jev-auto-mode 0.4.0 → 0.4.1

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 CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.1 - 2026-09-18
4
+
5
+ - **An unclear answer passes by default.** `uncertain` defaulted to `deny`, which meant a
6
+ judgment the model was unsure about stopped the call. That is the interruption an auto mode
7
+ exists to remove; clear rejections still block. `uncertain deny` and `uncertain ask` remain
8
+ available for anyone who wants the stricter behaviour.
9
+ - **Without a key the gate says so and stops**, instead of inventing a verdict and blocking with
10
+ an unexplained reason. The message names the fix: `/jev-auto-mode login`, or
11
+ `/jev-auto-mode off`. The footer reads `🛡 jev no key` in that state, and the session start
12
+ warns once.
13
+ - **A chain of read-only commands is read-only.** `cd src && ls -la && git log -3` was judged as
14
+ a whole because the allowlist rejected any command containing shell control syntax, so agents
15
+ paid a judgment round trip for their most common line. Each segment is now checked on its own,
16
+ and `cd` is allowed. A chain containing anything else (`curl … | sh`) is still judged.
17
+ - **The docs match the no-key behaviour.** The README and a comment still described a fallback
18
+ that confirms in a UI, which the gate no longer does: without a key it stops the calls it cannot
19
+ judge and says it is not connected to Jev.
20
+
3
21
 
4
22
  ## 0.4.0
5
23
 
package/README.md CHANGED
@@ -120,8 +120,9 @@ pi --jev-auto-mode start with auto mode enabled
120
120
  ```
121
121
 
122
122
  The semantic layer needs a [TypeSafe](https://typesafe.ai/) API key. Jev is early access, so an
123
- account may be waitlisted; **the gate still works without one**, running in ask-only mode
124
- (confirm in a UI, block without one) rather than silently allowing everything.
123
+ account may be waitlisted; **the gate still works without one**. Its own rules keep running —
124
+ read-only and user-declared safe commands pass, hard-deny shapes are blocked but a call
125
+ nothing vouches for is blocked with "Not connected to Jev" instead of being judged.
125
126
 
126
127
  `/jev-auto-mode login` asks for the key, verifies it against the API (`GET /v1/models`), and
127
128
  stores it as an owner-only file at
@@ -134,9 +135,9 @@ A key is only stored after the API accepts it: a typo that got saved would turn
134
135
  that silently blocks every escalated call. If the API cannot be reached the key is not stored
135
136
  either, and the command says so rather than claiming success.
136
137
 
137
- Without a key the gate does not disable itself: it falls back to the ask-only engine, which
138
- confirms in a UI and blocks when there is none. The footer shows `🛡 jev (<scope>)` while the
139
- semantic layer is active and `🛡 jev ask-only (<scope>)` when it is not.
138
+ Without a key the gate does not disable itself and does not allow everything: it stops the calls
139
+ it cannot judge and says it is not connected to Jev. The footer shows `🛡 jev (<scope>)` while the
140
+ semantic layer is active and `🛡 jev no key (<scope>)` when it is not.
140
141
 
141
142
  ## Tuning
142
143
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-jev-auto-mode",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Jev (TypeSafe System One) backed auto mode for the Pi coding agent: semantically auto-approves bash, write, and edit tool calls and fails closed when a decision cannot be made.",
5
5
  "keywords": [
6
6
  "pi-package",
package/src/decide.ts CHANGED
@@ -75,9 +75,8 @@ export interface DecisionEngine {
75
75
  /**
76
76
  * Milestone-1 engine: no semantic judgment at all.
77
77
  *
78
- * Every candidate is reported as `uncertain`, which means "ask the user when a UI
79
- * exists, block otherwise". That keeps the deterministic layer shippable and
80
- * verifiable on its own, and it fails in the safe direction.
78
+ * The gate checks for this engine before judging: with no key there is nothing to
79
+ * judge with, so it says so and blocks the call instead of inventing a verdict.
81
80
  */
82
81
  export function createManualEngine(): DecisionEngine {
83
82
  return {
package/src/extension.ts CHANGED
@@ -41,7 +41,7 @@ import {
41
41
  dangerousReasons,
42
42
  evaluateUserCommandRules,
43
43
  hardDenyReasons,
44
- isReadOnlyCommand,
44
+ isReadOnlyCommandChain,
45
45
  isUserDeclaredSafeCommand,
46
46
  PROTECTED_DIRECTORY_SEGMENTS,
47
47
  unique,
@@ -80,6 +80,13 @@ export const AUTO_MODE_COMMAND = "jev-auto-mode";
80
80
  /** The escalation reason for a call no pattern describes, under `gateScope: "all"`. */
81
81
  export const NOT_KNOWN_SAFE_REASON = "not on the known-safe list";
82
82
 
83
+ /** The engine used when no semantic layer is available. */
84
+ export const MANUAL_ENGINE_ID = "manual";
85
+
86
+ /** Shown (and used as the block reason) when the gate has no Jev connection. */
87
+ export const NO_ENGINE_MESSAGE =
88
+ "Not connected to Jev (no TypeSafe API key is set). Run `/jev-auto-mode login` to set a key, or `/jev-auto-mode off` to stop auto mode.";
89
+
83
90
  /** Structural context: what this extension needs from Pi, and nothing more. */
84
91
  export interface GateUi {
85
92
  notify(message: string, type?: "info" | "warning" | "error"): void;
@@ -248,7 +255,7 @@ export async function evaluateToolCall(
248
255
  const matchedReasons = dangerousReasons(command, ctx.cwd);
249
256
  if (matchedReasons.length > 0) {
250
257
  reasons = matchedReasons;
251
- } else if (isReadOnlyCommand(command)) {
258
+ } else if (isReadOnlyCommandChain(command)) {
252
259
  return undefined;
253
260
  } else if (state.settings.gateScope === "matched") {
254
261
  return undefined;
@@ -265,6 +272,20 @@ export async function evaluateToolCall(
265
272
  reasons = protectedReasons;
266
273
  }
267
274
 
275
+ // Without a key there is nothing to judge with. Say so and stop, rather than
276
+ // letting a call through unjudged or blocking it with an unexplained verdict.
277
+ if (deps.engine.id === MANUAL_ENGINE_ID) {
278
+ const rationale = NO_ENGINE_MESSAGE;
279
+ writeRecord(deps, {
280
+ call,
281
+ reasons,
282
+ status: "blocked",
283
+ source: "unavailable",
284
+ rationale,
285
+ });
286
+ return { block: true, reason: rationale };
287
+ }
288
+
268
289
  const input: CandidateInput = {
269
290
  call,
270
291
  reasons,
@@ -460,8 +481,9 @@ export interface RegisterOptions {
460
481
  /**
461
482
  * Build the semantic engine for the current settings.
462
483
  *
463
- * Without a key the gate keeps working with the ask-only engine rather than
464
- * dropping to "allow": the degradation stays visible and stays closed.
484
+ * With no key the gate still runs its own rules, but a call nothing vouches for is
485
+ * blocked with "Not connected to Jev" rather than judged. A missing key must not
486
+ * turn into "allow everything".
465
487
  */
466
488
  export function createEngine(
467
489
  settings: JevAutoModeSettings,
@@ -534,6 +556,9 @@ export function register(pi: ExtensionAPI, options: RegisterOptions = {}): void
534
556
  await rebuildEngine();
535
557
  loaded = true;
536
558
  updateStatus(ctx, { enabled: state.settings.enabled, engineId: deps.engine.id, scope: state.scope });
559
+ if (state.settings.enabled && deps.engine.id === MANUAL_ENGINE_ID) {
560
+ ctx.ui.notify(NO_ENGINE_MESSAGE, "warning");
561
+ }
537
562
  };
538
563
 
539
564
  const save = async (ctx: GateContext): Promise<void> => {
@@ -685,7 +710,7 @@ export function register(pi: ExtensionAPI, options: RegisterOptions = {}): void
685
710
  "Remove the stored TypeSafe API key?",
686
711
  availability.source === "env"
687
712
  ? "It is not in use anyway: TYPESAFE_API_KEY takes precedence."
688
- : "The semantic layer will fall back to ask-only until a key is available again.",
713
+ : "Without a key the gate blocks every call it cannot vouch for, and says why.",
689
714
  );
690
715
  if (!confirmed) return;
691
716
 
@@ -699,7 +724,7 @@ export function register(pi: ExtensionAPI, options: RegisterOptions = {}): void
699
724
  ctx.ui.notify(
700
725
  availability.available
701
726
  ? `Stored key removed. Still using ${describeKeySource(availability.source)}.`
702
- : "Stored key removed. The semantic layer is now ask-only.",
727
+ : "Stored key removed. The gate will block calls it cannot vouch for until a key is set again.",
703
728
  "info",
704
729
  );
705
730
  return;
@@ -1,10 +1,12 @@
1
1
  /**
2
2
  * Is the semantic layer usable right now, and where did the key come from?
3
3
  *
4
- * A missing key must not silently degrade into "allow everything": the engine
5
- * falls back to the ask-only engine, which confirms in a UI and blocks without
6
- * one. The reason and the key's origin are surfaced in `/jev-auto-mode status` so
7
- * the degradation and the credential path are visible rather than mysterious.
4
+ * A missing key must not silently degrade into "allow everything". The gate keeps
5
+ * running its own rules — read-only and user-declared safe commands pass, hard-deny
6
+ * patterns are blocked but a call nothing vouches for is blocked with "Not
7
+ * connected to Jev" instead of being judged. The reason and the key's origin are
8
+ * surfaced in `/jev-auto-mode status` so the situation is visible rather than
9
+ * mysterious.
8
10
  *
9
11
  * `TYPESAFE_API_KEY` wins over the stored secret, so a one-off or CI override does
10
12
  * not require touching the stored credential.
package/src/policy.ts CHANGED
@@ -45,6 +45,7 @@ export interface CommandPattern {
45
45
  export const SAFE_COMMANDS: readonly string[] = [
46
46
  // Shell state and navigation
47
47
  "pwd",
48
+ "cd*",
48
49
  "ls*",
49
50
  "tree*",
50
51
  "whoami",
@@ -392,6 +393,24 @@ export function isReadOnlyCommand(command: string): boolean {
392
393
  return matchesAnyCommandPattern(command, SAFE_COMMANDS, false) !== undefined;
393
394
  }
394
395
 
396
+ /**
397
+ * A chain of read-only commands, such as `cd src && ls -la && git log`.
398
+ *
399
+ * Agents chain commands constantly, and a single `&&` would otherwise take an
400
+ * otherwise harmless line out of the fast path and into a judgment round trip.
401
+ * Every segment must be read-only on its own; `curl … | sh` splits into `curl …`
402
+ * (not on the list) and `sh` (not on the list), so it is still judged, and a
403
+ * segment containing a redirection fails the matcher anyway.
404
+ */
405
+ export function isReadOnlyCommandChain(command: string): boolean {
406
+ const segments = command
407
+ .split(/&&|\|\||;|\||\n/)
408
+ .map((segment) => segment.trim())
409
+ .filter((segment) => segment.length > 0);
410
+ if (segments.length === 0) return false;
411
+ return segments.every((segment) => isReadOnlyCommand(segment));
412
+ }
413
+
395
414
  /**
396
415
  * Patterns the user declared safe.
397
416
  *
package/src/settings.ts CHANGED
@@ -28,7 +28,7 @@ export interface JevAutoModeSettings {
28
28
  readonly extraProtectedPaths: readonly string[];
29
29
  /** Shared state + questions budget guard, in characters. */
30
30
  readonly maxStateCharacters: number;
31
- /** What a middle-band judgment means. Default `deny`: no user confirmation. */
31
+ /** What a middle-band judgment means. Default `allow`: no user confirmation. */
32
32
  readonly uncertain: UncertainAction;
33
33
  /**
34
34
  * Which calls reach the semantic layer.
@@ -52,10 +52,11 @@ export type SettingsScope = "global" | "project";
52
52
  /**
53
53
  * How a judgment that lands in the middle band is resolved.
54
54
  *
55
- * `deny` (the default) means the gate never takes over the screen: Jev's probability
56
- * is the whole answer, and "not sure" fails closed like every other undecidable
57
- * state. `ask` hands the call to the user, which contradicts the point of an auto
58
- * mode and is therefore opt-in. `allow` trusts the middle band.
55
+ * `allow` (the default) keeps an auto mode useful: Jev blocks what it can clearly
56
+ * reject and lets an unclear answer through, so the gate never interrupts. `deny`
57
+ * is the conservative alternative for anyone who wants "not sure" to stop a call.
58
+ * `ask` hands the call to the user, which contradicts the point of an auto mode and
59
+ * is therefore not the default.
59
60
  */
60
61
  export type UncertainAction = "deny" | "ask" | "allow";
61
62
 
@@ -86,7 +87,7 @@ export const DEFAULT_SETTINGS: JevAutoModeSettings = {
86
87
  disallowedCommands: [],
87
88
  extraProtectedPaths: [],
88
89
  maxStateCharacters: 120_000,
89
- uncertain: "deny",
90
+ uncertain: "allow",
90
91
  gateScope: "all",
91
92
  thresholds: {},
92
93
  };
package/src/ui.ts CHANGED
@@ -21,8 +21,7 @@ export interface StatusInput {
21
21
  export function statusText(input: StatusInput): string {
22
22
  if (!input.enabled) return "🛡 jev off";
23
23
  const scope = input.scope === "project" ? "project" : "global";
24
- const engine = input.engineId === "manual" ? " ask-only" : "";
25
- return `🛡 jev${engine} (${scope})`;
24
+ return input.engineId === "manual" ? `🛡 jev no key (${scope})` : `🛡 jev (${scope})`;
26
25
  }
27
26
 
28
27
  export interface StatusContext {