pi-warden 0.40.2 → 0.47.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/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,8 @@ 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;
190
202
  }
191
203
  export declare const higher: (a: Level, b: Level) => Level;
192
204
  interface Rule {
@@ -196,6 +208,11 @@ interface Rule {
196
208
  test: RegExp;
197
209
  }
198
210
  export declare const SHELL_RULES: Rule[];
211
+ /**
212
+ * The command family the large-output steer is remembered by: the head of the first segment that is not a `cd`, plus the
213
+ * subcommand for tools that have one (`git log`, `npm test`, `npm run build`). Undefined when no head can be read.
214
+ */
215
+ export declare function commandFamily(command: string): string | undefined;
199
216
  export interface ScannedCommand {
200
217
  /** The command with data text blanked; what the pattern rules read. */
201
218
  text: string;
@@ -207,12 +224,75 @@ export interface ScannedCommand {
207
224
  * (`python3 - <<EOF`) are kept when the script calls out to a shell or process API.
208
225
  */
209
226
  export declare function stripDataText(command: string): ScannedCommand;
227
+ /**
228
+ * Whether the exemption applies on this platform. It rests on birth time being a real creation time: macOS and Windows
229
+ * report one; Linux without `statx` reports ctime, which `mv` updates, so moved-in content would pass the tree walk.
230
+ * Elsewhere nothing is recorded and a recursive rm is classified as if no scratch existed.
231
+ */
232
+ export declare function scratchPlatform(platform?: NodeJS.Platform): boolean;
233
+ /** What a recorded path was when it was created. A path whose current identity differs was replaced and is not scratch. */
234
+ export interface ScratchIdentity {
235
+ dev: number;
236
+ ino: number;
237
+ birthtimeMs: number;
238
+ }
239
+ /** Real paths the agent created under the temp directory in this session, with the identity each had when recorded. */
240
+ export type ScratchRecords = ReadonlyMap<string, ScratchIdentity>;
241
+ /** The identity of a path itself (a symlink is not followed); undefined when it does not exist. */
242
+ export declare function scratchIdentity(path: string): ScratchIdentity | undefined;
243
+ /** Drops records whose path is gone or now names a different file, so a later file at the same path is not scratch. */
244
+ export declare function pruneScratch(records: Map<string, ScratchIdentity>): void;
245
+ /** Real paths of the temp roots that exist: `os.tmpdir()`, `$TMPDIR`, `/tmp`, `/private/tmp`. */
246
+ export declare function tempRoots(): string[];
247
+ /**
248
+ * The real path of an absolute path: the realpath of its deepest existing ancestor plus the missing rest. Undefined when
249
+ * a component exists but cannot be resolved (a dangling symlink, a loop, no permission).
250
+ */
251
+ export declare function realTarget(path: string): string | undefined;
252
+ /** The temp root a real path lies strictly under; a temp root itself has none. */
253
+ export declare function tempRootOf(real: string, roots?: string[]): string | undefined;
254
+ /** How much of the target trees one rm segment may walk: entries seen, and the clock time it must finish by. */
255
+ export interface ScratchBudget {
256
+ entries: number;
257
+ deadline: number;
258
+ }
259
+ /**
260
+ * True when every entry of the tree at `path`, the root included, has a birth time at or after `born`. Symlinks are
261
+ * checked as links and never followed. False when the root is missing, an entry has no birth time or cannot be read,
262
+ * or the walk runs past its budget: content moved in from elsewhere keeps its older birth time and must not pass.
263
+ */
264
+ export declare function bornAfter(path: string, born: number, budget?: ScratchBudget): boolean;
265
+ /**
266
+ * Real paths under a temp root that this call may create and that do not exist yet: `mkdir` targets of a shell command,
267
+ * or a written file. Each path and its missing ancestors below the temp root are listed. Taken before the call runs, so
268
+ * a directory that already existed is never recorded as created.
269
+ */
270
+ export declare function scratchCandidates(tool: string, input: Record<string, unknown>, cwd: string, platform?: NodeJS.Platform): string[];
271
+ /**
272
+ * Real paths the call created, read after it ran, each with its identity. A candidate from `scratchCandidates` counts
273
+ * when it now exists as itself (not through a symlink). For a shell command, absolute temp paths printed in its output
274
+ * also count, bounded: the first 20 such paths only, each must exist under a temp root as a real directory or file, and
275
+ * its birth time must fall in a later millisecond than `started` (`Date.now()` when the call began). Where the file
276
+ * system reports no birth time, a printed path counts only from a command that does nothing but run `mktemp` and print
277
+ * what it made (`mktempOnly`). `birthtime` reads a file's birth time in milliseconds, 0 when the file system has none.
278
+ * Nothing is recorded where `scratchPlatform` is false.
279
+ */
280
+ 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>;
281
+ /**
282
+ * How many paths a command that only makes temp files may print: each segment is a bare `mktemp`, an assignment
283
+ * `name=$(mktemp …)`, or an `echo` of names assigned that way. 0 for any other command.
284
+ */
285
+ export declare function mktempOnly(command: string): number;
210
286
  /** Compiled user rules and exempt ids; passed from the config so matchPatterns stays pure. */
211
287
  export interface PatternOptions {
212
288
  commandRules?: readonly CommandRule[];
213
289
  commandDenyRules?: readonly CommandRule[];
214
290
  exemptRules?: readonly string[];
215
291
  pathRules?: readonly PathRule[];
292
+ /** Real paths the agent created under the temp directory in this session; a recursive rm of only these is not destructive. */
293
+ scratch?: ScratchRecords | undefined;
294
+ /** The platform `scratchPlatform` decides for; the running one when omitted. */
295
+ platform?: NodeJS.Platform | undefined;
216
296
  }
217
297
  /** Every id exemptRules can legitimately name: the built-in shell rules, the rm-classifier's derived ids, and the
218
298
  * sensitive-path id. Unknown ids (a typo, or a rule that never existed) are inert; this list lets the surface be
@@ -262,6 +342,10 @@ export declare const questions: {
262
342
  export declare const visibleQuestion: {
263
343
  visible: import("pi-typesafe").NoulQuestion;
264
344
  };
345
+ /** Bash only. Prevention before the call; the context saver still compresses whatever does print. Steers, never holds. */
346
+ export declare const largeOutputQuestion: {
347
+ large_output: import("pi-typesafe").NoulQuestion;
348
+ };
265
349
  /** Asked only when the agent said something before the call; an empty plan cannot be contradicted. */
266
350
  export declare const intentQuestion: {
267
351
  intent_mismatch: import("pi-typesafe").NoulQuestion;
@@ -320,6 +404,8 @@ export declare function buildRequest(summary: ActionSummary, task: string | unde
320
404
  rulesSource?: string | undefined;
321
405
  violations?: readonly Violation[] | undefined;
322
406
  floorHits?: string;
407
+ spine?: TaskSpine | undefined;
408
+ largeOutput?: boolean;
323
409
  }): {
324
410
  state: {
325
411
  floor_hits?: string;
@@ -332,6 +418,10 @@ export declare function buildRequest(summary: ActionSummary, task: string | unde
332
418
  path?: string;
333
419
  }[];
334
420
  plan?: string;
421
+ spine?: {
422
+ goal: string;
423
+ task_history: string[];
424
+ };
335
425
  task: string;
336
426
  action: Record<string, string | number | boolean>;
337
427
  context: {
@@ -348,6 +438,7 @@ export declare function buildRequest(summary: ActionSummary, task: string | unde
348
438
  slop_comments?: import("pi-typesafe").NoulQuestion;
349
439
  slop_dead?: import("pi-typesafe").NoulQuestion;
350
440
  slop_hedging?: import("pi-typesafe").NoulQuestion;
441
+ large_output?: import("pi-typesafe").NoulQuestion;
351
442
  intent_mismatch?: import("pi-typesafe").NoulQuestion;
352
443
  visible?: import("pi-typesafe").NoulQuestion;
353
444
  irreversible: import("pi-typesafe").NoulQuestion;
@@ -371,6 +462,7 @@ export declare function buildRequest(summary: ActionSummary, task: string | unde
371
462
  slop_comments?: import("pi-typesafe").NoulQuestion;
372
463
  slop_dead?: import("pi-typesafe").NoulQuestion;
373
464
  slop_hedging?: import("pi-typesafe").NoulQuestion;
465
+ large_output?: import("pi-typesafe").NoulQuestion;
374
466
  intent_mismatch?: import("pi-typesafe").NoulQuestion;
375
467
  visible?: import("pi-typesafe").NoulQuestion;
376
468
  irreversible: import("pi-typesafe").NoulQuestion;
@@ -390,6 +482,7 @@ export declare function buildRequest(summary: ActionSummary, task: string | unde
390
482
  slop_comments?: import("pi-typesafe").NoulQuestion;
391
483
  slop_dead?: import("pi-typesafe").NoulQuestion;
392
484
  slop_hedging?: import("pi-typesafe").NoulQuestion;
485
+ large_output?: import("pi-typesafe").NoulQuestion;
393
486
  intent_mismatch?: import("pi-typesafe").NoulQuestion;
394
487
  visible?: import("pi-typesafe").NoulQuestion;
395
488
  irreversible: import("pi-typesafe").NoulQuestion;
@@ -414,6 +507,10 @@ export declare function intentSteer(verdict: Verdict): string;
414
507
  export declare function shouldProceedMessage(verdict: Verdict): string;
415
508
  /** What the agent reads after an unrelated change ran: the request it drifted from, the two acceptable moves, one line. */
416
509
  export declare function offTaskSteer(verdict: Verdict): string;
510
+ /** What the agent reads the first time a command family is judged noisy: the family and one concrete way to print less. */
511
+ export declare function largeOutputSteer(verdict: Verdict): string;
512
+ /** The large-output steer for this verdict, once per command family per session; `steered` is the session's memory. */
513
+ export declare function largeOutputNotice(verdict: Verdict, steered: Set<string>): string | undefined;
417
514
  /** Offline stand-in for the approval question when TypeSafe is not available. */
418
515
  export declare function textApproves(task: string | undefined): boolean;
419
516
  /**