fix: error handling, sandbox env, file tool enforcement
- tools/file_*: hard-fail when OLLIE_SESSION_ID is unset; surface errors on all failure paths including dir checks, marker cleanup, and I/O - tools/file_read: treat nonexistent path as new file (emit "(new file)", write empty marker) so file_write can create new files - tools/file_write: colorize new-file lines with + (U+FF0B) - tools/file_glob, file_grep, memory_*, task_*: consistent error handling and output to stdout so the model sees all failures - tui/diffcolor.go: match +/− (U+FF0B/U+2212) lookalikes emitted by file_edit/file_write; fix +++ / --- header pattern - prompts: prohibit execute_code for file creation/modification; enforce execute_tool with file_write/file_edit instead - core submodule: surface isError from MCP responses; inject OLLIE_SESSION_ID into execute server at session creation Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f155ba91d0
commit
8ca905799b
2
core
2
core
|
|
@ -1 +1 @@
|
|||
Subproject commit 64d3915593c9600911cf332b9f0d42bc8a4e2d5d
|
||||
Subproject commit 655b9e706045f503b4889c6bee6e43261b50f4f0
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
You are ollie, an agentic assistant.
|
||||
|
||||
**Critical rule: never use `execute_code` to create or modify files. Always use `execute_tool` with `file_write` or `file_edit`. No exceptions.**
|
||||
|
||||
# Tone and style
|
||||
|
||||
- Format output as markdown. The frontend may render it as HTML, display it in a terminal, or show it as plain text — never rely on ANSI escape codes or terminal-specific formatting.
|
||||
|
|
@ -20,6 +22,7 @@ Be truthful. If the user is wrong, say so — do not soften, hedge, or dance aro
|
|||
# Tool preferences
|
||||
|
||||
- Make independent tool calls in parallel when there are no dependencies between them.
|
||||
- **Never use `execute_code` to create or modify files.** Use `execute_tool` with `file_write` or `file_edit` instead. This is a hard rule — there are no exceptions for "simple" files.
|
||||
|
||||
# Environment
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ Using `cat`, `sed`, `awk`, `grep`, `find`, `echo >`, or similar shell commands d
|
|||
|
||||
- **Read before writing.** Always call `file_read` on a file before `file_write` or `file_edit`. This is enforced — writes will fail without a prior read.
|
||||
- **Prefer `file_edit` over `file_write`** for partial changes.
|
||||
- **Never use `execute_code` or `execute_tool` for file I/O.** Reserve those for operations that genuinely require a shell: building, testing, git, process control.
|
||||
- **To create or modify a file with content you provide, always use `execute_tool` with `file_write` or `file_edit` — never `execute_code`.** Capturing process output to a file (e.g. `make > make.log`, `openssl genrsa > key.pem`) is fine.
|
||||
|
||||
## Examples
|
||||
|
||||
|
|
|
|||
|
|
@ -8,14 +8,27 @@ import hashlib
|
|||
import difflib
|
||||
|
||||
def marker_path(file_path):
|
||||
sid = os.environ.get('OLLIE_SESSION_ID', '')
|
||||
if not sid:
|
||||
print("error: OLLIE_SESSION_ID is not set")
|
||||
sys.exit(1)
|
||||
h = hashlib.sha256(file_path.encode()).hexdigest()[:64]
|
||||
sid = os.environ.get('OLLIE_SESSION_ID', '_')
|
||||
return f"/tmp/ollie/{sid}/{h}"
|
||||
|
||||
def unified_diff(path, old, new):
|
||||
old_lines = old.splitlines(keepends=True)
|
||||
new_lines = new.splitlines(keepends=True)
|
||||
return ''.join(difflib.unified_diff(old_lines, new_lines, fromfile=path, tofile=path))
|
||||
raw = ''.join(difflib.unified_diff(old_lines, new_lines, fromfile=path, tofile=path))
|
||||
# Replace leading +/- with Unicode lookalikes so only the colorizer's
|
||||
# explicit patterns trigger, not incidental ASCII +/- in content.
|
||||
out = []
|
||||
for line in raw.splitlines(keepends=True):
|
||||
if line.startswith('+'):
|
||||
line = '+' + line[1:]
|
||||
elif line.startswith('-'):
|
||||
line = '−' + line[1:]
|
||||
out.append(line)
|
||||
return ''.join(out)
|
||||
|
||||
def find_exact(content, find):
|
||||
return find if find in content else None
|
||||
|
|
@ -124,7 +137,11 @@ except OSError as e:
|
|||
print(f"error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
os.unlink(marker)
|
||||
try:
|
||||
os.unlink(marker)
|
||||
except OSError as e:
|
||||
print(f"error: failed to remove marker: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
diff = unified_diff(file_path, original, new_content)
|
||||
print(diff if diff else "(no changes)", end='' if diff else '\n')
|
||||
|
|
|
|||
|
|
@ -7,8 +7,23 @@ search_path="${2:-$PWD}"
|
|||
|
||||
[ -z "$pattern" ] && echo "usage: file_glob <pattern> [path]" && exit 1
|
||||
|
||||
if [ ! -d "$search_path" ]; then
|
||||
echo "error: path is not a directory: $search_path"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# collect matches and sort by mtime descending
|
||||
rg --no-config --files -g "$pattern" "$search_path" 2>/dev/null \
|
||||
| xargs -d '\n' stat --printf '%Y\t%n\n' 2>/dev/null \
|
||||
rg_out=$(rg --no-config --files -g "$pattern" "$search_path" 2>&1)
|
||||
rg_exit=$?
|
||||
if [ $rg_exit -gt 1 ]; then
|
||||
echo "error: $rg_out"
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "$rg_out" ]; then
|
||||
echo "no matches"
|
||||
exit 0
|
||||
fi
|
||||
echo "$rg_out" \
|
||||
| xargs -d '\n' stat --printf '%Y\t%n\n' 2>&1 \
|
||||
| sort -rn \
|
||||
| cut -f2-
|
||||
|
|
|
|||
|
|
@ -57,9 +57,10 @@ cmd += ['-e', args.pattern, search_path]
|
|||
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
if result.returncode == 1:
|
||||
sys.exit(0) # no matches
|
||||
print("no matches")
|
||||
sys.exit(0)
|
||||
if result.returncode > 1:
|
||||
print(result.stderr.strip() or f"rg exited with {result.returncode}", file=sys.stderr)
|
||||
print(f"error: {result.stderr.strip() or f'rg exited with {result.returncode}'}")
|
||||
sys.exit(1)
|
||||
|
||||
lines = result.stdout.splitlines()
|
||||
|
|
|
|||
|
|
@ -10,8 +10,21 @@ max_size=$((8 * 1024 * 1024))
|
|||
|
||||
[ -z "$file_path" ] && echo "usage: file_read <file_path> [offset] [limit]" && exit 1
|
||||
[[ "$file_path" != /* ]] && echo "error: path must be absolute, got: $file_path" && exit 1
|
||||
[ ! -e "$file_path" ] && echo "error: file not found: $file_path" && exit 1
|
||||
[ ! -f "$file_path" ] && echo "error: not a regular file: $file_path" && exit 1
|
||||
[ ! -f "$file_path" ] && [ -e "$file_path" ] && echo "error: not a regular file: $file_path" && exit 1
|
||||
|
||||
if [ -z "$OLLIE_SESSION_ID" ]; then
|
||||
echo "error: OLLIE_SESSION_ID is not set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "/tmp/ollie/$OLLIE_SESSION_ID"
|
||||
marker="/tmp/ollie/$OLLIE_SESSION_ID/$(printf '%s' "$file_path" | sha256sum | cut -c1-64)"
|
||||
|
||||
if [ ! -e "$file_path" ]; then
|
||||
echo "(new file)"
|
||||
: > "$marker"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
size=$(wc -c < "$file_path")
|
||||
[ "$size" -gt "$max_size" ] && echo "error: file too large ($size bytes, max $max_size)" && exit 1
|
||||
|
|
@ -31,6 +44,4 @@ else
|
|||
fi
|
||||
|
||||
# save content to marker (used by file_edit/file_write as staging area)
|
||||
mkdir -p "/tmp/ollie/$OLLIE_SESSION_ID"
|
||||
marker="/tmp/ollie/$OLLIE_SESSION_ID/$(printf '%s' "$file_path" | sha256sum | cut -c1-64)"
|
||||
cp "$file_path" "$marker"
|
||||
|
|
|
|||
|
|
@ -8,8 +8,11 @@ import hashlib
|
|||
import difflib
|
||||
|
||||
def marker_path(file_path):
|
||||
sid = os.environ.get('OLLIE_SESSION_ID', '')
|
||||
if not sid:
|
||||
print("error: OLLIE_SESSION_ID is not set")
|
||||
sys.exit(1)
|
||||
h = hashlib.sha256(file_path.encode()).hexdigest()[:64]
|
||||
sid = os.environ.get('OLLIE_SESSION_ID', '_')
|
||||
return f"/tmp/ollie/{sid}/{h}"
|
||||
|
||||
def unified_diff(path, old, new):
|
||||
|
|
@ -39,6 +42,11 @@ if exists:
|
|||
with open(marker, 'r', errors='replace') as f:
|
||||
old_content = f.read()
|
||||
|
||||
parent = os.path.dirname(file_path)
|
||||
if parent and not os.path.isdir(parent):
|
||||
print(f"error: parent directory does not exist: {parent}")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
with open(marker, 'w') as f:
|
||||
f.write(content)
|
||||
|
|
@ -48,11 +56,15 @@ except OSError as e:
|
|||
print(f"error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
os.unlink(marker)
|
||||
try:
|
||||
os.unlink(marker)
|
||||
except OSError as e:
|
||||
print(f"error: failed to remove marker: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
if not exists:
|
||||
for line in content.splitlines():
|
||||
print(f"+{line}")
|
||||
print(f"+{line}")
|
||||
else:
|
||||
diff = unified_diff(file_path, old_content, content)
|
||||
print(diff if diff else "(no changes)", end='' if diff else '\n')
|
||||
|
|
|
|||
|
|
@ -7,10 +7,20 @@
|
|||
mount="${OLLIE:-$HOME/mnt/ollie}"
|
||||
mem_dir="$mount/m"
|
||||
|
||||
if [ ! -d "$mem_dir" ]; then
|
||||
echo "error: memory directory does not exist: $mem_dir"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
query="$*"
|
||||
|
||||
# Search filenames (title/tag match) and file bodies
|
||||
matches=$(grep -ril "$query" "$mem_dir" 2>/dev/null)
|
||||
matches=$(grep -ril "$query" "$mem_dir" 2>&1)
|
||||
grep_exit=$?
|
||||
if [ $grep_exit -gt 1 ]; then
|
||||
echo "error: $matches"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$matches" ]; then
|
||||
echo "no matches for: $query"
|
||||
|
|
@ -19,6 +29,6 @@ fi
|
|||
|
||||
while IFS= read -r file; do
|
||||
echo "=== $(basename "$file") ==="
|
||||
cat "$file"
|
||||
cat "$file" || echo "error: could not read $file"
|
||||
echo
|
||||
done <<< "$matches"
|
||||
|
|
|
|||
|
|
@ -18,6 +18,10 @@ body = sys.argv[3]
|
|||
mount = os.environ.get("OLLIE") or os.path.expanduser("~/mnt/ollie")
|
||||
mem_dir = os.path.join(mount, "m")
|
||||
|
||||
if not os.path.isdir(mem_dir):
|
||||
print(f"error: memory directory does not exist: {mem_dir}")
|
||||
sys.exit(1)
|
||||
|
||||
now = datetime.now()
|
||||
ident = now.strftime("%Y%m%dT%H%M%S")
|
||||
date = now.strftime("%Y-%m-%d %a %H:%M")
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
import sys, os, re
|
||||
|
||||
if len(sys.argv) < 3:
|
||||
print("usage: task_complete <plan-file> <step-substring>", file=sys.stderr)
|
||||
print("usage: task_complete <plan-file> <step-substring>", file=sys.stdout)
|
||||
sys.exit(1)
|
||||
|
||||
ollie = os.environ.get('OLLIE', os.path.expanduser('~/mnt/ollie'))
|
||||
|
|
@ -14,8 +14,16 @@ plan_dir = os.path.join(ollie, 'pl')
|
|||
filepath = os.path.join(plan_dir, os.path.basename(sys.argv[1]))
|
||||
substring = sys.argv[2].lower()
|
||||
|
||||
with open(filepath) as f:
|
||||
lines = f.readlines()
|
||||
if not os.path.exists(filepath):
|
||||
print(f"error: plan file not found: {filepath}", file=sys.stdout)
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
with open(filepath) as f:
|
||||
lines = f.readlines()
|
||||
except OSError as e:
|
||||
print(f"error: {e}", file=sys.stdout)
|
||||
sys.exit(1)
|
||||
|
||||
matched = False
|
||||
for i, line in enumerate(lines):
|
||||
|
|
@ -25,7 +33,7 @@ for i, line in enumerate(lines):
|
|||
break
|
||||
|
||||
if not matched:
|
||||
print(f"no unchecked step matching {sys.argv[2]!r}", file=sys.stderr)
|
||||
print(f"no unchecked step matching {sys.argv[2]!r}", file=sys.stdout)
|
||||
sys.exit(1)
|
||||
|
||||
remaining = [line[6:].strip() for line in lines if re.match(r'^- \[ \] ', line)]
|
||||
|
|
@ -40,11 +48,19 @@ elif not remaining and '__wip' in filepath:
|
|||
else:
|
||||
newpath = filepath
|
||||
|
||||
with open(filepath, 'w') as f:
|
||||
f.writelines(lines)
|
||||
try:
|
||||
with open(filepath, 'w') as f:
|
||||
f.writelines(lines)
|
||||
except OSError as e:
|
||||
print(f"error: failed to write {filepath}: {e}", file=sys.stdout)
|
||||
sys.exit(1)
|
||||
|
||||
if newpath != filepath:
|
||||
os.rename(filepath, newpath)
|
||||
try:
|
||||
os.rename(filepath, newpath)
|
||||
except OSError as e:
|
||||
print(f"error: failed to rename to {newpath}: {e}", file=sys.stdout)
|
||||
sys.exit(1)
|
||||
|
||||
if remaining:
|
||||
print('\n'.join(f'- {s}' for s in remaining))
|
||||
|
|
|
|||
|
|
@ -8,14 +8,19 @@ OLLIE="${OLLIE:-$HOME/mnt/ollie}"
|
|||
PLAN_DIR="$OLLIE/pl"
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "usage: task_plan <goal> <step1> [step2] ..." >&2
|
||||
echo "usage: task_plan <goal> <step1> [step2] ..."
|
||||
exit 1
|
||||
fi
|
||||
GOAL="$1"
|
||||
shift
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "error: at least one step required" >&2
|
||||
echo "error: at least one step required"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -d "$PLAN_DIR" ]; then
|
||||
echo "error: plan directory does not exist: $PLAN_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
|
@ -36,6 +41,6 @@ FILEPATH="$PLAN_DIR/$FILENAME"
|
|||
for step in "$@"; do
|
||||
printf '- [ ] %s\n' "$step"
|
||||
done
|
||||
} > "$FILEPATH"
|
||||
} > "$FILEPATH" || { echo "error: failed to write $FILEPATH"; exit 1; }
|
||||
|
||||
echo "$FILENAME"
|
||||
|
|
|
|||
Loading…
Reference in New Issue