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 +2 -2
- package/package.json +1 -1
- package/src/app.tsx +4 -4
- package/src/check-policy.ts +120 -0
- package/src/status-metadata.ts +3 -3
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
|
|
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
|
|
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
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
|
|
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
|
|
1099
|
-
const
|
|
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);
|
package/src/check-policy.ts
CHANGED
|
@@ -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}`;
|
package/src/status-metadata.ts
CHANGED
|
@@ -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 "
|
|
36
|
-
if (/^[A-Za-z]:$/.test(withoutTrailingSeparators)) return
|
|
35
|
+
if (!withoutTrailingSeparators) return "/";
|
|
36
|
+
if (/^[A-Za-z]:$/.test(withoutTrailingSeparators)) return `${withoutTrailingSeparators}\\`;
|
|
37
37
|
const name = withoutTrailingSeparators.split(/[\\/]/).at(-1) || withoutTrailingSeparators;
|
|
38
|
-
return
|
|
38
|
+
return name;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
export function statusMetadataItems(values: StatusMetadataValues): StatusMetadataItem[] {
|