Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
87 commits
Select commit Hold shift + click to select a range
0c485e7
readonly-bash: add bash safety classifier with destructive command bl…
ofriw May 27, 2026
33b29c7
state+tui: add readonlyEnabled and readonlyNudgePending state fields …
ofriw May 27, 2026
671fe7a
readonly: add toggle, tool_call blocking, nudges, session lifecycle, …
ofriw May 27, 2026
e7b07b7
Remove dead-end '/readonly' instruction from model-facing readonly gu…
ofriw May 27, 2026
76439d3
Update readonly toggle messages and include handoff in block list des…
ofriw May 27, 2026
3c48eaa
Block handoff at tool_call layer in readonly mode
ofriw May 27, 2026
721c858
Gate /handoff command in readonly mode and apply --readonly CLI flag …
ofriw May 27, 2026
3686a5c
Add readonly-aware guidance to notebook boundary hint, watchdog nudge…
ofriw May 27, 2026
8ffc797
Rewrite bash classifier with shell-aware pipeline, code editor detect…
ofriw May 27, 2026
6540936
Add tests for readonly handoff blocking, bash classifier, context nud…
ofriw May 27, 2026
469a044
Add resolve-real-path utility and IDE config poisoning prevention
ofriw May 31, 2026
28ddffc
Add OS-level sandboxing for readonly bash on macOS (sandbox-exec) and…
ofriw May 31, 2026
0e009bd
Rewrite bash classifier with shell-aware pipeline, git allowlist, and…
ofriw May 31, 2026
27845c5
Refine config validator with case-insensitive key matching and MCP co…
ofriw May 31, 2026
58fd869
Add watchdog throttling and merge readonly nudges into context hook
ofriw May 31, 2026
4502977
Add readonly bash and config-validated write/edit tools to child spaw…
ofriw May 31, 2026
32de094
Update tests for new bash classifier, config validator, readonly chil…
ofriw May 31, 2026
e562b63
Remove standalone config-validator.ts and all references
ofriw Jun 2, 2026
fa8b893
Simplify bash guard from 3 enforcement layers to 2
ofriw Jun 2, 2026
2f5f520
Add user-friendly sandbox denial messages
ofriw Jun 2, 2026
2db1202
Remove debug logging from OS sandbox availability checks
ofriw Jun 2, 2026
d043d17
Ignore TypeScript compilation output
ofriw Jun 2, 2026
c6d0df9
Block wget without output flags to prevent unintended disk writes
ofriw Jun 2, 2026
26d3b27
Fix sed -i argument parsing for macOS empty backup extension
ofriw Jun 2, 2026
709513a
Allow bare git tag as read-only command
ofriw Jun 2, 2026
3da9806
Guard ui.notify behind hasUI check to prevent headless crash
ofriw Jun 2, 2026
0d41de0
Use type-safe tool call event handling and fix sandbox mutation path
ofriw Jun 2, 2026
64e911b
Ignore PR review artifacts in .gitignore
ofriw Jun 2, 2026
c1fad71
Remove TUI-corrupting console diagnostics
ofriw Jun 2, 2026
b3ccbe4
Share canonical temp dir outside readonly guard
ofriw Jun 2, 2026
84acc41
Allow blank readonly bash commands
ofriw Jun 2, 2026
b898b1a
Allow wget stdout output in readonly mode
ofriw Jun 2, 2026
320e0cf
Add readonly classifier edge-case tests
ofriw Jun 2, 2026
a9075c9
Fix critical rm/rmdir/unlink/mkdir mutation bypass
ofriw Jun 3, 2026
9d12a06
Fix flag-value skip for truncate/touch --no-create
ofriw Jun 3, 2026
fa34723
Fix sed -e expression values leaking as false targets
ofriw Jun 3, 2026
00d75e2
Fix package mutation false positive on 'build'
ofriw Jun 3, 2026
d739362
Resolve glob patterns and tilde expansion in temp path check
ofriw Jun 3, 2026
7e3f78c
Add type guard for malformed bash input
ofriw Jun 3, 2026
b661a13
Guard readonly toggle behind hasUI check
ofriw Jun 3, 2026
e1fdfa0
Optimize and compact context primer
ofriw Jun 3, 2026
039d200
Add AGENTS.md project instructions
ofriw Jun 3, 2026
1f703a9
Add null guards for notebook branch rehydration
ofriw Jun 3, 2026
3e00c91
Add null guards for readonly branch rehydration on session_start
ofriw Jun 3, 2026
1ebfb4f
Fix bash classifier bypasses: sudo -h, env --split-string, process su…
ofriw Jun 3, 2026
f833a36
Add sandbox functional health probes for bwrap and sandbox-exec
ofriw Jun 3, 2026
f44c31a
Switch bwrap from /bin/sh to /bin/bash
ofriw Jun 3, 2026
ba2645c
Harden sandbox profile path quoting against injection
ofriw Jun 3, 2026
420392a
Remove process.stderr.write calls that corrupt TUI rendering
ofriw Jun 4, 2026
ff03361
detect curl -O/--remote-name and document known L2 bypasses
ofriw Jun 5, 2026
4a81348
Merge branch 'main' into feat/readonly
ofriw Jun 5, 2026
0c88424
Refactor handoff state and command flow for readonly+handoff
ofriw Jun 8, 2026
3eb5f3b
Update readonly guardrails, watchdog nudges, and UI text for readonly…
ofriw Jun 8, 2026
8febb87
Update tests for readonly+handoff flow
ofriw Jun 8, 2026
f2f4977
Detect --remote-name-all and add CURL_VALUE_SHORT_FLAGS for curl remo…
ofriw Jun 10, 2026
3511bb2
Add regression tests for curl value-consuming flags not confused with -O
ofriw Jun 10, 2026
97f9a4f
Add pip3, pipx to blocklist and npm/yarn short verb forms
ofriw Jun 10, 2026
fcc22c9
Fix sed -i -e bare-filename bypass
ofriw Jun 10, 2026
410b4a5
Suppress >= false-positive in redirect scanner
ofriw Jun 10, 2026
05a3ad5
Add *.d.ts, *.d.cts, *.d.mts to .gitignore
ofriw Jun 11, 2026
6112e0a
Propagate shellVars through mutation classification pipeline
ofriw Jun 11, 2026
e644e91
Detect temp-dir download flags for wget and curl
ofriw Jun 11, 2026
4d408f2
Add sandbox test verifying temp-only write enforcement
ofriw Jun 11, 2026
163079e
add @types/node dev dependency
ofriw Jun 14, 2026
786dfb2
support shell parameter expansion defaults in path validation
ofriw Jun 14, 2026
24004f9
block unresolved command-substitution paths outside temp
ofriw Jun 14, 2026
1d97ebd
validate mktemp flags before synthesizing temp synthetic paths
ofriw Jun 14, 2026
10bc5f7
Merge branch 'main' into feat/readonly
ofriw Jun 14, 2026
c9cee68
Extract getReadonlyFromBranch pure function from inline rehydration l…
ofriw Jun 16, 2026
1fd4e04
Remove redundant malformed bash input guard (deferred to applyReadonl…
ofriw Jun 16, 2026
b667853
Export buildEnrichedTask and extract filterReadonlyToolNames utility
ofriw Jun 16, 2026
84ef03a
Add registerReadonlyPI, makeReadonlyUICtx, ToolCall type, and flag/sh…
ofriw Jun 16, 2026
0550cf7
Delete monolithic readonly bash, lifecycle, and nudge test files
ofriw Jun 16, 2026
2846987
Add behavioral contract tests for readonly bash command classification
ofriw Jun 16, 2026
16130de
Add OS sandbox behavioral tests and resolvePath unit tests
ofriw Jun 16, 2026
2f8f57b
Simplify readonly-handoff test with shared registerReadonlyPI helpers
ofriw Jun 16, 2026
b764a44
Simplify readonly-mode test with shared registerReadonlyPI helpers
ofriw Jun 16, 2026
2bf4843
Simplify readonly-spawn test with shared spawnWithCapture helper
ofriw Jun 16, 2026
2ec8db4
Add truncateText multi-byte boundary test to spawn.test.ts
ofriw Jun 16, 2026
c07f1be
Add buildChildToolNames 2-arg fallback tests to spawn.test.ts
ofriw Jun 16, 2026
ca474c4
Remove fragile warning-count assertions from spawn and spawn-event tests
ofriw Jun 16, 2026
55f20e2
Add notebook rehydration null and malformed branch entry guard test
ofriw Jun 16, 2026
af2ff4e
Add readonly indicator and widget tests to tui-indicators.test
ofriw Jun 16, 2026
013f02e
Rewrite watchdog test to exercise real /handoff command instead of ma…
ofriw Jun 16, 2026
46035f5
Fix type mismatches and state shape for SDK v0.78.1
ofriw Jun 17, 2026
dbb3831
Expand hidden-file globs for Node v24+ dotfile handling
ofriw Jun 17, 2026
997d743
Add Husky pre-commit hook blocking type errors
ofriw Jun 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ web_modules/
# TypeScript cache
*.tsbuildinfo

# TypeScript compilation output
*.js
*.d.ts
*.d.cts
*.d.mts

# Optional npm cache directory
.npm

Expand Down Expand Up @@ -148,3 +154,10 @@ vite.config.ts.timestamp-*
.chunkhound.json
.chunkhound/
.mcp.json

# macOS
.DS_Store

# PR review artifacts
AGENT_REVIEW.md
HUMAN_REVIEW.md
1 change: 1 addition & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
npm run typecheck
5 changes: 5 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# TUI Safety

**Never use `console.debug/warn/error/log`** — writes to stdout/stderr corrupt pi's TUI ANSI rendering. Extension host runs in the same process.

Use `ctx.ui.notify()` / `setStatus()` / `setWidget()` instead. For diagnostics, remove entirely.
18 changes: 15 additions & 3 deletions handoff/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,19 @@ export function registerHandoffCommand(pi: ExtensionAPI, state: AgenticodingStat
}

state.pendingRequestedHandoff = {
direction,
enforcementAttempts: 0,
toolCalled: false,
readonlyBypassActive: state.readonlyEnabled,
resumeReadonlyAfterHandoff: state.readonlyEnabled,
enforcementAttempts: 0,
};

if (ctx.hasUI && state.readonlyEnabled) {
ctx.ui.notify(
"Readonly is active. A temporary handoff-only exception is now enabled for this required handoff. The fresh context after handoff will resume in readonly mode.",
"info",
);
}

// Show live progress indicator in footer
if (ctx.hasUI && ctx.ui.theme) {
ctx.ui.setStatus(
Expand All @@ -36,8 +44,12 @@ export function registerHandoffCommand(pi: ExtensionAPI, state: AgenticodingStat
);
}

const readonlyNotice = state.readonlyEnabled
? "\n\nReadonly remains active for normal mutations: write, edit, and non-temp bash writes stay blocked. A temporary exception allows the handoff tool for this request only. You must perform a real handoff now rather than continue normal work. The fresh context after compaction will resume in readonly mode with this handoff exception cleared, so draft the brief for a readonly continuation."
: "\n\nYou must perform a real handoff now rather than continue normal work.";

pi.sendUserMessage(
`Handoff direction: ${direction}\n\nPrepare a handoff in the current session. First, save any durable reusable knowledge that aligns with the direction above to the notebook: findings worth keeping, constraints discovered, decisions made, or other grounding future contexts will need. Then draft a concise but sufficiently detailed handoff brief capturing only the remaining situational context: current state, blockers, unresolved questions, failed paths worth avoiding, and next steps. The next context will read the notebook on demand, so do not duplicate notebook content in the brief. Use any structure that makes the next work unambiguous. Reference notebook pages by name when relevant.`,
`Handoff direction: ${direction}\n\nThe user explicitly requested /handoff. Prepare a handoff in the current session. First, save any durable reusable knowledge that aligns with the direction above to the notebook: findings worth keeping, constraints discovered, decisions made, or other grounding future contexts will need. Then draft a concise but sufficiently detailed handoff brief capturing only the remaining situational context: current state, blockers, unresolved questions, failed paths worth avoiding, and next steps. The next context will read the notebook on demand, so do not duplicate notebook content in the brief. Use any structure that makes the next work unambiguous. Reference notebook pages by name when relevant.${readonlyNotice}`,
ctx.isIdle() ? undefined : { deliverAs: "followUp" },
);
},
Expand Down
13 changes: 3 additions & 10 deletions handoff/compact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,22 @@

import type { ExtensionAPI, ExtensionContext, SessionEntry } from "@earendil-works/pi-coding-agent";
import type { AgenticodingState } from "../state.js";
import { clearActiveNotebookTopic } from "../notebook/topic.js";
import { STATUS_KEY_HANDOFF } from "../tui.js";

function getImpossibleKeptId(branchEntries: SessionEntry[]): string {
const leaf = branchEntries[branchEntries.length - 1];
return `${leaf?.id ?? "handoff"}-handoff-cut`;
}

export function registerHandoffCompaction(pi: ExtensionAPI, state: AgenticodingState): void {
pi.on("session_before_compact", async (event, ctx: ExtensionContext) => {
pi.on("session_before_compact", async (event, _ctx: ExtensionContext) => {
const pending = state.pendingHandoff;
if (!pending) {
return;
}

state.pendingHandoff = null;
state.pendingRequestedHandoff = null;
clearActiveNotebookTopic(state);

// Clear the handoff progress indicator now that compaction is consuming it
if (ctx.hasUI) {
ctx.ui.setStatus(STATUS_KEY_HANDOFF, undefined);
}
// Keep pendingRequestedHandoff — compaction must complete successfully first.
// onComplete in handoff/tool.ts clears it after success; onError preserves it for retry.

return {
compaction: {
Expand Down
34 changes: 25 additions & 9 deletions handoff/tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";
import { clearActiveNotebookTopic } from "../notebook/topic.js";
import type { AgenticodingState } from "../state.js";
import { STATUS_KEY_HANDOFF } from "../tui.js";

Expand All @@ -19,7 +20,7 @@ import { STATUS_KEY_HANDOFF } from "../tui.js";
*
* Shape: handoff primer + original task.
*/
function buildEnrichedTask(task: string): string {
export function buildEnrichedTask(task: string, options?: { resumeReadonlyAfterHandoff?: boolean }): string {
const parts: string[] = [
"## Handoff — Continue Previous Work",
"",
Expand All @@ -29,12 +30,20 @@ function buildEnrichedTask(task: string): string {
"- Use `notebook_index` to scan available pages when needed",
"- Use `spawn` to delegate isolated subtasks to child agents",
"- Build on notebook grounding and this brief rather than reconstructing old context",
"",
"## Task",
"",
task,
];

if (options?.resumeReadonlyAfterHandoff) {
parts.push(
"",
"## Execution Constraints",
"",
"- Fresh context resumes in readonly mode.",
"- The temporary handoff-only exception used to reach this context is no longer active.",
"- Write, edit, and non-temp bash filesystem mutations remain blocked unless the user changes readonly mode.",
);
}

parts.push("", "## Task", "", task);
return parts.join("\n");
}

Expand Down Expand Up @@ -79,18 +88,25 @@ export function registerHandoffTool(
}),

async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
const enrichedTask = buildEnrichedTask(params.task);
const requestedHandoff = state.pendingRequestedHandoff;
const enrichedTask = buildEnrichedTask(params.task, {
resumeReadonlyAfterHandoff: requestedHandoff?.resumeReadonlyAfterHandoff === true,
});
state.pendingHandoff = { task: enrichedTask, source: "tool" };
if (state.pendingRequestedHandoff) {
state.pendingRequestedHandoff.toolCalled = true;
if (requestedHandoff) {
requestedHandoff.toolCalled = true;
}
ctx.compact({
onComplete: () => {
clearActiveNotebookTopic(state);
state.pendingRequestedHandoff = null;
if (ctx.hasUI) {
ctx.ui.setStatus(STATUS_KEY_HANDOFF, undefined);
}
pi.sendUserMessage("Proceed.");
},
onError: () => {
state.pendingHandoff = null;
// Safe: pendingRequestedHandoff may already be cleaned up by watchdog
if (state.pendingRequestedHandoff) {
state.pendingRequestedHandoff.toolCalled = false;
}
Expand Down
Loading
Loading