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 +18 -0
- package/README.md +6 -5
- package/package.json +1 -1
- package/src/decide.ts +2 -3
- package/src/extension.ts +31 -6
- package/src/jev/availability.ts +6 -4
- package/src/policy.ts +19 -0
- package/src/settings.ts +7 -6
- package/src/ui.ts +1 -2
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
|
|
124
|
-
|
|
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
|
|
138
|
-
|
|
139
|
-
semantic layer is active and `🛡 jev
|
|
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.
|
|
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
|
-
*
|
|
79
|
-
*
|
|
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
|
-
|
|
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 (
|
|
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
|
-
*
|
|
464
|
-
*
|
|
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
|
-
: "
|
|
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
|
|
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;
|
package/src/jev/availability.ts
CHANGED
|
@@ -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"
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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 `
|
|
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
|
-
* `
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
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: "
|
|
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
|
-
|
|
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 {
|