pi-warden 0.25.0 → 0.26.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 +17 -0
- package/dist/backend.d.ts +23 -0
- package/dist/backend.js +36 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.js +3 -0
- package/dist/extension.js +22 -12
- package/dist/extension.js.bak +1262 -0
- package/dist/shape.js +2 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,23 @@ How to keep this current: add the entry in the same pull request as the change,
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
<!-- Empty. Next release starts here. -->
|
|
10
|
+
|
|
11
|
+
## 0.26.0
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- OpenRouter as a configurable judgment backend (`typesafeBackend` in user config). Set to `"openrouter"` to route Jev decisions through OpenRouter's decisions API instead of api.typesafe.ai. New `src/backend.ts` module; `OPENROUTER_API_KEY` env var; project overrides cannot redirect judgments.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Bumped pi-typesafe to ^0.6.0 (native `backend` option on `TypeSafeOptions`).
|
|
20
|
+
- Deduplicated `resolveBackend` call in `shape.ts`.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- Host-TUI capability guard: the extension no longer crashes when the host's bundled `pi-tui` lacks the `MouseRegion` component (e.g. omp 18.2.5). A missing named import was a link-time error that silently killed every guard, the `/warden` command, and the shortcut. The import is now a namespace property read (`tuiModule.MouseRegion`), which degrades to `undefined` instead of failing. The status widget renders the same guard text without the clickable wrapper.
|
|
25
|
+
|
|
9
26
|
## 0.25.0
|
|
10
27
|
|
|
11
28
|
### Added
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { TypeSafeOptions } from "pi-typesafe";
|
|
2
|
+
/** The judgment backend that receives pi-warden's Jev requests. */
|
|
3
|
+
export type JudgmentBackend = "typesafe" | "openrouter";
|
|
4
|
+
/** The host the consent disclosure names as the destination. */
|
|
5
|
+
export declare function backendHost(backend: JudgmentBackend): string;
|
|
6
|
+
/** Adapt the consent text to the active backend by substituting the destination host. */
|
|
7
|
+
export declare function disclosureFor(backend: JudgmentBackend, disclosure: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* Whether the right credential exists for the chosen backend.
|
|
10
|
+
* TypeSafe uses the pi-typesafe keystore; other backends use an environment variable.
|
|
11
|
+
*/
|
|
12
|
+
export declare function keyAvailable(backend: JudgmentBackend, env: Record<string, string | undefined>, keystoreUsable: () => boolean): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Build the options forwarded to `createTypeSafe`.
|
|
15
|
+
* The caller must gate on `keyAvailable` before calling `createTypeSafe`.
|
|
16
|
+
*/
|
|
17
|
+
export declare function judgeOptions(config: {
|
|
18
|
+
maxRequests: number;
|
|
19
|
+
timeoutMs: number;
|
|
20
|
+
typesafeBackend: JudgmentBackend;
|
|
21
|
+
}): TypeSafeOptions;
|
|
22
|
+
/** Resolve the effective backend from a raw config value; invalid values fall back to "typesafe". */
|
|
23
|
+
export declare function resolveBackend(raw: unknown): JudgmentBackend;
|
package/dist/backend.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { authState } from "pi-typesafe";
|
|
2
|
+
const BACKENDS = {
|
|
3
|
+
typesafe: { host: "api.typesafe.ai" },
|
|
4
|
+
openrouter: { host: "openrouter.ai", keyEnv: "OPENROUTER_API_KEY" },
|
|
5
|
+
};
|
|
6
|
+
/** The host the consent disclosure names as the destination. */
|
|
7
|
+
export function backendHost(backend) {
|
|
8
|
+
return BACKENDS[backend].host;
|
|
9
|
+
}
|
|
10
|
+
/** Adapt the consent text to the active backend by substituting the destination host. */
|
|
11
|
+
export function disclosureFor(backend, disclosure) {
|
|
12
|
+
return backend === "typesafe" ? disclosure : disclosure.replace(BACKENDS.typesafe.host, BACKENDS[backend].host);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Whether the right credential exists for the chosen backend.
|
|
16
|
+
* TypeSafe uses the pi-typesafe keystore; other backends use an environment variable.
|
|
17
|
+
*/
|
|
18
|
+
export function keyAvailable(backend, env, keystoreUsable) {
|
|
19
|
+
const keyEnv = BACKENDS[backend].keyEnv;
|
|
20
|
+
return keyEnv ? (env[keyEnv]?.trim() ?? "") !== "" : keystoreUsable();
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Build the options forwarded to `createTypeSafe`.
|
|
24
|
+
* The caller must gate on `keyAvailable` before calling `createTypeSafe`.
|
|
25
|
+
*/
|
|
26
|
+
export function judgeOptions(config) {
|
|
27
|
+
return {
|
|
28
|
+
maxRequests: config.maxRequests,
|
|
29
|
+
timeoutMs: config.timeoutMs,
|
|
30
|
+
...(config.typesafeBackend === "typesafe" ? {} : { backend: config.typesafeBackend }),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** Resolve the effective backend from a raw config value; invalid values fall back to "typesafe". */
|
|
34
|
+
export function resolveBackend(raw) {
|
|
35
|
+
return raw === "typesafe" || raw === "openrouter" ? raw : "typesafe";
|
|
36
|
+
}
|
package/dist/config.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { JudgmentBackend } from "./backend.js";
|
|
1
2
|
import type { WidgetConfig } from "./widget.js";
|
|
2
3
|
export interface Threshold {
|
|
3
4
|
/** P(yes) at or above this shows a warning and continues. */
|
|
@@ -217,6 +218,8 @@ export interface WardenConfig {
|
|
|
217
218
|
enabled: boolean;
|
|
218
219
|
/** Consent to send task and action summaries to api.typesafe.ai. Set by /warden enable; never by a project file. */
|
|
219
220
|
typesafe: boolean;
|
|
221
|
+
/** The decisions service the judgments go to. User file only: a project must not redirect judgments to another vendor. */
|
|
222
|
+
typesafeBackend: JudgmentBackend;
|
|
220
223
|
/**
|
|
221
224
|
* steer (default): a confirm-level call is held and the agent receives the judgment as its tool result, so it re-plans or asks
|
|
222
225
|
* the user in chat. confirm: open a dialog and let the user decide (falls back to steer without a UI). advise: never hold; report only.
|
package/dist/config.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { chmodSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
|
+
import { resolveBackend } from "./backend.js";
|
|
4
5
|
import { COMMAND_TOOLS } from "./tools.js";
|
|
5
6
|
import { defaultWidgetConfig } from "./widget.js";
|
|
6
7
|
const RECALL_TOOLS = ["auto", "rg", "ag", "ugrep", "git-grep", "grep", "select-string", "findstr", "none"];
|
|
@@ -15,6 +16,7 @@ export function defaultConfig() {
|
|
|
15
16
|
return {
|
|
16
17
|
enabled: true,
|
|
17
18
|
typesafe: false,
|
|
19
|
+
typesafeBackend: "typesafe",
|
|
18
20
|
mode: "steer",
|
|
19
21
|
timeoutMs: 5000,
|
|
20
22
|
maxRequests: 500,
|
|
@@ -404,6 +406,7 @@ export function applyUserOverrides(base, raw) {
|
|
|
404
406
|
return {
|
|
405
407
|
enabled: boolean(raw.enabled, base.enabled),
|
|
406
408
|
typesafe: boolean(raw.typesafe, base.typesafe),
|
|
409
|
+
typesafeBackend: resolveBackend(raw.typesafeBackend),
|
|
407
410
|
mode: isMode(raw.mode) ? raw.mode : base.mode,
|
|
408
411
|
...shared,
|
|
409
412
|
...applyGuards(base, raw, shared.timeoutMs, "user"),
|
package/dist/extension.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as tuiModule from "@earendil-works/pi-tui";
|
|
2
|
+
const MouseRegion = tuiModule.MouseRegion;
|
|
2
3
|
import { authState, createTypeSafe, describeAuth } from "pi-typesafe";
|
|
3
4
|
import { ensureApiKey } from "pi-typesafe/ui";
|
|
5
|
+
import { backendHost, disclosureFor, judgeOptions, keyAvailable, resolveBackend } from "./backend.js";
|
|
4
6
|
import { ActionGuard } from "./action-guard.js";
|
|
5
7
|
import { ArmingTracker, unparseableArmingRules } from "./arming.js";
|
|
6
8
|
import * as configModule from "./config.js";
|
|
@@ -290,11 +292,13 @@ export default function wardenExtension(pi) {
|
|
|
290
292
|
};
|
|
291
293
|
const consentGiven = (config) => config.typesafe || process.env.PI_WARDEN_ENABLED === "1";
|
|
292
294
|
const consentSource = (config) => config.typesafe ? "/warden enable" : process.env.PI_WARDEN_ENABLED === "1" ? "PI_WARDEN_ENABLED" : undefined;
|
|
293
|
-
/** A consent flag is not proof that judgments happen;
|
|
295
|
+
/** A consent flag is not proof that judgments happen; check the key state for the chosen backend. */
|
|
294
296
|
const judgeFor = (config) => {
|
|
295
|
-
if (!consentGiven(config) || budgetExhausted
|
|
297
|
+
if (!consentGiven(config) || budgetExhausted)
|
|
296
298
|
return undefined;
|
|
297
|
-
|
|
299
|
+
if (!keyAvailable(config.typesafeBackend, process.env, () => authState().usable))
|
|
300
|
+
return undefined;
|
|
301
|
+
return client ??= createTypeSafe(judgeOptions(config));
|
|
298
302
|
};
|
|
299
303
|
const noteError = (ctx, message, code) => {
|
|
300
304
|
stats.errors++;
|
|
@@ -326,12 +330,18 @@ export default function wardenExtension(pi) {
|
|
|
326
330
|
}
|
|
327
331
|
const entries = [...widget].map(([guard, line]) => ({ guard, line }));
|
|
328
332
|
// A custom component so the lines wrap to the pane and a click (fullscreen mode) opens the trace panel.
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
333
|
+
// When the host TUI lacks MouseRegion (e.g. omp 18.2.5), the extension still loads — the widget
|
|
334
|
+
// renders the same text, but a click does nothing. A missing named import is a link-time error
|
|
335
|
+
// that kills the entire extension silently; a missing namespace property is undefined.
|
|
336
|
+
ctx.ui.setWidget(WIDGET, (_tui, theme) => {
|
|
337
|
+
const body = statusWidget(entries, theme);
|
|
338
|
+
return MouseRegion ? new MouseRegion(body, event => {
|
|
339
|
+
if (event.type !== "click" || event.button !== "left")
|
|
340
|
+
return undefined;
|
|
341
|
+
togglePanel(lastUi, config);
|
|
342
|
+
return { handled: true };
|
|
343
|
+
}) : body;
|
|
344
|
+
}, { placement: config.widget.placement });
|
|
335
345
|
};
|
|
336
346
|
const record = (ctx, config, guard, line, details) => {
|
|
337
347
|
widget.set(guard, line);
|
|
@@ -1138,7 +1148,7 @@ export default function wardenExtension(pi) {
|
|
|
1138
1148
|
const usage = client?.getUsage();
|
|
1139
1149
|
const guards = [config.action.enabled && "action", config.stuck.enabled && "stuck", config.done.enabled && "done-check", config.slop.enabled && "slop", config.slop.enabled && config.slop.prose.enabled && `prose (${config.slop.prose.audience})`, config.security.enabled && "security", config.rules.enabled && "rules", config.context.enabled && "context", config.runaway.enabled && "runaway", config.subagent.enabled && "subagent triage", config.notify.enabled && "desktop notifications"].filter(Boolean).join(", ");
|
|
1140
1150
|
report([
|
|
1141
|
-
`pi-warden: ${config.enabled ? `guarding ${config.action.tools.join(", ")} (${guards})` : "off"}; mode ${activeMode(config, ctx.hasUI)}; TypeSafe judgments ${source ? `consented via ${source}` : "not consented (run /warden enable)"}; ${auth.text}`,
|
|
1151
|
+
`pi-warden: ${config.enabled ? `guarding ${config.action.tools.join(", ")} (${guards})` : "off"}; mode ${activeMode(config, ctx.hasUI)}; TypeSafe judgments ${source ? `consented via ${source}` : "not consented (run /warden enable)"}${config.typesafeBackend !== "typesafe" ? ` (${config.typesafeBackend})` : ""}; ${auth.text}`,
|
|
1142
1152
|
`Session: ${stats.inspected} inspected, ${stats.judged} judged, ${stats.warned} warned, ${stats.held} held, ${stats.approved} approved on retry, ${stats.offPlan} off plan, ${stats.offTask} off task, ${stats.slop} slop notes, ${stats.ruleViolations}/${stats.ruleChecks} rule violations, ${stats.pathNotes} sensitive-path notes, ${stats.stuck}/${stats.stuckChecks} stuck, ${stats.unverified}/${stats.doneChecks} unverified done, ${stats.proseNudges}/${stats.proseChecks} prose nudges, ${stats.runaway} runaway stops, ${stats.subagentWoken}/${stats.subagentReports} subagent reports woken, ${stats.restatements} restatements, ${stats.errors} TypeSafe errors; ${usage?.requestsStarted ?? 0}/${config.maxRequests} requests. Steers are ${config.steerVisible ? "shown in the transcript" : "hidden from the transcript (trace panel shows them)"}. Steer budget: ${config.steerBudget === 0 ? "off" : `${config.steerBudget} per run`}.`,
|
|
1143
1153
|
formatSteers(stats),
|
|
1144
1154
|
`Thresholds: irreversible warn ${config.action.irreversible.warn} / hold ${config.action.irreversible.confirm}; off-task warn ${config.action.offTask.warn} / steer ${config.action.offTask.steer} (never holds); intent mismatch ${config.action.intentMismatch} (${config.action.visibleMismatch} on a visible action); stuck same-strategy ${config.stuck.sameStrategy} after ${config.stuck.minFailures} failures; done claims ${config.done.claimsDone}; slop ${config.slop.threshold}, rules ${config.rules.threshold}, prose ${config.slop.prose.threshold} in ${config.slop.prose.trend}/3 replies; runaway ${config.runaway.repeats} repeats (thinking ${config.runaway.thinkingRepeats}), recover ${config.runaway.recover}; failOpen ${config.action.failOpen}.`,
|
|
@@ -1225,7 +1235,7 @@ export default function wardenExtension(pi) {
|
|
|
1225
1235
|
}
|
|
1226
1236
|
if (action === "test") {
|
|
1227
1237
|
const judge = judgeFor(config);
|
|
1228
|
-
if (judge && ctx.hasUI && !await ctx.ui.confirm("Send one synthetic pi-warden test request?", `A synthetic action ("rm -rf /tmp/pi-warden-demo" for the task "Clean up the demo directory") goes to
|
|
1238
|
+
if (judge && ctx.hasUI && !await ctx.ui.confirm("Send one synthetic pi-warden test request?", `A synthetic action ("rm -rf /tmp/pi-warden-demo" for the task "Clean up the demo directory") goes to ${backendHost(config.typesafeBackend)} and may incur charges. ${disclosureFor(config.typesafeBackend, disclosure)}`))
|
|
1229
1239
|
return;
|
|
1230
1240
|
const verdict = await evaluateAction({ tool: "bash", input: { command: "rm -rf /tmp/pi-warden-demo" }, cwd: ctx.cwd, task: "Clean up the demo directory" }, { config: { ...config.action, enabled: true, tools: ["bash"] }, judge });
|
|
1231
1241
|
record(ctx, config, "action", formatVerdict(verdict, config.widget.action), actionDetails(verdict, { mode: activeMode(config, ctx.hasUI) }));
|