pum-agent 0.2.6-beta.1 → 0.2.7-beta.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/README.md CHANGED
@@ -223,10 +223,10 @@ Trigger events target one exact main or retained child session. A missing sessio
223
223
  Select a Check mode profile in `Ctrl+P`. It applies to `bash`, `edit`, `apply_patch`, and external-trigger process execution:
224
224
 
225
225
  - **Strict:** Run deterministic hard rules, then require a clear verifier approval.
226
- - **Balanced:** Block deterministic hard-rule or suspicious findings. Allow ordinary complete project-local calls, explicit non-sensitive external reads, and narrowly validated lifecycle-disabled `npm pack` operations whose cache and output stay in approved roots. Verifier review is non-blocking unless the verifier returns explicit `UNSAFE`.
226
+ - **Balanced:** Block deterministic hard-rule or suspicious findings. Allow ordinary complete project-local calls, explicit non-sensitive external reads, and narrow lifecycle-disabled npm release verification operations whose writes stay in approved roots. Verifier review is non-blocking unless the verifier returns explicit `UNSAFE`.
227
227
  - **Ask:** Show the approval popup for every checked call that passes hard rules, unless an exact session or project approval already matches. A verifier `SAFE`, unclear, error, or unavailable result still requires approval.
228
228
 
229
- Every active profile hard-blocks external writes, location changes, execution operands, ambiguous path access, escaping links, credential access, privilege escalation, persistence, remote-script execution, destructive Git operations, and broad deletion. Balanced permits explicit, deterministically classified, non-sensitive external reads. It also accepts one direct `npm pack` only when lifecycle scripts are disabled, an explicit cache stays in an approved root, output stays in an approved root, and any package operand is one exact registry version; file, Git, URL, tag, range, composed, and global-install forms remain blocked. These hard blocks cannot be overridden and do not open the popup. An explicit verifier `UNSAFE` verdict also blocks without a popup. The only publication exception is a deterministic match for direct main-agent `npm publish` or `npm dist-tag add`. The verifier category does not control this exception. The exception still requires explicit popup approval. Managed subagents cannot use it.
229
+ Every active profile hard-blocks external writes, location changes, execution operands, ambiguous path access, escaping links, credential access, privilege escalation, persistence, remote-script execution, destructive Git operations, and broad deletion. Balanced permits explicit, deterministically classified, non-sensitive external reads. It accepts one direct `npm pack` only when lifecycle scripts are disabled, an explicit cache stays in an approved root, output stays in an approved root, and any package operand is one exact registry version. Balanced also accepts one direct `npm install` of one exact registry version only when `--ignore-scripts`, an approved `--prefix`, and an approved `--cache` are explicit. File, Git, URL, tag, range, composed, general install, and global-install forms remain blocked. These hard blocks cannot be overridden and do not open the popup. An explicit verifier `UNSAFE` verdict also blocks without a popup. The only publication exception is a deterministic match for direct main-agent `npm publish` or `npm dist-tag add`. The verifier category does not control this exception. The exception still requires explicit popup approval. Managed subagents cannot use it.
230
230
 
231
231
  Use `/check-path list`, `/check-path add <directory>`, `/check-path remove <directory>`, or `/check-path clear` to manage up to 16 additional directory roots for the current launch project. Bash, edit, and external-trigger checks can use these roots; `apply_patch` remains project-local. Added roots are canonicalized and remain subject to credential, traversal, symlink or junction, broad-deletion, and other hard blocks.
232
232
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pum-agent",
3
- "version": "0.2.6-beta.1",
3
+ "version": "0.2.7-beta.1",
4
4
  "description": "A compact terminal coding agent powered by pi and OpenTUI.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/app.tsx CHANGED
@@ -1,7 +1,7 @@
1
1
  import { decodePasteBytes, type ScrollBoxRenderable, type TextareaRenderable } from "@opentui/core";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { useKeyboard, usePaste, useTerminalDimensions } from "@opentui/react";
4
- import type { Model } from "@earendil-works/pi-ai";
4
+ import { getSupportedThinkingLevels, type Model } from "@earendil-works/pi-ai";
5
5
  import type { AgentSession, ModelRuntime } from "@earendil-works/pi-coding-agent";
6
6
  import { Fragment, useEffect, useMemo, useRef, useState } from "react";
7
7
  import { AnimationProvider, supportsTrueColor, useWorkingRule, type WorkingRuleRole } from "./animation";
@@ -13,7 +13,6 @@ import {
13
13
  moveSettingSelection,
14
14
  SettingsPopup,
15
15
  SETTINGS_ROWS,
16
- THINKING_LEVELS,
17
16
  type SettingRowId,
18
17
  type ThinkingLevel,
19
18
  } from "./settings-popup";
@@ -1095,8 +1094,9 @@ export function App({
1095
1094
  };
1096
1095
 
1097
1096
  const stepThinking = (step: number) => {
1098
- const i = THINKING_LEVELS.indexOf(thinkingLevel);
1099
- const target = THINKING_LEVELS[Math.max(0, Math.min(THINKING_LEVELS.length - 1, i + step))]!;
1097
+ const levels = getSupportedThinkingLevels(session.agent.state.model);
1098
+ const i = levels.indexOf(thinkingLevel);
1099
+ const target = levels[Math.max(0, Math.min(levels.length - 1, i + step))]!;
1100
1100
  session.setThinkingLevel(target);
1101
1101
  // setThinkingLevel clamps to what the model supports — show the real value.
1102
1102
  setThinkingLevel(session.agent.state.thinkingLevel as ThinkingLevel);
@@ -17,6 +17,7 @@ export type CheckPolicyFindingCode =
17
17
  | "external-read-exfiltration"
18
18
  | "destructive-git"
19
19
  | "broad-deletion"
20
+ | "unsafe-npm-install"
20
21
  | "unsafe-npm-pack"
21
22
  | "suspicious-execution"
22
23
  | "shell-complexity"
@@ -219,6 +220,14 @@ type NpmPackCommand = {
219
220
  packDestination: string;
220
221
  };
221
222
 
223
+ type NpmInstallCommand = {
224
+ valid: boolean;
225
+ reason?: string;
226
+ packageSpec?: string;
227
+ prefix?: string;
228
+ cache?: string;
229
+ };
230
+
222
231
  function isExactRegistryPackageVersion(value: string): boolean {
223
232
  const packageName = String.raw`(?:@[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*|[a-z0-9][a-z0-9._-]*)`;
224
233
  const numericIdentifier = String.raw`(?:0|[1-9]\d*)`;
@@ -228,6 +237,65 @@ function isExactRegistryPackageVersion(value: string): boolean {
228
237
  return new RegExp(`^${packageName}@${version}$`).test(value);
229
238
  }
230
239
 
240
+ function npmInstallCommand(argv: string[]): NpmInstallCommand | undefined {
241
+ if (commandName(argv[0]) !== "npm" || argv[1] !== "install") return undefined;
242
+ const positionals: string[] = [];
243
+ let prefix: string | undefined;
244
+ let cache: string | undefined;
245
+ let ignoreScriptsCount = 0;
246
+ const pathOptions = new Map([["--prefix", "prefix"], ["--cache", "cache"]] as const);
247
+
248
+ for (let index = 2; index < argv.length; index++) {
249
+ const value = argv[index]!;
250
+ if (value === "--ignore-scripts") {
251
+ ignoreScriptsCount++;
252
+ continue;
253
+ }
254
+ const separate = pathOptions.get(value as "--prefix" | "--cache");
255
+ if (separate) {
256
+ const path = argv[++index];
257
+ if (!path || path.startsWith("-")) return { valid: false, reason: `${value} requires one explicit path` };
258
+ if (separate === "prefix") {
259
+ if (prefix !== undefined) return { valid: false, reason: "npm install --prefix must occur exactly once" };
260
+ prefix = path;
261
+ } else {
262
+ if (cache !== undefined) return { valid: false, reason: "npm install --cache must occur exactly once" };
263
+ cache = path;
264
+ }
265
+ continue;
266
+ }
267
+ const attached = /^(--prefix|--cache)=(.*)$/.exec(value);
268
+ if (attached) {
269
+ if (!attached[2]) return { valid: false, reason: `${attached[1]} requires one explicit path` };
270
+ if (attached[1] === "--prefix") {
271
+ if (prefix !== undefined) return { valid: false, reason: "npm install --prefix must occur exactly once" };
272
+ prefix = attached[2];
273
+ } else {
274
+ if (cache !== undefined) return { valid: false, reason: "npm install --cache must occur exactly once" };
275
+ cache = attached[2];
276
+ }
277
+ continue;
278
+ }
279
+ if (value.startsWith("-")) {
280
+ return { valid: false, reason: `npm install option ${value} is not in the deterministic allowlist` };
281
+ }
282
+ positionals.push(value);
283
+ }
284
+
285
+ if (ignoreScriptsCount !== 1) {
286
+ return { valid: false, reason: "npm install must disable lifecycle scripts exactly once with --ignore-scripts" };
287
+ }
288
+ if (!prefix) return { valid: false, reason: "npm install must set an explicit approved --prefix path" };
289
+ if (!cache) return { valid: false, reason: "npm install must set an explicit approved --cache path" };
290
+ if ([prefix, cache].some((path) => /[*?\[\]{}]/.test(path))) {
291
+ return { valid: false, reason: "npm install write paths must not contain shell expansion patterns" };
292
+ }
293
+ if (positionals.length !== 1 || !isExactRegistryPackageVersion(positionals[0]!)) {
294
+ return { valid: false, reason: "npm install must use one exact registry package version" };
295
+ }
296
+ return { valid: true, packageSpec: positionals[0], prefix, cache };
297
+ }
298
+
231
299
  function npmPackCommand(argv: string[]): NpmPackCommand | undefined {
232
300
  if (commandName(argv[0]) !== "npm" || argv[1] !== "pack") return undefined;
233
301
  const positionals: string[] = [];
@@ -294,6 +362,21 @@ function commandName(argv0: string | undefined): string {
294
362
  return basename.replace(/\.(?:exe|cmd|bat)$/i, "").toLowerCase();
295
363
  }
296
364
 
365
+ function npmSubcommand(argv: string[]): string | undefined {
366
+ if (commandName(argv[0]) !== "npm") return undefined;
367
+ const valueOptions = new Set(["--cache", "--prefix", "--registry", "--userconfig"]);
368
+ for (let index = 1; index < argv.length; index++) {
369
+ const value = argv[index]!;
370
+ if (valueOptions.has(value)) {
371
+ index++;
372
+ continue;
373
+ }
374
+ if (value.startsWith("-")) continue;
375
+ return value.toLowerCase();
376
+ }
377
+ return undefined;
378
+ }
379
+
297
380
  function effectiveArgv(argv: string[]): string[] {
298
381
  let current = argv;
299
382
  for (let depth = 0; depth < 4; depth++) {
@@ -431,6 +514,7 @@ function mutationIndicators(argv: string[], redirections: BashRedirection[]): st
431
514
  if (name === "git" && argv[1] && !new Set(["status", "diff", "log", "show", "rev-parse", "ls-files", "branch"]).has(argv[1])) {
432
515
  indicators.push("Git state change");
433
516
  }
517
+ if (npmInstallCommand(argv)?.valid) indicators.push("package installation or cache output");
434
518
  if (npmPackCommand(argv)?.valid) indicators.push("package archive or cache output");
435
519
  return [...new Set(indicators)];
436
520
  }
@@ -822,6 +906,12 @@ function classifyStageAccesses(stage: BashStage): ClassifiedAccess[] {
822
906
  }
823
907
 
824
908
  if (DATA_OPERAND_COMMANDS.has(name)) return accesses;
909
+ const npmInstall = npmInstallCommand(argv);
910
+ if (npmInstall?.valid) {
911
+ accesses.push({ path: npmInstall.prefix!, mode: "write", source: "operand" });
912
+ accesses.push({ path: npmInstall.cache!, mode: "write", source: "operand" });
913
+ return accesses;
914
+ }
825
915
  const npmPack = npmPackCommand(argv);
826
916
  if (npmPack?.valid) {
827
917
  accesses.push({ path: npmPack.cache!, mode: "write", source: "operand" });
@@ -1016,6 +1106,33 @@ function inspectHardBlocks(
1016
1106
  const argv = effectiveArgv(stage.argv);
1017
1107
  const name = commandName(argv[0]);
1018
1108
  const lowerArgs = argv.map((arg) => arg.toLowerCase());
1109
+ const npmInstall = npmInstallCommand(argv);
1110
+ if (npmInstall) {
1111
+ const direct = commandName(stage.argv[0]) === "npm"
1112
+ && analysis.stages.length === 1
1113
+ && analysis.operators.length === 0
1114
+ && stage.substitutions.length === 0
1115
+ && stage.redirections.length === 0
1116
+ && Object.keys(stage.envAssignments).length === 0;
1117
+ if (!direct || !npmInstall.valid) {
1118
+ addFinding(findings, {
1119
+ code: "unsafe-npm-install",
1120
+ severity: "hard-block",
1121
+ message: !direct ? "npm install must be one direct command without shell composition" : npmInstall.reason!,
1122
+ stage: stage.index,
1123
+ });
1124
+ }
1125
+ } else if (name === "npm" && new Set([
1126
+ "install", "i", "add", "ci", "install-ci-test", "install-test", "rebuild",
1127
+ "update", "upgrade", "remove", "uninstall", "link",
1128
+ ]).has(npmSubcommand(argv) ?? "")) {
1129
+ addFinding(findings, {
1130
+ code: "unsafe-npm-install",
1131
+ severity: "hard-block",
1132
+ message: "only the deterministic direct npm install verification form is supported",
1133
+ stage: stage.index,
1134
+ });
1135
+ }
1019
1136
  const npmPack = npmPackCommand(argv);
1020
1137
  if (npmPack) {
1021
1138
  const direct = commandName(stage.argv[0]) === "npm"
@@ -1308,6 +1425,9 @@ function stageNetworkCommand(stage: BashStage): string | undefined {
1308
1425
  if (name === "npm" && subcommand === "pack") {
1309
1426
  return npmPackCommand(argv)?.packageSpec ? "npm pack" : undefined;
1310
1427
  }
1428
+ if (name === "npm" && subcommand === "install") {
1429
+ return npmInstallCommand(argv)?.valid ? "npm install" : undefined;
1430
+ }
1311
1431
  if (["npm", "pnpm", "yarn"].includes(name)
1312
1432
  && new Set(["add", "audit", "install", "publish", "search", "update", "upgrade", "view", "info"]).has(subcommand)) {
1313
1433
  return `${name} ${subcommand}`;
@@ -32,10 +32,10 @@ export const statusTextWidth = (text: string): number => Bun.stringWidth(text);
32
32
  /** Show the launch directory without the long parent path. */
33
33
  export function formatWorkingDirectory(cwd: string): string {
34
34
  const withoutTrailingSeparators = cwd.replace(/[\\/]+$/, "");
35
- if (!withoutTrailingSeparators) return "cwd /";
36
- if (/^[A-Za-z]:$/.test(withoutTrailingSeparators)) return `cwd ${withoutTrailingSeparators}\\`;
35
+ if (!withoutTrailingSeparators) return "/";
36
+ if (/^[A-Za-z]:$/.test(withoutTrailingSeparators)) return `${withoutTrailingSeparators}\\`;
37
37
  const name = withoutTrailingSeparators.split(/[\\/]/).at(-1) || withoutTrailingSeparators;
38
- return `cwd ${name}`;
38
+ return name;
39
39
  }
40
40
 
41
41
  export function statusMetadataItems(values: StatusMetadataValues): StatusMetadataItem[] {