pum-agent 0.2.1-beta.1 → 0.2.3-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 +44 -10
- package/package.json +3 -2
- package/src/app.tsx +82 -27
- package/src/check-mode.ts +4 -1
- package/src/check-mutation.ts +6 -1
- package/src/check-policy.ts +414 -24
- package/src/help-popup.tsx +6 -1
- package/src/main.tsx +16 -0
- package/src/message-cache.ts +41 -10
- package/src/sandbox/index.ts +207 -0
- package/src/sandbox/linux.ts +362 -0
- package/src/sandbox/types.ts +62 -0
- package/src/sandbox/windows.ts +330 -0
- package/src/sandbox-policy.ts +204 -0
- package/src/settings-popup.tsx +2 -0
- package/src/settings.ts +10 -0
- package/src/stash-batch.ts +3 -1
- package/src/subagents/manager.ts +41 -6
- package/src/terminal-title.ts +41 -0
- package/src/theme.ts +9 -9
- package/src/transcript.tsx +14 -3
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ pum
|
|
|
23
23
|
PUM opens the login panel automatically on the first start.
|
|
24
24
|
|
|
25
25
|
> [!WARNING]
|
|
26
|
-
> PUM can read, write, and delete files.
|
|
26
|
+
> PUM can read, write, and delete files. Check mode adds deterministic policy checks and can enforce native Bash isolation on supported Linux and Windows hosts. Other tools, extensions, and external triggers still run in the PUM process boundary described below. Review the safeguards and prerequisites before using untrusted workspaces.
|
|
27
27
|
|
|
28
28
|
## See PUM in action
|
|
29
29
|
|
|
@@ -46,7 +46,7 @@ The following screens are real OpenTUI renders captured through `tmux`. A local
|
|
|
46
46
|
- **Prompt control:** Steer active work, answer model questionnaires, use an ownership-aware message cache, attach clipboard images, and resume sessions with metadata-rich history.
|
|
47
47
|
- **External triggers:** Supervise background commands such as `gh run watch` and automatically wake the exact target agent when they exit.
|
|
48
48
|
- **Provider choice:** Search the providers exposed by pi, or add an OpenAI-compatible custom endpoint.
|
|
49
|
-
- **Optional safeguards:** Use strict, balanced, or ask Check mode
|
|
49
|
+
- **Optional safeguards:** Use strict, balanced, or ask Check mode, plus native Bash sandboxing through Bubblewrap or Windows CreateProcessInSandbox when available.
|
|
50
50
|
- **Terminal-first appearance:** Nine themes, semantic color overrides, Unicode glyphs, and optional animation.
|
|
51
51
|
|
|
52
52
|
PUM uses [pi](https://github.com/earendil-works/pi) for the agent loop and [OpenTUI](https://github.com/anomalyco/opentui) for rendering.
|
|
@@ -60,7 +60,22 @@ PUM uses [pi](https://github.com/earendil-works/pi) for the agent loop and [Open
|
|
|
60
60
|
|
|
61
61
|
Linux and macOS are the primary environments. Windows CI checks the code and Windows path behavior. Native Windows TUI operation remains provisional because it has not been fully validated in a Windows terminal.
|
|
62
62
|
|
|
63
|
-
On
|
|
63
|
+
On Linux, native Bash sandboxing requires Bubblewrap (`bwrap`) and working unprivileged user namespaces. PUM probes a minimal sandbox launch; finding the executable alone is not sufficient. On Arch Linux, install the prerequisite separately with `sudo pacman -S --needed bubblewrap`.
|
|
64
|
+
|
|
65
|
+
On Windows, install Git for Windows. Ensure that `bash.exe` is in `PATH` or remains in its standard location. Use Windows Terminal with PowerShell. Do not use PowerShell ISE. Native Bash sandboxing uses the optional alpha `@microsoft/mxc-sdk` package and requires its `base-container` CreateProcessInSandbox tier. PUM deliberately rejects the SDK's AppContainer+DACL fallback because it can modify host ACLs.
|
|
66
|
+
|
|
67
|
+
### Terminal title
|
|
68
|
+
|
|
69
|
+
PUM sets a compact terminal title such as `Pum · working · 2 subagents`. The title reports overall activity and counts only starting or running subagents. PUM clears the title during graceful shutdown.
|
|
70
|
+
|
|
71
|
+
Windows Terminal and common Linux terminal emulators accept the title through OpenTUI. Inside `tmux`, PUM sets the active pane title. To copy that pane title to the outer terminal title, add this configuration:
|
|
72
|
+
|
|
73
|
+
```tmux
|
|
74
|
+
set -g set-titles on
|
|
75
|
+
set -g set-titles-string '#T'
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Keep `allow-set-title` enabled so applications can update the pane title. A `tmux` configuration can replace or suppress application titles. PUM cannot override that server policy.
|
|
64
79
|
|
|
65
80
|
## Install and start
|
|
66
81
|
|
|
@@ -123,6 +138,7 @@ Set `PUM_DIR` to override PUM's complete configuration and data directory. Run `
|
|
|
123
138
|
| `Ctrl+L` | Open the agent transcript selector |
|
|
124
139
|
| `Shift+Tab` / `Ctrl+Shift+Tab` | Cycle through agent transcripts |
|
|
125
140
|
| `Ctrl+H` | Open session history when the terminal reports the key distinctly |
|
|
141
|
+
| `Ctrl+End` | Scroll to the end of the selected transcript |
|
|
126
142
|
| `Ctrl+P` | Open settings |
|
|
127
143
|
| `Ctrl+T` | Open supervised external triggers |
|
|
128
144
|
| `Esc` twice | Cancel the selected working agent |
|
|
@@ -162,7 +178,7 @@ PUM runs up to 10 active subagents by default. Configure a limit from 1 through
|
|
|
162
178
|
- Its own transcript, draft, usage data, and cancellation state
|
|
163
179
|
- Tools for progress messages and a single final completion report
|
|
164
180
|
|
|
165
|
-
Select a range of stashed prompts and press `Enter`. The main agent can group related work and run independent groups in parallel. Successful managed merges remove the completed worktree and branch. A parent cannot finish, merge, or be removed until every retained descendant closes deepest-first.
|
|
181
|
+
Select a range of stashed prompts and press `Enter`. The main agent can group related work and run independent groups in parallel. A managed merge requires both authoritative `completed` status and a persisted completion notice. Idle settlement is not completion. Successful managed merges remove the completed worktree and branch. A parent cannot finish, merge, or be removed until every retained descendant closes deepest-first.
|
|
166
182
|
|
|
167
183
|
Use `Ctrl+L` to select an agent transcript. Input then goes to that agent. Finished or interrupted agents remain available until PUM merges or removes them.
|
|
168
184
|
|
|
@@ -170,7 +186,7 @@ The public `spawn_subagent` tool accepts `preview: true`. PUM then shows the exa
|
|
|
170
186
|
|
|
171
187
|
Press `↑` on an empty single-line prompt to recall the newest queued user-authored message for the selected transcript. PUM removes the message from the authoritative queue before restoring its text. PUM does not recall inter-agent, trigger, lifecycle, cache, delivered, or image-bearing messages.
|
|
172
188
|
|
|
173
|
-
Idle notices report settled work cycles to the direct spawner. They are not completion notices. PUM
|
|
189
|
+
Idle notices report settled work cycles to the direct spawner. They are not completion notices. PUM acknowledges completion delivery only after the notice enters the parent session. Persisted completion intent and stable message identifiers let interrupted delivery resume without duplicate completion messages.
|
|
174
190
|
|
|
175
191
|
## Tools and safeguards
|
|
176
192
|
|
|
@@ -186,7 +202,7 @@ Main and managed child agents can list and read the current workspace message ca
|
|
|
186
202
|
|
|
187
203
|
Agents can add entries. An agent can delete only entries created by that exact agent. User-created and legacy entries remain user-owned.
|
|
188
204
|
|
|
189
|
-
The `message_cache_send` tool accepts stable entry IDs. Single entries use the selected agent delivery path. Multiple entries use main-agent worktree orchestration.
|
|
205
|
+
The `message_cache_send` tool accepts stable entry IDs. Single entries use the selected agent delivery path. Multiple entries use main-agent worktree orchestration. PUM reserves selected entries during delivery and marks them executed only after delivery succeeds. Failed main or child delivery leaves the entries pending.
|
|
190
206
|
|
|
191
207
|
### External triggers
|
|
192
208
|
|
|
@@ -205,10 +221,10 @@ Trigger events target one exact main or retained child session. A missing sessio
|
|
|
205
221
|
Select a Check mode profile in `Ctrl+P`. It applies to `bash`, `edit`, `apply_patch`, and external-trigger process execution:
|
|
206
222
|
|
|
207
223
|
- **Strict:** Run deterministic hard rules, then require a clear verifier approval.
|
|
208
|
-
- **Balanced:** Block deterministic hard-rule or suspicious findings. Allow ordinary complete project-local calls. Verifier review is non-blocking unless the verifier returns explicit `UNSAFE`.
|
|
224
|
+
- **Balanced:** Block deterministic hard-rule or suspicious findings. Allow ordinary complete project-local calls and explicit non-sensitive external reads. Verifier review is non-blocking unless the verifier returns explicit `UNSAFE`.
|
|
209
225
|
- **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.
|
|
210
226
|
|
|
211
|
-
Every active profile hard-blocks
|
|
227
|
+
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 only explicit, deterministically classified, non-sensitive external reads. These hard blocks cannot be overridden and do not open the popup. An explicit verifier `UNSAFE` verdict also blocks without a popup. The only 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 the exception.
|
|
212
228
|
|
|
213
229
|
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.
|
|
214
230
|
|
|
@@ -216,7 +232,25 @@ For `edit` and `apply_patch`, PUM validates the complete proposed change before
|
|
|
216
232
|
|
|
217
233
|
Ask mode can allow an exact call once, for the current session, or for the current project. Approvals match the authoritative main or child identity, tool, verifier model, project, and canonical complete input. Chat text is not approval. Use **Clear approvals** in Settings to remove project approvals.
|
|
218
234
|
|
|
219
|
-
The verifier uses a structured decision schema. One unclear response can receive one adjudication under the shared 15-second watchdog. Strict blocks malformed replies, errors, aborts, and timeouts. Balanced allows a fully validated call after an unclear, unavailable, failed, or timed-out review. Balanced still blocks explicit verifier `UNSAFE`, aborts, deterministic suspicious findings, malformed structures, and incomplete analysis. Ask requires the popup after hard rules for verifier `SAFE`, unclear, error, and unavailable results. Check mode is off by default
|
|
235
|
+
The verifier uses a structured decision schema. One unclear response can receive one adjudication under the shared 15-second watchdog. Strict blocks malformed replies, errors, aborts, and timeouts. Balanced allows a fully validated call after an unclear, unavailable, failed, or timed-out review. Balanced still blocks explicit verifier `UNSAFE`, aborts, deterministic suspicious findings, malformed structures, and incomplete analysis. Ask requires the popup after hard rules for verifier `SAFE`, unclear, error, and unavailable results. Check mode is off by default.
|
|
236
|
+
|
|
237
|
+
#### Native Bash sandbox
|
|
238
|
+
|
|
239
|
+
The **Sandbox** setting has three modes:
|
|
240
|
+
|
|
241
|
+
- **Auto:** Enforce the platform sandbox for Bash when available. If probing fails, retain deterministic Check mode and show one process-local warning that is not written to session context.
|
|
242
|
+
- **Require:** Block checked Bash calls unless native enforcement is available.
|
|
243
|
+
- **Off:** Do not sandbox Bash. Check mode policy and approval behavior remain unchanged.
|
|
244
|
+
|
|
245
|
+
Check mode **Off** always uses pi's normal unsandboxed Bash backend. For an active Check mode, PUM recomputes the sandbox policy from the exact approved command, authoritative working directory, configured additional roots, and deterministic access analysis. Model input cannot supply policy fields.
|
|
246
|
+
|
|
247
|
+
The project and configured additional roots are writable. Explicit Balanced external reads are mounted read-only. PUM configuration and common credential paths are denied, and credential-shaped or process-injection environment variables are removed. A private temporary directory is supplied for the command. Safe pi metadata such as `PI_PROVIDER`, `PI_MODEL`, and `PI_REASONING_LEVEL` remains available; session paths and identifiers are withheld.
|
|
248
|
+
|
|
249
|
+
Network access is denied unless deterministic analysis recognizes an approved network operation. Bubblewrap's host-network mode is all-or-nothing and is **not domain-filtered**. Windows similarly grants or withholds the SDK's broad network capabilities; it does not provide hostname allowlists.
|
|
250
|
+
|
|
251
|
+
The override uses pi's `createBashTool` implementation and custom Bash operations, preserving streaming, truncation, full-output files, rendering, timeout messages, abort handling, shell configuration, and child-tree cleanup. Only Bash commands are routed through this backend. PUM does not sandbox the TUI/model process itself.
|
|
252
|
+
|
|
253
|
+
External triggers preserve direct executable/argument boundaries and continue to use deterministic Check mode, but they are not routed through the native sandbox in this release. The trigger manager's synchronous spawn boundary does not carry the exact approved policy object into execution; silently recomputing a second process policy there would weaken approval identity. Trigger output, environment, limits, and process supervision remain unchanged.
|
|
220
254
|
|
|
221
255
|
Verifier prompts stay bounded. For an oversized Balanced review, PUM sends complete validation metadata, counts, findings, and SHA-256 digests. PUM does not send a raw prefix or suffix as if it were complete. Strict and Ask keep their fail-closed oversized-input behavior.
|
|
222
256
|
|
|
@@ -254,7 +288,7 @@ Set `PUM_DIR` to override the complete PUM data directory.
|
|
|
254
288
|
| `auth.json` | Provider credentials and custom-provider keys |
|
|
255
289
|
| `models.json` | Custom endpoints and model metadata; submitted keys are not stored here |
|
|
256
290
|
| `settings.json` | Model and thinking level managed by pi |
|
|
257
|
-
| `pum.json` | Theme, animation, search, writing, explanation, and
|
|
291
|
+
| `pum.json` | Theme, animation, search, writing, explanation, Check mode, sandbox, and subagent settings |
|
|
258
292
|
| `theme.json` | Optional semantic color overrides |
|
|
259
293
|
| `history.json` | Prompt history by working directory |
|
|
260
294
|
| `prompt-stash.json` | Stashed prompts by working directory |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pum-agent",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3-beta.1",
|
|
4
4
|
"description": "A compact terminal coding agent powered by pi and OpenTUI.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -60,7 +60,8 @@
|
|
|
60
60
|
"ws": "^8.21.3"
|
|
61
61
|
},
|
|
62
62
|
"optionalDependencies": {
|
|
63
|
-
"@mariozechner/clipboard": "0.3.9"
|
|
63
|
+
"@mariozechner/clipboard": "0.3.9",
|
|
64
|
+
"@microsoft/mxc-sdk": "^0.7.0"
|
|
64
65
|
},
|
|
65
66
|
"devDependencies": {
|
|
66
67
|
"@types/bun": "^1.3.14",
|
package/src/app.tsx
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
checkPathsForProject,
|
|
23
23
|
MAX_ACTIVE_SUBAGENTS,
|
|
24
24
|
MIN_ACTIVE_SUBAGENTS,
|
|
25
|
+
SANDBOX_MODES,
|
|
25
26
|
saveSettings,
|
|
26
27
|
WORKING_RULE_ANIMATION_MODES,
|
|
27
28
|
type PumSettings,
|
|
@@ -81,7 +82,7 @@ import {
|
|
|
81
82
|
removePendingImage,
|
|
82
83
|
type PendingImage,
|
|
83
84
|
} from "./image-paste";
|
|
84
|
-
import type
|
|
85
|
+
import { countActiveSubagents, type SubagentManager } from "./subagents/manager";
|
|
85
86
|
import type { SpawnPreviewManager } from "./subagents/spawn-preview";
|
|
86
87
|
import { SpawnPreviewPopup } from "./subagents/spawn-preview-popup";
|
|
87
88
|
import { recallNewestQueuedUserMessage } from "./queue-recall";
|
|
@@ -119,6 +120,7 @@ import {
|
|
|
119
120
|
type TriggerAction,
|
|
120
121
|
type TriggerManagerLike,
|
|
121
122
|
} from "./triggers/popup";
|
|
123
|
+
import type { TerminalTitleController } from "./terminal-title";
|
|
122
124
|
|
|
123
125
|
type Stream = { kind: "assistant" | "thinking"; text: string } | null;
|
|
124
126
|
type Transcript = { lines: Line[]; stream: Stream; pending: PendingLine[] };
|
|
@@ -344,6 +346,10 @@ export function App({
|
|
|
344
346
|
checkApprovalStore,
|
|
345
347
|
triggerManager,
|
|
346
348
|
messageCacheController,
|
|
349
|
+
terminalTitle,
|
|
350
|
+
startupWarnings = [],
|
|
351
|
+
onSandboxModeChange,
|
|
352
|
+
sandboxWarningSource,
|
|
347
353
|
}: {
|
|
348
354
|
session: AgentSession;
|
|
349
355
|
modelRuntime: ModelRuntime;
|
|
@@ -365,16 +371,24 @@ export function App({
|
|
|
365
371
|
checkApprovalStore?: CheckApprovalStore;
|
|
366
372
|
triggerManager?: TriggerManagerLike;
|
|
367
373
|
messageCacheController?: MessageCacheController;
|
|
374
|
+
terminalTitle?: TerminalTitleController;
|
|
375
|
+
/** Visible process-local warnings. These lines never enter pi session context. */
|
|
376
|
+
startupWarnings?: readonly string[];
|
|
377
|
+
onSandboxModeChange?: (mode: NonNullable<PumSettings["sandboxMode"]>) => void;
|
|
378
|
+
sandboxWarningSource?: { subscribeWarnings(listener: (warning: string) => void): () => void };
|
|
368
379
|
}) {
|
|
369
380
|
const cwd = process.cwd();
|
|
370
381
|
const [session, setSession] = useState(initialSession);
|
|
371
382
|
const [tx, setTx] = useState<Transcript>(() => ({
|
|
372
383
|
// A resumed session already holds messages; show them instead of a blank pane.
|
|
373
|
-
lines:
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
384
|
+
lines: [
|
|
385
|
+
...replayEntries(
|
|
386
|
+
initialSession.sessionManager.buildContextEntries(),
|
|
387
|
+
cwd,
|
|
388
|
+
initial.showThinking,
|
|
389
|
+
),
|
|
390
|
+
...startupWarnings.map((text): Line => ({ kind: "text", role: "system", text })),
|
|
391
|
+
],
|
|
378
392
|
stream: null,
|
|
379
393
|
pending: [],
|
|
380
394
|
}));
|
|
@@ -436,6 +450,7 @@ export function App({
|
|
|
436
450
|
const syntaxStyle = useMemo(() => buildSyntaxStyle(theme), [theme]);
|
|
437
451
|
const animations = settings.animations && supportsTrueColor();
|
|
438
452
|
const agents = subagentManager.getAgents();
|
|
453
|
+
const activeSubagentCount = countActiveSubagents(agents);
|
|
439
454
|
const activeAgent = activeAgentId
|
|
440
455
|
? agents.find((agent) => agent.id === activeAgentId)
|
|
441
456
|
: undefined;
|
|
@@ -471,6 +486,7 @@ export function App({
|
|
|
471
486
|
), [modelRuntime, modelId, modelQuery, loginPage]);
|
|
472
487
|
|
|
473
488
|
const inputRef = useRef<TextareaRenderable>(null);
|
|
489
|
+
const transcriptScrollRef = useRef<ScrollBoxRenderable>(null);
|
|
474
490
|
const questionnaireInputRef = useRef<TextareaRenderable>(null);
|
|
475
491
|
const spawnPreviewInputRef = useRef<TextareaRenderable>(null);
|
|
476
492
|
const settingsOpenRef = useRef(settingsOpen);
|
|
@@ -553,6 +569,7 @@ export function App({
|
|
|
553
569
|
};
|
|
554
570
|
// The event subscription is set up once, so it reads the toggle via a ref.
|
|
555
571
|
const showThinkingRef = useRef(initial.showThinking);
|
|
572
|
+
const startupWarningsRef = useRef([...startupWarnings]);
|
|
556
573
|
const sessionRef = useRef(session);
|
|
557
574
|
sessionRef.current = session;
|
|
558
575
|
const loginControllerRef = useRef<LoginController | null>(null);
|
|
@@ -794,6 +811,10 @@ export function App({
|
|
|
794
811
|
return pending ? resolvePendingDelivery(value, pending.id) : value;
|
|
795
812
|
});
|
|
796
813
|
|
|
814
|
+
useEffect(() => sandboxWarningSource?.subscribeWarnings((warning) => {
|
|
815
|
+
append({ kind: "text", role: "system", text: warning });
|
|
816
|
+
}), [sandboxWarningSource]);
|
|
817
|
+
|
|
797
818
|
useEffect(() => checkApprovalCoordinator?.subscribe((request) => {
|
|
798
819
|
setCheckApproval(request);
|
|
799
820
|
setCheckApprovalDecision("allowOnce");
|
|
@@ -818,6 +839,13 @@ export function App({
|
|
|
818
839
|
[subagentManager],
|
|
819
840
|
);
|
|
820
841
|
|
|
842
|
+
useEffect(() => {
|
|
843
|
+
terminalTitle?.update({
|
|
844
|
+
working: busy || activeSubagentCount > 0,
|
|
845
|
+
activeSubagentCount,
|
|
846
|
+
});
|
|
847
|
+
}, [terminalTitle, busy, activeSubagentCount]);
|
|
848
|
+
|
|
821
849
|
useEffect(() => spawnPreviewManager?.subscribe(() => {
|
|
822
850
|
setSpawnPreviewRevision((revision) => revision + 1);
|
|
823
851
|
}), [spawnPreviewManager]);
|
|
@@ -865,8 +893,13 @@ export function App({
|
|
|
865
893
|
.catch((error) => append({ kind: "text", role: "error", text: String(error) }));
|
|
866
894
|
setThinkingLevel(session.agent.state.thinkingLevel as ThinkingLevel);
|
|
867
895
|
setModelId(session.agent.state.model.id);
|
|
896
|
+
const visibleStartupWarnings = startupWarningsRef.current;
|
|
897
|
+
startupWarningsRef.current = [];
|
|
868
898
|
setTx({
|
|
869
|
-
lines:
|
|
899
|
+
lines: [
|
|
900
|
+
...replayEntries(session.sessionManager.buildContextEntries(), cwd, showThinkingRef.current),
|
|
901
|
+
...visibleStartupWarnings.map((text): Line => ({ kind: "text", role: "system", text })),
|
|
902
|
+
],
|
|
870
903
|
stream: null,
|
|
871
904
|
pending: [],
|
|
872
905
|
});
|
|
@@ -1029,6 +1062,7 @@ export function App({
|
|
|
1029
1062
|
if (patch.explanationStrength !== undefined) {
|
|
1030
1063
|
setExplanationStrength(patch.explanationStrength);
|
|
1031
1064
|
}
|
|
1065
|
+
if (patch.sandboxMode !== undefined) onSandboxModeChange?.(patch.sandboxMode);
|
|
1032
1066
|
if (patch.checkMode !== undefined || patch.checkModel !== undefined || patch.checkPaths !== undefined) {
|
|
1033
1067
|
setCheckModeConfig({
|
|
1034
1068
|
profile: next.checkMode,
|
|
@@ -1328,12 +1362,12 @@ export function App({
|
|
|
1328
1362
|
return true;
|
|
1329
1363
|
};
|
|
1330
1364
|
|
|
1331
|
-
const deliverMainPrompt = (
|
|
1365
|
+
const deliverMainPrompt = async (
|
|
1332
1366
|
promptText: string,
|
|
1333
1367
|
displayText: string,
|
|
1334
1368
|
images: ReturnType<typeof imageContent>[] = [],
|
|
1335
1369
|
recallable = images.length === 0,
|
|
1336
|
-
) => {
|
|
1370
|
+
): Promise<void> => {
|
|
1337
1371
|
const userLine: Extract<Line, { kind: "text" }> = {
|
|
1338
1372
|
kind: "text",
|
|
1339
1373
|
role: "user",
|
|
@@ -1350,20 +1384,26 @@ export function App({
|
|
|
1350
1384
|
recallable,
|
|
1351
1385
|
};
|
|
1352
1386
|
addPending(pending);
|
|
1353
|
-
|
|
1387
|
+
try {
|
|
1388
|
+
await withSearchRoute(session.sessionId, () => session.steer(promptText, images));
|
|
1389
|
+
} catch (error) {
|
|
1354
1390
|
dropPending(pending.id);
|
|
1355
1391
|
append({ kind: "text", role: "error", text: String(error) });
|
|
1356
|
-
|
|
1392
|
+
throw error;
|
|
1393
|
+
}
|
|
1357
1394
|
return;
|
|
1358
1395
|
}
|
|
1359
1396
|
|
|
1360
1397
|
append(userLine);
|
|
1361
1398
|
inFlight.current = promptText;
|
|
1362
1399
|
setWorking(true);
|
|
1363
|
-
|
|
1400
|
+
try {
|
|
1401
|
+
await withSearchRoute(session.sessionId, () => session.prompt(promptText, { images }));
|
|
1402
|
+
} catch (error) {
|
|
1364
1403
|
append({ kind: "text", role: "error", text: String(error) });
|
|
1365
1404
|
setWorking(false);
|
|
1366
|
-
|
|
1405
|
+
throw error;
|
|
1406
|
+
}
|
|
1367
1407
|
};
|
|
1368
1408
|
|
|
1369
1409
|
const submitPrompt = (value?: string, stashIndex?: number) => {
|
|
@@ -1410,7 +1450,7 @@ export function App({
|
|
|
1410
1450
|
histCursor.current = null;
|
|
1411
1451
|
draft.current = "";
|
|
1412
1452
|
setSelectedStash(-1);
|
|
1413
|
-
deliverMainPrompt(promptText, displayText, images);
|
|
1453
|
+
void deliverMainPrompt(promptText, displayText, images).catch(() => {});
|
|
1414
1454
|
};
|
|
1415
1455
|
|
|
1416
1456
|
const cachedBatchDisplay = (prompts: readonly string[]): string => [
|
|
@@ -1444,7 +1484,7 @@ export function App({
|
|
|
1444
1484
|
setStash(next);
|
|
1445
1485
|
refreshHistoryAfterStashMutation();
|
|
1446
1486
|
resetAfterCacheExecution();
|
|
1447
|
-
deliverMainPrompt(buildStashBatchPrompt(prompts), cachedBatchDisplay(prompts), [], false);
|
|
1487
|
+
void deliverMainPrompt(buildStashBatchPrompt(prompts), cachedBatchDisplay(prompts), [], false).catch(() => {});
|
|
1448
1488
|
};
|
|
1449
1489
|
|
|
1450
1490
|
useEffect(() => {
|
|
@@ -1460,24 +1500,22 @@ export function App({
|
|
|
1460
1500
|
const detach = messageCacheController.bindExecutor(
|
|
1461
1501
|
session.sessionId,
|
|
1462
1502
|
async (request: MessageCacheSendRequest): Promise<MessageCacheSendResult> => {
|
|
1463
|
-
const
|
|
1464
|
-
stashRef.current = state.stash;
|
|
1465
|
-
setStash(state.stash);
|
|
1466
|
-
history.current = state.history;
|
|
1467
|
-
resetAfterCacheExecution(
|
|
1468
|
-
request.requester.kind === "subagent" ? request.requester.id : null,
|
|
1469
|
-
);
|
|
1470
|
-
const prompts = entries.map((entry) => entry.text);
|
|
1503
|
+
const prompts = request.entries.map((entry) => entry.text);
|
|
1471
1504
|
if (prompts.length > 1) {
|
|
1472
|
-
deliverMainPrompt(buildStashBatchPrompt(prompts), cachedBatchDisplay(prompts), [], false);
|
|
1505
|
+
await deliverMainPrompt(buildStashBatchPrompt(prompts), cachedBatchDisplay(prompts), [], false);
|
|
1506
|
+
resetAfterCacheExecution(
|
|
1507
|
+
request.requester.kind === "subagent" ? request.requester.id : null,
|
|
1508
|
+
);
|
|
1473
1509
|
return { count: prompts.length, route: "main" };
|
|
1474
1510
|
}
|
|
1475
1511
|
const prompt = prompts[0]!;
|
|
1476
1512
|
if (request.requester.kind === "subagent") {
|
|
1477
1513
|
await subagentManager.sendUserMessage(request.requester.id, prompt, [], prompt, false);
|
|
1514
|
+
resetAfterCacheExecution(request.requester.id);
|
|
1478
1515
|
return { count: 1, route: "subagent" };
|
|
1479
1516
|
}
|
|
1480
|
-
deliverMainPrompt(prompt, prompt, [], false);
|
|
1517
|
+
await deliverMainPrompt(prompt, prompt, [], false);
|
|
1518
|
+
resetAfterCacheExecution(null);
|
|
1481
1519
|
return { count: 1, route: "main" };
|
|
1482
1520
|
},
|
|
1483
1521
|
);
|
|
@@ -1519,6 +1557,12 @@ export function App({
|
|
|
1519
1557
|
update({ checkMode: CHECK_MODE_PROFILES[(index + step + CHECK_MODE_PROFILES.length) % CHECK_MODE_PROFILES.length]! });
|
|
1520
1558
|
};
|
|
1521
1559
|
|
|
1560
|
+
const stepSandboxMode = (step: number) => {
|
|
1561
|
+
const current = settings.sandboxMode ?? "auto";
|
|
1562
|
+
const index = SANDBOX_MODES.indexOf(current);
|
|
1563
|
+
update({ sandboxMode: SANDBOX_MODES[(index + step + SANDBOX_MODES.length) % SANDBOX_MODES.length]! });
|
|
1564
|
+
};
|
|
1565
|
+
|
|
1522
1566
|
const rowActions: Record<SettingRowId, { step?: (n: number) => void; enter?: () => void }> = {
|
|
1523
1567
|
theme: { step: stepTheme },
|
|
1524
1568
|
providers: { enter: openLogin },
|
|
@@ -1528,6 +1572,7 @@ export function App({
|
|
|
1528
1572
|
writingStyle: { step: stepWritingStyle },
|
|
1529
1573
|
explanationStrength: { step: stepExplanationStrength },
|
|
1530
1574
|
checkMode: { step: stepCheckMode },
|
|
1575
|
+
sandboxMode: { step: stepSandboxMode },
|
|
1531
1576
|
checkModel: { enter: () => {
|
|
1532
1577
|
setModelQuery("");
|
|
1533
1578
|
setModelSearchFocused(false);
|
|
@@ -1575,6 +1620,7 @@ export function App({
|
|
|
1575
1620
|
writingStyle: `‹ ${settings.writingStyle} ›`,
|
|
1576
1621
|
explanationStrength: `‹ ${settings.explanationStrength} ›`,
|
|
1577
1622
|
checkMode: `‹ ${settings.checkMode} ›`,
|
|
1623
|
+
sandboxMode: `‹ ${settings.sandboxMode ?? "auto"} ›`,
|
|
1578
1624
|
checkModel: `${settings.checkModel} ›`,
|
|
1579
1625
|
checkPaths: `${checkPathsForProject(settings, cwd).length} additional · /check-path ›`,
|
|
1580
1626
|
clearCheckApprovals: "clear ›",
|
|
@@ -1948,6 +1994,13 @@ export function App({
|
|
|
1948
1994
|
return;
|
|
1949
1995
|
}
|
|
1950
1996
|
|
|
1997
|
+
if (key.ctrl && key.name === "end") {
|
|
1998
|
+
key.stopPropagation();
|
|
1999
|
+
const transcriptScroll = transcriptScrollRef.current;
|
|
2000
|
+
if (transcriptScroll) transcriptScroll.scrollTop = transcriptScroll.scrollHeight;
|
|
2001
|
+
return;
|
|
2002
|
+
}
|
|
2003
|
+
|
|
1951
2004
|
const isAgentCycle =
|
|
1952
2005
|
(key.name === "tab" && key.shift) ||
|
|
1953
2006
|
key.name === "backtab" ||
|
|
@@ -2236,7 +2289,7 @@ export function App({
|
|
|
2236
2289
|
busy={visibleBusy}
|
|
2237
2290
|
elapsedSec={visibleElapsedSec}
|
|
2238
2291
|
agentCount={agents.length}
|
|
2239
|
-
runningAgentCount={
|
|
2292
|
+
runningAgentCount={activeSubagentCount}
|
|
2240
2293
|
maxActiveAgentCount={settings.maxActiveSubagents}
|
|
2241
2294
|
activeAgentName={activeAgent?.name}
|
|
2242
2295
|
/>
|
|
@@ -2249,6 +2302,8 @@ export function App({
|
|
|
2249
2302
|
/>
|
|
2250
2303
|
<scrollbox
|
|
2251
2304
|
key={activeAgentId ?? "main"}
|
|
2305
|
+
ref={transcriptScrollRef}
|
|
2306
|
+
id="transcript-scrollbox"
|
|
2252
2307
|
style={{ flexGrow: 1, paddingLeft: 1, paddingRight: 1 }}
|
|
2253
2308
|
stickyScroll
|
|
2254
2309
|
stickyStart="bottom"
|
|
@@ -2389,7 +2444,7 @@ export function App({
|
|
|
2389
2444
|
textColor={theme.fg}
|
|
2390
2445
|
cursorColor={theme.accent}
|
|
2391
2446
|
selectionBg={theme.selectionBg}
|
|
2392
|
-
wrapMode="
|
|
2447
|
+
wrapMode="word"
|
|
2393
2448
|
scrollMargin={1}
|
|
2394
2449
|
focused={!settingsOpen && !helpOpen && !historyOpen && !agentSelectorOpen && !triggersOpen && !loginOpen && !questionnaire && !spawnPreview && !checkApproval}
|
|
2395
2450
|
onContentChange={handleTextareaChange}
|
package/src/check-mode.ts
CHANGED
|
@@ -262,12 +262,13 @@ export async function prepareCheck(
|
|
|
262
262
|
cwd: input.cwd,
|
|
263
263
|
projectCwd: cwd,
|
|
264
264
|
allowedPaths: additionalPaths,
|
|
265
|
+
protectedPaths: [AGENT_DIR],
|
|
265
266
|
profile,
|
|
266
267
|
});
|
|
267
268
|
} else {
|
|
268
269
|
const command = input && typeof input === "object" ? (input as { command?: unknown }).command : undefined;
|
|
269
270
|
if (typeof command !== "string") return { block: "Bash safety check requires a complete command string or process proposal" };
|
|
270
|
-
policy = analyzeCheckPolicy({ command, cwd, profile, allowedPaths: additionalPaths });
|
|
271
|
+
policy = analyzeCheckPolicy({ command, cwd, profile, allowedPaths: additionalPaths, protectedPaths: [AGENT_DIR] });
|
|
271
272
|
}
|
|
272
273
|
bash = policy.analysis;
|
|
273
274
|
if (!bash.complete || bash.truncated || !bash.syntaxBalanced) {
|
|
@@ -316,6 +317,7 @@ export async function prepareCheck(
|
|
|
316
317
|
decision: policy.decision,
|
|
317
318
|
reason: policy.reason,
|
|
318
319
|
findings: policy.findings,
|
|
320
|
+
accesses: policy.accesses,
|
|
319
321
|
} : undefined,
|
|
320
322
|
shell: processProposal ? undefined : bash,
|
|
321
323
|
process: processProposal ? {
|
|
@@ -379,6 +381,7 @@ export async function prepareCheck(
|
|
|
379
381
|
substitutionCount: bash.substitutions.length,
|
|
380
382
|
mutationIntent: bash.mutationIntent,
|
|
381
383
|
errors: bash.errors,
|
|
384
|
+
accesses: policy?.accesses,
|
|
382
385
|
} : undefined,
|
|
383
386
|
process: processProposal ? {
|
|
384
387
|
source: processProposal.source,
|
package/src/check-mutation.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { generateUnifiedPatch } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { lstat, readFile, realpath } from "node:fs/promises";
|
|
4
|
-
import { basename, dirname, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { basename, dirname, parse, relative, resolve, sep } from "node:path";
|
|
5
5
|
import { previewApplyPatch } from "./apply-patch";
|
|
6
6
|
import type { CheckedToolName } from "./check-approvals";
|
|
7
7
|
import {
|
|
@@ -85,6 +85,11 @@ async function validateEditPath(
|
|
|
85
85
|
const roots = await Promise.all([projectRoot, ...allowedPaths].map((path) => realpath(path)));
|
|
86
86
|
const absolute = resolve(projectRoot, inputPath);
|
|
87
87
|
const sortedRoots = roots.sort((first, second) => second.length - first.length);
|
|
88
|
+
if (process.platform === "win32" && windowsAbsolute(inputPath)) {
|
|
89
|
+
const targetRoot = parse(absolute).root.toLowerCase();
|
|
90
|
+
const hasAllowedVolume = sortedRoots.some((candidate) => parse(candidate).root.toLowerCase() === targetRoot);
|
|
91
|
+
if (!hasAllowedVolume) throw new Error(`Edit path is outside the allowed Check mode paths: ${inputPath}`);
|
|
92
|
+
}
|
|
88
93
|
let root = sortedRoots.find((candidate) => isPathInsideOrSame(candidate, absolute));
|
|
89
94
|
if (!root) {
|
|
90
95
|
let targetIdentity: string;
|