pi-warden 0.40.2 → 0.48.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/dist/guard.d.ts CHANGED
@@ -1,5 +1,7 @@
1
+ import type { Stats } from "node:fs";
1
2
  import type { IntegrationErrorCode, Judge, Questions } from "pi-typesafe";
2
- import type { ActionGuardConfig, ArmingRule, CommandRule, PathRule, RulesConfig, SecurityConfig, SlopGuardConfig } from "./config.js";
3
+ import type { ActionGuardConfig, ArmingRule, CommandRule, LargeOutputConfig, PathRule, RulesConfig, SecurityConfig, SlopGuardConfig } from "./config.js";
4
+ import type { TaskSpine } from "./shape.js";
3
5
  import type { RulesSourceConfig } from "./rules.js";
4
6
  export type Level = "allow" | "warn" | "confirm" | "deny";
5
7
  export type Severity = "destructive" | "risky" | "sensitive" | "deny";
@@ -63,6 +65,8 @@ export interface ActionInput {
63
65
  task?: string | undefined;
64
66
  /** Prior conversation clarifies scope, but never grants approval for a held action. */
65
67
  context?: readonly TaskMessage[] | undefined;
68
+ /** The task spine: the thread's goal and earlier user turns, so follow-ups are judged with the goal they belong to. Scope context only; it never authorizes. */
69
+ spine?: TaskSpine | undefined;
66
70
  /** The agent's own words in the message that makes this call (or its latest text under this prompt). Explains the step; never authorizes it. */
67
71
  plan?: string | undefined;
68
72
  }
@@ -109,6 +113,8 @@ export interface Judgment {
109
113
  visible?: number;
110
114
  /** P(action is safe to proceed without asking). Inverted: low = hold. */
111
115
  shouldProceed?: number;
116
+ /** P(the command prints far more than the agent needs); bash only, never holds. */
117
+ largeOutput?: number;
112
118
  model: string;
113
119
  elapsedMs: number;
114
120
  }
@@ -156,6 +162,8 @@ export interface Verdict {
156
162
  offTaskTraceOnly?: boolean;
157
163
  /** Index of the trace-only off-task diagnostic; later reasons append, and any prepend must adjust this index. */
158
164
  offTaskTraceOnlyReasonIndex?: number;
165
+ /** Command family of a bash call at or above the large-output threshold; the agent is steered once per family per session. */
166
+ largeOutputFamily?: string;
159
167
  /** Answers to the caller's own `questions`: P(yes) for a noul, the picked option for a choice, the level for a score. */
160
168
  extra?: Record<string, number | string>;
161
169
  /** Safe TypeSafe error message when the judge could not answer. */
@@ -170,6 +178,8 @@ export interface EvaluateOptions {
170
178
  signal?: AbortSignal | undefined;
171
179
  /** Adds quality questions for write/edit content to the same request. */
172
180
  slop?: SlopGuardConfig | undefined;
181
+ /** Adds the large-output question to a bash request. */
182
+ largeOutput?: LargeOutputConfig | undefined;
173
183
  security?: SecurityConfig | undefined;
174
184
  /**
175
185
  * The rules guard's switch. The rules the active config resolves ride every judged action request, so
@@ -187,6 +197,13 @@ export interface EvaluateOptions {
187
197
  * How a candidate question is measured on recorded sessions before it earns an acting rule (scripts/calibrate-action.mjs).
188
198
  */
189
199
  questions?: Questions | undefined;
200
+ /** Real paths the agent created under the temp directory in this session (`PatternOptions.scratch`). */
201
+ scratch?: ScratchRecords | undefined;
202
+ /**
203
+ * Real paths of host directories (`hostPaths()`). A write or edit in one is not held by the outside-project rule;
204
+ * every other check still applies.
205
+ */
206
+ hostPaths?: readonly string[] | undefined;
190
207
  }
191
208
  export declare const higher: (a: Level, b: Level) => Level;
192
209
  interface Rule {
@@ -196,6 +213,16 @@ interface Rule {
196
213
  test: RegExp;
197
214
  }
198
215
  export declare const SHELL_RULES: Rule[];
216
+ /**
217
+ * The command family the large-output steer is remembered by: the head of the first segment that is not a `cd`, plus the
218
+ * subcommand for tools that have one (`git log`, `npm test`, `npm run build`). Undefined when no head can be read.
219
+ */
220
+ export declare function commandFamily(command: string): string | undefined;
221
+ /**
222
+ * Whether a shell command has a segment whose effect is visible outside the working tree, decided in code before any
223
+ * request. Quoted data such as a commit message is blanked first, so a message that mentions `git push` is not a push.
224
+ */
225
+ export declare function isVisibleCommand(command: string): boolean;
199
226
  export interface ScannedCommand {
200
227
  /** The command with data text blanked; what the pattern rules read. */
201
228
  text: string;
@@ -207,12 +234,82 @@ export interface ScannedCommand {
207
234
  * (`python3 - <<EOF`) are kept when the script calls out to a shell or process API.
208
235
  */
209
236
  export declare function stripDataText(command: string): ScannedCommand;
237
+ /**
238
+ * Whether the exemption applies on this platform. It rests on birth time being a real creation time: macOS and Windows
239
+ * report one; Linux without `statx` reports ctime, which `mv` updates, so moved-in content would pass the tree walk.
240
+ * Elsewhere nothing is recorded and a recursive rm is classified as if no scratch existed.
241
+ */
242
+ export declare function scratchPlatform(platform?: NodeJS.Platform): boolean;
243
+ /** What a recorded path was when it was created. A path whose current identity differs was replaced and is not scratch. */
244
+ export interface ScratchIdentity {
245
+ dev: number;
246
+ ino: number;
247
+ birthtimeMs: number;
248
+ }
249
+ /** Real paths the agent created under the temp directory in this session, with the identity each had when recorded. */
250
+ export type ScratchRecords = ReadonlyMap<string, ScratchIdentity>;
251
+ /** The identity of a path itself (a symlink is not followed); undefined when it does not exist. */
252
+ export declare function scratchIdentity(path: string): ScratchIdentity | undefined;
253
+ /** Drops records whose path is gone or now names a different file, so a later file at the same path is not scratch. */
254
+ export declare function pruneScratch(records: Map<string, ScratchIdentity>): void;
255
+ /** Real paths of the temp roots that exist: `os.tmpdir()`, `$TMPDIR`, `/tmp`, `/private/tmp`. */
256
+ export declare function tempRoots(): string[];
257
+ /**
258
+ * The real path of an absolute path: the realpath of its deepest existing ancestor plus the missing rest. Undefined when
259
+ * a component exists but cannot be resolved (a dangling symlink, a loop, no permission).
260
+ */
261
+ export declare function realTarget(path: string): string | undefined;
262
+ /**
263
+ * Host paths: directories outside the project where the host lets its agent write, from `PI_WARDEN_HOST_PATHS` only (a
264
+ * `:`-separated list). Relative entries, empty entries, and a filesystem root are ignored. Each entry is kept as its real
265
+ * path, so a target is compared after `..` and symlinks are resolved on both sides.
266
+ */
267
+ export declare const HOST_PATHS_ENV = "PI_WARDEN_HOST_PATHS";
268
+ export declare function hostPaths(env?: NodeJS.ProcessEnv): string[];
269
+ /** The temp root a real path lies strictly under; a temp root itself has none. */
270
+ export declare function tempRootOf(real: string, roots?: string[]): string | undefined;
271
+ /** How much of the target trees one rm segment may walk: entries seen, and the clock time it must finish by. */
272
+ export interface ScratchBudget {
273
+ entries: number;
274
+ deadline: number;
275
+ }
276
+ /**
277
+ * True when every entry of the tree at `path`, the root included, has a birth time at or after `born`. Symlinks are
278
+ * checked as links and never followed. False when the root is missing, an entry has no birth time or cannot be read,
279
+ * or the walk runs past its budget: content moved in from elsewhere keeps its older birth time and must not pass.
280
+ */
281
+ export declare function bornAfter(path: string, born: number, budget?: ScratchBudget): boolean;
282
+ /**
283
+ * Real paths under a temp root that this call may create and that do not exist yet: `mkdir` targets of a shell command,
284
+ * or a written file. Each path and its missing ancestors below the temp root are listed. Taken before the call runs, so
285
+ * a directory that already existed is never recorded as created.
286
+ */
287
+ export declare function scratchCandidates(tool: string, input: Record<string, unknown>, cwd: string, platform?: NodeJS.Platform): string[];
288
+ /**
289
+ * Real paths the call created, read after it ran, each with its identity. A candidate from `scratchCandidates` counts
290
+ * when it now exists as itself (not through a symlink). For a shell command, absolute temp paths printed in its output
291
+ * also count, bounded: the first 20 such paths only, each must exist under a temp root as a real directory or file, and
292
+ * its birth time must fall in a later millisecond than `started` (`Date.now()` when the call began). Where the file
293
+ * system reports no birth time, a printed path counts only from a command that does nothing but run `mktemp` and print
294
+ * what it made (`mktempOnly`). `birthtime` reads a file's birth time in milliseconds, 0 when the file system has none.
295
+ * Nothing is recorded where `scratchPlatform` is false.
296
+ */
297
+ export declare function createdScratch(tool: string, input: Record<string, unknown>, output: string, started: number, candidates: readonly string[], birthtime?: (stats: Stats) => number, platform?: NodeJS.Platform): Map<string, ScratchIdentity>;
298
+ /**
299
+ * How many paths a command that only makes temp files may print: each segment is a bare `mktemp`, an assignment
300
+ * `name=$(mktemp …)`, or an `echo` of names assigned that way. 0 for any other command.
301
+ */
302
+ export declare function mktempOnly(command: string): number;
210
303
  /** Compiled user rules and exempt ids; passed from the config so matchPatterns stays pure. */
211
304
  export interface PatternOptions {
212
305
  commandRules?: readonly CommandRule[];
213
306
  commandDenyRules?: readonly CommandRule[];
214
307
  exemptRules?: readonly string[];
215
308
  pathRules?: readonly PathRule[];
309
+ /** Real paths the agent created under the temp directory in this session; a recursive rm of only these is not destructive. */
310
+ scratch?: ScratchRecords | undefined;
311
+ /** The platform `scratchPlatform` decides for; the running one when omitted. */
312
+ platform?: NodeJS.Platform | undefined;
216
313
  }
217
314
  /** Every id exemptRules can legitimately name: the built-in shell rules, the rm-classifier's derived ids, and the
218
315
  * sensitive-path id. Unknown ids (a typo, or a rule that never existed) are inert; this list lets the surface be
@@ -262,6 +359,10 @@ export declare const questions: {
262
359
  export declare const visibleQuestion: {
263
360
  visible: import("pi-typesafe").NoulQuestion;
264
361
  };
362
+ /** Bash only. Prevention before the call; the context saver still compresses whatever does print. Steers, never holds. */
363
+ export declare const largeOutputQuestion: {
364
+ large_output: import("pi-typesafe").NoulQuestion;
365
+ };
265
366
  /** Asked only when the agent said something before the call; an empty plan cannot be contradicted. */
266
367
  export declare const intentQuestion: {
267
368
  intent_mismatch: import("pi-typesafe").NoulQuestion;
@@ -320,6 +421,8 @@ export declare function buildRequest(summary: ActionSummary, task: string | unde
320
421
  rulesSource?: string | undefined;
321
422
  violations?: readonly Violation[] | undefined;
322
423
  floorHits?: string;
424
+ spine?: TaskSpine | undefined;
425
+ largeOutput?: boolean;
323
426
  }): {
324
427
  state: {
325
428
  floor_hits?: string;
@@ -332,6 +435,10 @@ export declare function buildRequest(summary: ActionSummary, task: string | unde
332
435
  path?: string;
333
436
  }[];
334
437
  plan?: string;
438
+ spine?: {
439
+ goal: string;
440
+ task_history: string[];
441
+ };
335
442
  task: string;
336
443
  action: Record<string, string | number | boolean>;
337
444
  context: {
@@ -348,6 +455,7 @@ export declare function buildRequest(summary: ActionSummary, task: string | unde
348
455
  slop_comments?: import("pi-typesafe").NoulQuestion;
349
456
  slop_dead?: import("pi-typesafe").NoulQuestion;
350
457
  slop_hedging?: import("pi-typesafe").NoulQuestion;
458
+ large_output?: import("pi-typesafe").NoulQuestion;
351
459
  intent_mismatch?: import("pi-typesafe").NoulQuestion;
352
460
  visible?: import("pi-typesafe").NoulQuestion;
353
461
  irreversible: import("pi-typesafe").NoulQuestion;
@@ -371,6 +479,7 @@ export declare function buildRequest(summary: ActionSummary, task: string | unde
371
479
  slop_comments?: import("pi-typesafe").NoulQuestion;
372
480
  slop_dead?: import("pi-typesafe").NoulQuestion;
373
481
  slop_hedging?: import("pi-typesafe").NoulQuestion;
482
+ large_output?: import("pi-typesafe").NoulQuestion;
374
483
  intent_mismatch?: import("pi-typesafe").NoulQuestion;
375
484
  visible?: import("pi-typesafe").NoulQuestion;
376
485
  irreversible: import("pi-typesafe").NoulQuestion;
@@ -390,6 +499,7 @@ export declare function buildRequest(summary: ActionSummary, task: string | unde
390
499
  slop_comments?: import("pi-typesafe").NoulQuestion;
391
500
  slop_dead?: import("pi-typesafe").NoulQuestion;
392
501
  slop_hedging?: import("pi-typesafe").NoulQuestion;
502
+ large_output?: import("pi-typesafe").NoulQuestion;
393
503
  intent_mismatch?: import("pi-typesafe").NoulQuestion;
394
504
  visible?: import("pi-typesafe").NoulQuestion;
395
505
  irreversible: import("pi-typesafe").NoulQuestion;
@@ -414,6 +524,10 @@ export declare function intentSteer(verdict: Verdict): string;
414
524
  export declare function shouldProceedMessage(verdict: Verdict): string;
415
525
  /** What the agent reads after an unrelated change ran: the request it drifted from, the two acceptable moves, one line. */
416
526
  export declare function offTaskSteer(verdict: Verdict): string;
527
+ /** What the agent reads the first time a command family is judged noisy: the family and one concrete way to print less. */
528
+ export declare function largeOutputSteer(verdict: Verdict): string;
529
+ /** The large-output steer for this verdict, once per command family per session; `steered` is the session's memory. */
530
+ export declare function largeOutputNotice(verdict: Verdict, steered: Set<string>): string | undefined;
417
531
  /** Offline stand-in for the approval question when TypeSafe is not available. */
418
532
  export declare function textApproves(task: string | undefined): boolean;
419
533
  /**